summaryrefslogtreecommitdiff
path: root/browser/devtools
diff options
context:
space:
mode:
authorwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
committerwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
commitd25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch)
tree16ec27edc7d5f83986f16236d3a36a2682a0f37e /browser/devtools
parenta942906574671868daf122284a9c4689e6924f74 (diff)
downloadpalemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'browser/devtools')
-rw-r--r--browser/devtools/Makefile.in15
-rw-r--r--browser/devtools/commandline/BuiltinCommands.jsm2169
-rw-r--r--browser/devtools/commandline/Commands.jsm17
-rw-r--r--browser/devtools/commandline/Makefile.in16
-rw-r--r--browser/devtools/commandline/commandline.css59
-rw-r--r--browser/devtools/commandline/commandlineoutput.xhtml18
-rw-r--r--browser/devtools/commandline/commandlinetooltip.xhtml19
-rw-r--r--browser/devtools/commandline/gcli.jsm20
-rw-r--r--browser/devtools/commandline/moz.build7
-rw-r--r--browser/devtools/commandline/test/Makefile.in74
-rw-r--r--browser/devtools/commandline/test/browser_cmd_addon.js153
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid.js102
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache55
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^2
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_index.html14
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_page1.html14
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_page2.html14
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html14
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^2
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid.js179
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache5
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^2
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html13
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_page1.html13
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_page2.html13
-rw-r--r--browser/devtools/commandline/test/browser_cmd_appcache_valid_page3.html13
-rw-r--r--browser/devtools/commandline/test/browser_cmd_calllog.js114
-rw-r--r--browser/devtools/commandline/test/browser_cmd_calllog_chrome.js113
-rw-r--r--browser/devtools/commandline/test/browser_cmd_commands.js71
-rw-r--r--browser/devtools/commandline/test/browser_cmd_cookie.html18
-rw-r--r--browser/devtools/commandline/test/browser_cmd_cookie.js170
-rw-r--r--browser/devtools/commandline/test/browser_cmd_jsb.js86
-rw-r--r--browser/devtools/commandline/test/browser_cmd_jsb_script.jsi2
-rw-r--r--browser/devtools/commandline/test/browser_cmd_pagemod_export.html25
-rw-r--r--browser/devtools/commandline/test/browser_cmd_pagemod_export.js376
-rw-r--r--browser/devtools/commandline/test/browser_cmd_pref.js502
-rw-r--r--browser/devtools/commandline/test/browser_cmd_restart.js35
-rw-r--r--browser/devtools/commandline/test/browser_cmd_screenshot.html6
-rw-r--r--browser/devtools/commandline/test/browser_cmd_screenshot.js195
-rw-r--r--browser/devtools/commandline/test/browser_cmd_settings.js136
-rw-r--r--browser/devtools/commandline/test/browser_gcli_async.js169
-rw-r--r--browser/devtools/commandline/test/browser_gcli_canon.js271
-rw-r--r--browser/devtools/commandline/test/browser_gcli_cli.js1322
-rw-r--r--browser/devtools/commandline/test/browser_gcli_completion.js537
-rw-r--r--browser/devtools/commandline/test/browser_gcli_context.js276
-rw-r--r--browser/devtools/commandline/test/browser_gcli_date.js247
-rw-r--r--browser/devtools/commandline/test/browser_gcli_exec.js657
-rw-r--r--browser/devtools/commandline/test/browser_gcli_fail.js99
-rw-r--r--browser/devtools/commandline/test/browser_gcli_focus.js80
-rw-r--r--browser/devtools/commandline/test/browser_gcli_history.js84
-rw-r--r--browser/devtools/commandline/test/browser_gcli_incomplete.js398
-rw-r--r--browser/devtools/commandline/test/browser_gcli_inputter.js105
-rw-r--r--browser/devtools/commandline/test/browser_gcli_intro.js85
-rw-r--r--browser/devtools/commandline/test/browser_gcli_js.js475
-rw-r--r--browser/devtools/commandline/test/browser_gcli_keyboard1.js111
-rw-r--r--browser/devtools/commandline/test/browser_gcli_keyboard2.js413
-rw-r--r--browser/devtools/commandline/test/browser_gcli_keyboard3.js91
-rw-r--r--browser/devtools/commandline/test/browser_gcli_menu.js64
-rw-r--r--browser/devtools/commandline/test/browser_gcli_node.js346
-rw-r--r--browser/devtools/commandline/test/browser_gcli_remote.js462
-rw-r--r--browser/devtools/commandline/test/browser_gcli_resource.js160
-rw-r--r--browser/devtools/commandline/test/browser_gcli_scratchpad.js72
-rw-r--r--browser/devtools/commandline/test/browser_gcli_spell.js58
-rw-r--r--browser/devtools/commandline/test/browser_gcli_split.js99
-rw-r--r--browser/devtools/commandline/test/browser_gcli_string.js293
-rw-r--r--browser/devtools/commandline/test/browser_gcli_tokenize.js303
-rw-r--r--browser/devtools/commandline/test/browser_gcli_tooltip.js162
-rw-r--r--browser/devtools/commandline/test/browser_gcli_types.js106
-rw-r--r--browser/devtools/commandline/test/browser_gcli_util.js53
-rw-r--r--browser/devtools/commandline/test/head.js22
-rw-r--r--browser/devtools/commandline/test/helpers.js1038
-rw-r--r--browser/devtools/commandline/test/mockCommands.js575
-rw-r--r--browser/devtools/commandline/test/moz.build6
-rw-r--r--browser/devtools/debugger/CmdDebugger.jsm454
-rw-r--r--browser/devtools/debugger/DebuggerPanel.jsm101
-rw-r--r--browser/devtools/debugger/DebuggerProcess.jsm164
-rw-r--r--browser/devtools/debugger/Makefile.in15
-rw-r--r--browser/devtools/debugger/debugger-controller.js1540
-rw-r--r--browser/devtools/debugger/debugger-panes.js2317
-rw-r--r--browser/devtools/debugger/debugger-toolbar.js1611
-rw-r--r--browser/devtools/debugger/debugger-view.js850
-rw-r--r--browser/devtools/debugger/debugger.css24
-rw-r--r--browser/devtools/debugger/debugger.xul375
-rw-r--r--browser/devtools/debugger/moz.build7
-rw-r--r--browser/devtools/debugger/test/Makefile.in164
-rw-r--r--browser/devtools/debugger/test/binary_search.coffee18
-rw-r--r--browser/devtools/debugger/test/binary_search.html12
-rw-r--r--browser/devtools/debugger/test/binary_search.js29
-rw-r--r--browser/devtools/debugger/test/binary_search.map10
-rw-r--r--browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js73
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bfcache.js129
-rw-r--r--browser/devtools/debugger/test/browser_dbg_big-data.html27
-rw-r--r--browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js154
-rw-r--r--browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.html20
-rw-r--r--browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.js92
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js308
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-contextmenu.js466
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-highlight.js224
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js301
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js241
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js394
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js127
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug737803_editor_actual_location.js121
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js393
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js583
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js129
-rw-r--r--browser/devtools/debugger/test/browser_dbg_bug868163_highight_on_pause.js78
-rw-r--r--browser/devtools/debugger/test/browser_dbg_chrome-debugging.js68
-rw-r--r--browser/devtools/debugger/test/browser_dbg_clean-exit.js43
-rw-r--r--browser/devtools/debugger/test/browser_dbg_cmd.html48
-rw-r--r--browser/devtools/debugger/test/browser_dbg_cmd.js108
-rw-r--r--browser/devtools/debugger/test/browser_dbg_cmd_break.html19
-rw-r--r--browser/devtools/debugger/test/browser_dbg_cmd_break.js220
-rw-r--r--browser/devtools/debugger/test/browser_dbg_conditional-breakpoints.html30
-rw-r--r--browser/devtools/debugger/test/browser_dbg_createChrome.js93
-rw-r--r--browser/devtools/debugger/test/browser_dbg_debuggerstatement.html18
-rw-r--r--browser/devtools/debugger/test/browser_dbg_debuggerstatement.js70
-rw-r--r--browser/devtools/debugger/test/browser_dbg_displayName.html29
-rw-r--r--browser/devtools/debugger/test/browser_dbg_displayName.js78
-rw-r--r--browser/devtools/debugger/test/browser_dbg_frame-parameters.html36
-rw-r--r--browser/devtools/debugger/test/browser_dbg_function-search-01.html17
-rw-r--r--browser/devtools/debugger/test/browser_dbg_function-search-02.html29
-rw-r--r--browser/devtools/debugger/test/browser_dbg_function-search.js499
-rw-r--r--browser/devtools/debugger/test/browser_dbg_globalactor-01.js65
-rw-r--r--browser/devtools/debugger/test/browser_dbg_iframes.html12
-rw-r--r--browser/devtools/debugger/test/browser_dbg_iframes.js67
-rw-r--r--browser/devtools/debugger/test/browser_dbg_listtabs-01.js102
-rw-r--r--browser/devtools/debugger/test/browser_dbg_listtabs-02.js150
-rw-r--r--browser/devtools/debugger/test/browser_dbg_location-changes-blank.js108
-rw-r--r--browser/devtools/debugger/test/browser_dbg_location-changes-bp.js163
-rw-r--r--browser/devtools/debugger/test/browser_dbg_location-changes-new.js108
-rw-r--r--browser/devtools/debugger/test/browser_dbg_location-changes.js69
-rw-r--r--browser/devtools/debugger/test/browser_dbg_menustatus.js45
-rw-r--r--browser/devtools/debugger/test/browser_dbg_multiple-windows.js115
-rw-r--r--browser/devtools/debugger/test/browser_dbg_nav-01.js54
-rw-r--r--browser/devtools/debugger/test/browser_dbg_pane-collapse.js159
-rw-r--r--browser/devtools/debugger/test/browser_dbg_panesize-inner.js77
-rw-r--r--browser/devtools/debugger/test/browser_dbg_pause-exceptions.html30
-rw-r--r--browser/devtools/debugger/test/browser_dbg_pause-exceptions.js135
-rw-r--r--browser/devtools/debugger/test/browser_dbg_pause-resume.js93
-rw-r--r--browser/devtools/debugger/test/browser_dbg_pause-warning.js97
-rw-r--r--browser/devtools/debugger/test/browser_dbg_progress-listener-bug.js70
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-01.js165
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-02.js134
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-03.js211
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-04.js78
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-05.js95
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-06.js198
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-07.js106
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-08.js244
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-09.js105
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-10.js110
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-11.js241
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-12.js95
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js147
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js352
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js185
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-data.js888
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js119
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js514
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js516
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js438
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-03.js93
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-04.js93
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js284
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js249
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js254
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js324
-rw-r--r--browser/devtools/debugger/test/browser_dbg_propertyview-reexpand.js394
-rw-r--r--browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js79
-rw-r--r--browser/devtools/debugger/test/browser_dbg_reload-same-script.js218
-rw-r--r--browser/devtools/debugger/test/browser_dbg_script-switching-02.html13
-rw-r--r--browser/devtools/debugger/test/browser_dbg_script-switching.html14
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js304
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js267
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js337
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js285
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js162
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js150
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js262
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js208
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js693
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-searching-popup.js57
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-sorting.js124
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-switching-02.js175
-rw-r--r--browser/devtools/debugger/test/browser_dbg_scripts-switching.js178
-rw-r--r--browser/devtools/debugger/test/browser_dbg_select-line.js122
-rw-r--r--browser/devtools/debugger/test/browser_dbg_source_maps-01.js156
-rw-r--r--browser/devtools/debugger/test/browser_dbg_source_maps-02.js205
-rw-r--r--browser/devtools/debugger/test/browser_dbg_sources-cache.js167
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack-01.js54
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack-02.js86
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack-03.js71
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack-04.js65
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack-05.js114
-rw-r--r--browser/devtools/debugger/test/browser_dbg_stack.html32
-rw-r--r--browser/devtools/debugger/test/browser_dbg_step-out.js132
-rw-r--r--browser/devtools/debugger/test/browser_dbg_tab1.html11
-rw-r--r--browser/devtools/debugger/test/browser_dbg_tab2.html11
-rw-r--r--browser/devtools/debugger/test/browser_dbg_tabactor-01.js47
-rw-r--r--browser/devtools/debugger/test/browser_dbg_tabactor-02.js61
-rw-r--r--browser/devtools/debugger/test/browser_dbg_update-editor-mode.html16
-rw-r--r--browser/devtools/debugger/test/browser_dbg_update-editor-mode.js145
-rw-r--r--browser/devtools/debugger/test/browser_dbg_watch-expressions.html27
-rw-r--r--browser/devtools/debugger/test/browser_dbg_with-frame.html33
-rw-r--r--browser/devtools/debugger/test/head.js207
-rw-r--r--browser/devtools/debugger/test/moz.build6
-rw-r--r--browser/devtools/debugger/test/test-editor-mode6
-rw-r--r--browser/devtools/debugger/test/test-function-search-01.js42
-rw-r--r--browser/devtools/debugger/test/test-function-search-02.js21
-rw-r--r--browser/devtools/debugger/test/test-function-search-03.js32
-rw-r--r--browser/devtools/debugger/test/test-location-changes-bp.html17
-rw-r--r--browser/devtools/debugger/test/test-location-changes-bp.js7
-rw-r--r--browser/devtools/debugger/test/test-script-switching-01.js6
-rw-r--r--browser/devtools/debugger/test/test-script-switching-02.js11
-rw-r--r--browser/devtools/debugger/test/test-step-out.html37
-rw-r--r--browser/devtools/debugger/test/testactors.js31
-rw-r--r--browser/devtools/fontinspector/font-inspector.css16
-rw-r--r--browser/devtools/fontinspector/font-inspector.js231
-rw-r--r--browser/devtools/fontinspector/font-inspector.xhtml42
-rw-r--r--browser/devtools/fontinspector/moz.build7
-rw-r--r--browser/devtools/fontinspector/test/Makefile.in19
-rw-r--r--browser/devtools/fontinspector/test/browser_font.woffbin0 -> 4704 bytes
-rw-r--r--browser/devtools/fontinspector/test/browser_fontinspector.html20
-rw-r--r--browser/devtools/fontinspector/test/browser_fontinspector.js102
-rw-r--r--browser/devtools/fontinspector/test/moz.build6
-rw-r--r--browser/devtools/framework/Makefile.in16
-rw-r--r--browser/devtools/framework/connect/connect.css109
-rw-r--r--browser/devtools/framework/connect/connect.js175
-rw-r--r--browser/devtools/framework/connect/connect.xhtml50
-rw-r--r--browser/devtools/framework/gDevTools.jsm748
-rw-r--r--browser/devtools/framework/moz.build7
-rw-r--r--browser/devtools/framework/sidebar.js237
-rw-r--r--browser/devtools/framework/target.js601
-rw-r--r--browser/devtools/framework/test/Makefile.in33
-rw-r--r--browser/devtools/framework/test/browser_devtools_api.js122
-rw-r--r--browser/devtools/framework/test/browser_new_activation_workflow.js71
-rw-r--r--browser/devtools/framework/test/browser_target_events.js56
-rw-r--r--browser/devtools/framework/test/browser_toolbox_dynamic_registration.js107
-rw-r--r--browser/devtools/framework/test/browser_toolbox_highlight.js84
-rw-r--r--browser/devtools/framework/test/browser_toolbox_hosts.js132
-rw-r--r--browser/devtools/framework/test/browser_toolbox_options.js163
-rw-r--r--browser/devtools/framework/test/browser_toolbox_options_disablejs.html45
-rw-r--r--browser/devtools/framework/test/browser_toolbox_options_disablejs.js128
-rw-r--r--browser/devtools/framework/test/browser_toolbox_options_disablejs_iframe.html33
-rw-r--r--browser/devtools/framework/test/browser_toolbox_ready.js43
-rw-r--r--browser/devtools/framework/test/browser_toolbox_select_event.js98
-rw-r--r--browser/devtools/framework/test/browser_toolbox_sidebar.js150
-rw-r--r--browser/devtools/framework/test/browser_toolbox_tool_ready.js32
-rw-r--r--browser/devtools/framework/test/browser_toolbox_window_shortcuts.js78
-rw-r--r--browser/devtools/framework/test/browser_toolbox_window_title_changes.js84
-rw-r--r--browser/devtools/framework/test/head.js78
-rw-r--r--browser/devtools/framework/test/moz.build6
-rw-r--r--browser/devtools/framework/toolbox-hosts.js282
-rw-r--r--browser/devtools/framework/toolbox-options.js245
-rw-r--r--browser/devtools/framework/toolbox-options.xul73
-rw-r--r--browser/devtools/framework/toolbox-window.xul32
-rw-r--r--browser/devtools/framework/toolbox.css33
-rw-r--r--browser/devtools/framework/toolbox.js785
-rw-r--r--browser/devtools/framework/toolbox.xul54
-rw-r--r--browser/devtools/inspector/CmdInspect.jsm39
-rw-r--r--browser/devtools/inspector/Makefile.in16
-rw-r--r--browser/devtools/inspector/breadcrumbs.js597
-rw-r--r--browser/devtools/inspector/highlighter.js798
-rw-r--r--browser/devtools/inspector/inspector-panel.js657
-rw-r--r--browser/devtools/inspector/inspector.css28
-rw-r--r--browser/devtools/inspector/inspector.xul95
-rw-r--r--browser/devtools/inspector/moz.build7
-rw-r--r--browser/devtools/inspector/selection.js235
-rw-r--r--browser/devtools/inspector/selector-search.js549
-rw-r--r--browser/devtools/inspector/test/Makefile.in49
-rw-r--r--browser/devtools/inspector/test/browser_inspector_breadcrumbs.html40
-rw-r--r--browser/devtools/inspector/test/browser_inspector_breadcrumbs.js96
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_650804_search.html26
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_650804_search.js115
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_665880.js51
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js95
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_674871.js100
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js65
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js50
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js113
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js115
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html27
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js156
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_835722_infobar_reappears.js103
-rw-r--r--browser/devtools/inspector/test/browser_inspector_bug_840156_destroy_after_navigation.js54
-rw-r--r--browser/devtools/inspector/test/browser_inspector_changes.js112
-rw-r--r--browser/devtools/inspector/test/browser_inspector_cmd_inspect.html25
-rw-r--r--browser/devtools/inspector/test/browser_inspector_cmd_inspect.js131
-rw-r--r--browser/devtools/inspector/test/browser_inspector_destroyselection.html4
-rw-r--r--browser/devtools/inspector/test/browser_inspector_destroyselection.js48
-rw-r--r--browser/devtools/inspector/test/browser_inspector_highlighter.js156
-rw-r--r--browser/devtools/inspector/test/browser_inspector_highlighter_autohide.js46
-rw-r--r--browser/devtools/inspector/test/browser_inspector_iframeTest.js99
-rw-r--r--browser/devtools/inspector/test/browser_inspector_infobar.js92
-rw-r--r--browser/devtools/inspector/test/browser_inspector_initialization.js140
-rw-r--r--browser/devtools/inspector/test/browser_inspector_invalidate.js50
-rw-r--r--browser/devtools/inspector/test/browser_inspector_menu.html10
-rw-r--r--browser/devtools/inspector/test/browser_inspector_menu.js164
-rw-r--r--browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js71
-rw-r--r--browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js160
-rw-r--r--browser/devtools/inspector/test/browser_inspector_scrolling.js75
-rw-r--r--browser/devtools/inspector/test/browser_inspector_sidebarstate.js73
-rw-r--r--browser/devtools/inspector/test/browser_inspector_tree_height.js111
-rw-r--r--browser/devtools/inspector/test/head.js151
-rw-r--r--browser/devtools/inspector/test/moz.build6
-rw-r--r--browser/devtools/jar.mn68
-rw-r--r--browser/devtools/layoutview/moz.build8
-rw-r--r--browser/devtools/layoutview/test/Makefile.in17
-rw-r--r--browser/devtools/layoutview/test/browser_layoutview.js130
-rw-r--r--browser/devtools/layoutview/test/moz.build6
-rw-r--r--browser/devtools/layoutview/view.css187
-rw-r--r--browser/devtools/layoutview/view.js247
-rw-r--r--browser/devtools/layoutview/view.xhtml111
-rw-r--r--browser/devtools/main.js241
-rw-r--r--browser/devtools/markupview/Makefile.in16
-rw-r--r--browser/devtools/markupview/markup-view.css71
-rw-r--r--browser/devtools/markupview/markup-view.js1529
-rw-r--r--browser/devtools/markupview/markup-view.xhtml47
-rw-r--r--browser/devtools/markupview/moz.build7
-rw-r--r--browser/devtools/markupview/test/Makefile.in25
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_edit.html44
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_edit.js443
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_mutation.html37
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_mutation.js181
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_navigation.html28
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_navigation.js151
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_subset.html32
-rw-r--r--browser/devtools/markupview/test/browser_inspector_markup_subset.js146
-rw-r--r--browser/devtools/markupview/test/head.js18
-rw-r--r--browser/devtools/markupview/test/moz.build6
-rw-r--r--browser/devtools/moz.build26
-rw-r--r--browser/devtools/netmonitor/Makefile.in15
-rw-r--r--browser/devtools/netmonitor/NetMonitorPanel.jsm66
-rw-r--r--browser/devtools/netmonitor/moz.build6
-rw-r--r--browser/devtools/netmonitor/netmonitor-controller.js571
-rw-r--r--browser/devtools/netmonitor/netmonitor-view.js1825
-rw-r--r--browser/devtools/netmonitor/netmonitor.css68
-rw-r--r--browser/devtools/netmonitor/netmonitor.xul377
-rw-r--r--browser/devtools/netmonitor/test/Makefile.in73
-rw-r--r--browser/devtools/netmonitor/test/browser_net_aaa_leaktest.js28
-rw-r--r--browser/devtools/netmonitor/test/browser_net_accessibility-01.js80
-rw-r--r--browser/devtools/netmonitor/test/browser_net_accessibility-02.js123
-rw-r--r--browser/devtools/netmonitor/test/browser_net_autoscroll.js89
-rw-r--r--browser/devtools/netmonitor/test/browser_net_content-type.js228
-rw-r--r--browser/devtools/netmonitor/test/browser_net_cyrillic-01.js41
-rw-r--r--browser/devtools/netmonitor/test/browser_net_cyrillic-02.js42
-rw-r--r--browser/devtools/netmonitor/test/browser_net_filter-01.js184
-rw-r--r--browser/devtools/netmonitor/test/browser_net_filter-02.js181
-rw-r--r--browser/devtools/netmonitor/test/browser_net_filter-03.js186
-rw-r--r--browser/devtools/netmonitor/test/browser_net_footer-summary.js121
-rw-r--r--browser/devtools/netmonitor/test/browser_net_json-long.js96
-rw-r--r--browser/devtools/netmonitor/test/browser_net_json-malformed.js71
-rw-r--r--browser/devtools/netmonitor/test/browser_net_jsonp.js83
-rw-r--r--browser/devtools/netmonitor/test/browser_net_large-response.js47
-rw-r--r--browser/devtools/netmonitor/test/browser_net_page-nav.js68
-rw-r--r--browser/devtools/netmonitor/test/browser_net_pane-collapse.js66
-rw-r--r--browser/devtools/netmonitor/test/browser_net_pane-toggle.js79
-rw-r--r--browser/devtools/netmonitor/test/browser_net_post-data-01.js153
-rw-r--r--browser/devtools/netmonitor/test/browser_net_post-data-02.js62
-rw-r--r--browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js67
-rw-r--r--browser/devtools/netmonitor/test/browser_net_prefs-reload.js217
-rw-r--r--browser/devtools/netmonitor/test/browser_net_req-resp-bodies.js60
-rw-r--r--browser/devtools/netmonitor/test/browser_net_simple-init.js84
-rw-r--r--browser/devtools/netmonitor/test/browser_net_simple-request-data.js237
-rw-r--r--browser/devtools/netmonitor/test/browser_net_simple-request-details.js239
-rw-r--r--browser/devtools/netmonitor/test/browser_net_simple-request.js60
-rw-r--r--browser/devtools/netmonitor/test/browser_net_sort-01.js248
-rw-r--r--browser/devtools/netmonitor/test/browser_net_sort-02.js249
-rw-r--r--browser/devtools/netmonitor/test/browser_net_sort-03.js177
-rw-r--r--browser/devtools/netmonitor/test/browser_net_status-codes.js155
-rw-r--r--browser/devtools/netmonitor/test/browser_net_timeline_ticks.js135
-rw-r--r--browser/devtools/netmonitor/test/head.js283
-rw-r--r--browser/devtools/netmonitor/test/html_content-type-test-page.html43
-rw-r--r--browser/devtools/netmonitor/test/html_custom-get-page.html39
-rw-r--r--browser/devtools/netmonitor/test/html_cyrillic-test-page.html34
-rw-r--r--browser/devtools/netmonitor/test/html_filter-test-page.html55
-rw-r--r--browser/devtools/netmonitor/test/html_infinite-get-page.html36
-rw-r--r--browser/devtools/netmonitor/test/html_json-long-test-page.html33
-rw-r--r--browser/devtools/netmonitor/test/html_json-malformed-test-page.html33
-rw-r--r--browser/devtools/netmonitor/test/html_jsonp-test-page.html33
-rw-r--r--browser/devtools/netmonitor/test/html_navigate-test-page.html13
-rw-r--r--browser/devtools/netmonitor/test/html_post-data-test-page.html72
-rw-r--r--browser/devtools/netmonitor/test/html_post-raw-test-page.html34
-rw-r--r--browser/devtools/netmonitor/test/html_simple-test-page.html13
-rw-r--r--browser/devtools/netmonitor/test/html_sorting-test-page.html42
-rw-r--r--browser/devtools/netmonitor/test/html_status-codes-test-page.html41
-rw-r--r--browser/devtools/netmonitor/test/moz.build5
-rw-r--r--browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs127
-rw-r--r--browser/devtools/netmonitor/test/sjs_simple-test-server.sjs9
-rw-r--r--browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs18
-rw-r--r--browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs34
-rw-r--r--browser/devtools/netmonitor/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--browser/devtools/profiler/Makefile.in15
-rw-r--r--browser/devtools/profiler/ProfilerController.jsm394
-rw-r--r--browser/devtools/profiler/ProfilerHelpers.jsm43
-rw-r--r--browser/devtools/profiler/ProfilerPanel.jsm716
-rw-r--r--browser/devtools/profiler/cleopatra/cleopatra.html28
-rw-r--r--browser/devtools/profiler/cleopatra/css/devtools.css23
-rw-r--r--browser/devtools/profiler/cleopatra/css/tree.css236
-rw-r--r--browser/devtools/profiler/cleopatra/css/ui.css341
-rw-r--r--browser/devtools/profiler/cleopatra/images/circlearrow.svg27
-rw-r--r--browser/devtools/profiler/cleopatra/images/noise.png0
-rw-r--r--browser/devtools/profiler/cleopatra/images/throbber.svg23
-rw-r--r--browser/devtools/profiler/cleopatra/images/treetwisty.svg32
-rw-r--r--browser/devtools/profiler/cleopatra/js/ProgressReporter.js185
-rw-r--r--browser/devtools/profiler/cleopatra/js/devtools.js259
-rw-r--r--browser/devtools/profiler/cleopatra/js/parser.js275
-rw-r--r--browser/devtools/profiler/cleopatra/js/parserWorker.js1655
-rw-r--r--browser/devtools/profiler/cleopatra/js/strings.js23
-rw-r--r--browser/devtools/profiler/cleopatra/js/tree.js705
-rw-r--r--browser/devtools/profiler/cleopatra/js/ui.js2013
-rw-r--r--browser/devtools/profiler/cmd-profiler.jsm233
-rw-r--r--browser/devtools/profiler/moz.build7
-rw-r--r--browser/devtools/profiler/profiler.xul46
-rw-r--r--browser/devtools/profiler/test/Makefile.in39
-rw-r--r--browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js63
-rw-r--r--browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js38
-rw-r--r--browser/devtools/profiler/test/browser_profiler_bug_855244_multiple_tabs.js103
-rw-r--r--browser/devtools/profiler/test/browser_profiler_cmd.js154
-rw-r--r--browser/devtools/profiler/test/browser_profiler_console_api.js64
-rw-r--r--browser/devtools/profiler/test/browser_profiler_console_api_content.js56
-rw-r--r--browser/devtools/profiler/test/browser_profiler_console_api_mixed.js36
-rw-r--r--browser/devtools/profiler/test/browser_profiler_console_api_named.js66
-rw-r--r--browser/devtools/profiler/test/browser_profiler_controller.js64
-rw-r--r--browser/devtools/profiler/test/browser_profiler_profiles.js69
-rw-r--r--browser/devtools/profiler/test/browser_profiler_remote.js55
-rw-r--r--browser/devtools/profiler/test/browser_profiler_run.js66
-rw-r--r--browser/devtools/profiler/test/head.js119
-rw-r--r--browser/devtools/profiler/test/mock_console_api.html21
-rw-r--r--browser/devtools/profiler/test/mock_profiler_bug_834878_page.html14
-rw-r--r--browser/devtools/profiler/test/mock_profiler_bug_834878_script.js7
-rw-r--r--browser/devtools/profiler/test/moz.build6
-rw-r--r--browser/devtools/responsivedesign/CmdResize.jsm93
-rw-r--r--browser/devtools/responsivedesign/Makefile.in15
-rw-r--r--browser/devtools/responsivedesign/moz.build8
-rw-r--r--browser/devtools/responsivedesign/responsivedesign.jsm710
-rw-r--r--browser/devtools/responsivedesign/test/Makefile.in22
-rw-r--r--browser/devtools/responsivedesign/test/browser_responsive_cmd.js107
-rw-r--r--browser/devtools/responsivedesign/test/browser_responsivecomputedview.js108
-rw-r--r--browser/devtools/responsivedesign/test/browser_responsiveruleview.js102
-rw-r--r--browser/devtools/responsivedesign/test/browser_responsiveui.js197
-rw-r--r--browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js202
-rw-r--r--browser/devtools/responsivedesign/test/head.js20
-rw-r--r--browser/devtools/responsivedesign/test/moz.build6
-rw-r--r--browser/devtools/scratchpad/CmdScratchpad.jsm22
-rw-r--r--browser/devtools/scratchpad/Makefile.in16
-rw-r--r--browser/devtools/scratchpad/moz.build7
-rw-r--r--browser/devtools/scratchpad/scratchpad-manager.jsm166
-rw-r--r--browser/devtools/scratchpad/scratchpad.js1650
-rw-r--r--browser/devtools/scratchpad/scratchpad.xul301
-rw-r--r--browser/devtools/scratchpad/test/Makefile.in44
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js97
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js158
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js56
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js45
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js73
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js107
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js92
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js51
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js60
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js355
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js227
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js81
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js94
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js120
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js68
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js187
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js137
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_contexts.js174
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js138
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_files.js118
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_initialization.js48
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_inspect.js55
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_open.js76
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_restore.js98
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js103
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_ui.js70
-rw-r--r--browser/devtools/scratchpad/test/head.js197
-rw-r--r--browser/devtools/scratchpad/test/moz.build6
-rw-r--r--browser/devtools/shared/AppCacheUtils.jsm630
-rw-r--r--browser/devtools/shared/AutocompletePopup.jsm500
-rw-r--r--browser/devtools/shared/DOMHelpers.jsm124
-rw-r--r--browser/devtools/shared/DeveloperToolbar.jsm1248
-rw-r--r--browser/devtools/shared/FloatingScrollbars.jsm126
-rw-r--r--browser/devtools/shared/Jsbeautify.jsm1303
-rw-r--r--browser/devtools/shared/LayoutHelpers.jsm384
-rw-r--r--browser/devtools/shared/Makefile.in18
-rw-r--r--browser/devtools/shared/Parser.jsm2293
-rw-r--r--browser/devtools/shared/SplitView.jsm302
-rw-r--r--browser/devtools/shared/event-emitter.js118
-rw-r--r--browser/devtools/shared/inplace-editor.js851
-rw-r--r--browser/devtools/shared/moz.build7
-rw-r--r--browser/devtools/shared/splitview.css98
-rw-r--r--browser/devtools/shared/telemetry.js259
-rw-r--r--browser/devtools/shared/test/Makefile.in42
-rw-r--r--browser/devtools/shared/test/browser_eventemitter_basic.js80
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers.html25
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers.js99
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers_iframe.html19
-rw-r--r--browser/devtools/shared/test/browser_require_basic.js140
-rw-r--r--browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js179
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js110
-rw-r--r--browser/devtools/shared/test/browser_templater_basic.html13
-rw-r--r--browser/devtools/shared/test/browser_templater_basic.js288
-rw-r--r--browser/devtools/shared/test/browser_toolbar_basic.html35
-rw-r--r--browser/devtools/shared/test/browser_toolbar_basic.js74
-rw-r--r--browser/devtools/shared/test/browser_toolbar_tooltip.js54
-rw-r--r--browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html32
-rw-r--r--browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js245
-rw-r--r--browser/devtools/shared/test/head.js121
-rw-r--r--browser/devtools/shared/test/leakhunt.js170
-rw-r--r--browser/devtools/shared/test/moz.build7
-rw-r--r--browser/devtools/shared/test/unit/test_undoStack.js98
-rw-r--r--browser/devtools/shared/test/unit/xpcshell.ini6
-rw-r--r--browser/devtools/shared/theme-switching.js74
-rw-r--r--browser/devtools/shared/undo.js206
-rw-r--r--browser/devtools/shared/widgets/BreadcrumbsWidget.jsm227
-rw-r--r--browser/devtools/shared/widgets/SideMenuWidget.jsm621
-rw-r--r--browser/devtools/shared/widgets/VariablesView.jsm3166
-rw-r--r--browser/devtools/shared/widgets/VariablesView.xul16
-rw-r--r--browser/devtools/shared/widgets/VariablesViewController.jsm350
-rw-r--r--browser/devtools/shared/widgets/ViewHelpers.jsm1606
-rw-r--r--browser/devtools/shared/widgets/widgets.css59
-rw-r--r--browser/devtools/sourceeditor/Makefile.in19
-rw-r--r--browser/devtools/sourceeditor/moz.build7
-rw-r--r--browser/devtools/sourceeditor/orion/LICENSE29
-rw-r--r--browser/devtools/sourceeditor/orion/Makefile.dryice.js56
-rw-r--r--browser/devtools/sourceeditor/orion/README43
-rw-r--r--browser/devtools/sourceeditor/orion/UPGRADE20
-rw-r--r--browser/devtools/sourceeditor/orion/orion.css277
-rw-r--r--browser/devtools/sourceeditor/orion/orion.js12303
-rw-r--r--browser/devtools/sourceeditor/source-editor-orion.jsm2129
-rw-r--r--browser/devtools/sourceeditor/source-editor-overlay.xul204
-rw-r--r--browser/devtools/sourceeditor/source-editor-ui.jsm332
-rw-r--r--browser/devtools/sourceeditor/source-editor.jsm455
-rw-r--r--browser/devtools/sourceeditor/test/Makefile.in37
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug650345_find.js149
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js72
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js119
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug687160_line_api.js90
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug687568_pagescroll.js89
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug687573_vscroll.js133
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug687580_drag_and_drop.js162
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug695035_middle_click_paste.js100
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug700893_dirty_state.js94
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug703692_focus_blur.js71
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug707987_debugger_breakpoints.js169
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug712982_line_ruler_click.js74
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug725388_mouse_events.js107
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug725392_mouse_coords_char_offset.js160
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug725430_comment_uncomment.js151
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug725618_moveLines_shortcut.js117
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug729480_line_vertical_align.js99
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug729960_block_bracket_jump.js164
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug731721_debugger_stepping.js59
-rw-r--r--browser/devtools/sourceeditor/test/browser_bug744021_next_prev_bracket_jump.js104
-rw-r--r--browser/devtools/sourceeditor/test/browser_sourceeditor_initialization.js499
-rw-r--r--browser/devtools/sourceeditor/test/head.js182
-rw-r--r--browser/devtools/sourceeditor/test/moz.build6
-rw-r--r--browser/devtools/styleeditor/CmdEdit.jsm50
-rw-r--r--browser/devtools/styleeditor/Makefile.in15
-rw-r--r--browser/devtools/styleeditor/StyleEditorDebuggee.jsm342
-rw-r--r--browser/devtools/styleeditor/StyleEditorPanel.jsm130
-rw-r--r--browser/devtools/styleeditor/StyleEditorUI.jsm460
-rw-r--r--browser/devtools/styleeditor/StyleEditorUtil.jsm221
-rw-r--r--browser/devtools/styleeditor/StyleSheetEditor.jsm548
-rw-r--r--browser/devtools/styleeditor/moz.build7
-rw-r--r--browser/devtools/styleeditor/styleeditor.css89
-rw-r--r--browser/devtools/styleeditor/styleeditor.xul104
-rw-r--r--browser/devtools/styleeditor/test/Makefile.in55
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_bug_740541_iframes.js90
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js68
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js46
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.html50
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js207
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_enabled.js75
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_filesave.js90
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_import.js76
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_import_rule.js43
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_init.js87
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_loading.js39
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_new.js143
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js41
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_pretty.js52
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js62
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_reload.js99
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js78
-rw-r--r--browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js61
-rw-r--r--browser/devtools/styleeditor/test/four.html25
-rw-r--r--browser/devtools/styleeditor/test/head.js108
-rw-r--r--browser/devtools/styleeditor/test/import.css10
-rw-r--r--browser/devtools/styleeditor/test/import.html11
-rw-r--r--browser/devtools/styleeditor/test/import2.css10
-rw-r--r--browser/devtools/styleeditor/test/longload.html28
-rw-r--r--browser/devtools/styleeditor/test/media-small.css5
-rw-r--r--browser/devtools/styleeditor/test/media.html11
-rw-r--r--browser/devtools/styleeditor/test/minified.html17
-rw-r--r--browser/devtools/styleeditor/test/moz.build6
-rw-r--r--browser/devtools/styleeditor/test/nostyle.html5
-rw-r--r--browser/devtools/styleeditor/test/resources_inpage.jsi12
-rw-r--r--browser/devtools/styleeditor/test/resources_inpage1.css11
-rw-r--r--browser/devtools/styleeditor/test/resources_inpage2.css11
-rw-r--r--browser/devtools/styleeditor/test/simple.css9
-rw-r--r--browser/devtools/styleeditor/test/simple.css.gzbin0 -> 166 bytes
-rw-r--r--browser/devtools/styleeditor/test/simple.css.gz^headers^4
-rw-r--r--browser/devtools/styleeditor/test/simple.gz.html23
-rw-r--r--browser/devtools/styleeditor/test/simple.html23
-rw-r--r--browser/devtools/styleeditor/test/test_private.css3
-rw-r--r--browser/devtools/styleeditor/test/test_private.html7
-rw-r--r--browser/devtools/styleinspector/Makefile.in16
-rw-r--r--browser/devtools/styleinspector/computed-view.js953
-rw-r--r--browser/devtools/styleinspector/computedview.xhtml114
-rw-r--r--browser/devtools/styleinspector/css-logic.js1757
-rw-r--r--browser/devtools/styleinspector/cssruleview.xhtml38
-rw-r--r--browser/devtools/styleinspector/moz.build7
-rw-r--r--browser/devtools/styleinspector/rule-view.js1875
-rw-r--r--browser/devtools/styleinspector/ruleview.css38
-rw-r--r--browser/devtools/styleinspector/style-inspector.js238
-rw-r--r--browser/devtools/styleinspector/test/Makefile.in60
-rw-r--r--browser/devtools/styleinspector/test/browser_bug589375_keybindings.js141
-rw-r--r--browser/devtools/styleinspector/test/browser_bug683672.html28
-rw-r--r--browser/devtools/styleinspector/test/browser_bug683672.js78
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.html33
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.js102
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.xul9
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported.css5
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported2.css3
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_linked.css3
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_script.css5
-rw-r--r--browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_xul.css3
-rw-r--r--browser/devtools/styleinspector/test/browser_bug722196_identify_media_queries.html24
-rw-r--r--browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js68
-rw-r--r--browser/devtools/styleinspector/test/browser_bug722196_rule_view_media_queries.js52
-rw-r--r--browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js198
-rw-r--r--browser/devtools/styleinspector/test/browser_bug_592743_specificity.js99
-rw-r--r--browser/devtools/styleinspector/test/browser_bug_692400_element_style.js82
-rw-r--r--browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js175
-rw-r--r--browser/devtools/styleinspector/test/browser_computedview_copy.js148
-rw-r--r--browser/devtools/styleinspector/test/browser_csslogic_inherited.js44
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js175
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_copy.js145
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_editor.js119
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js158
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_focus.js71
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_inherit.js99
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_manipulation.js68
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_override.js159
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_ui.js218
-rw-r--r--browser/devtools/styleinspector/test/browser_ruleview_update.js153
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector.js89
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js116
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js116
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html21
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js97
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css9
-rw-r--r--browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js118
-rw-r--r--browser/devtools/styleinspector/test/head.js126
-rw-r--r--browser/devtools/styleinspector/test/moz.build6
-rw-r--r--browser/devtools/styleinspector/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--browser/devtools/tilt/CmdTilt.jsm216
-rw-r--r--browser/devtools/tilt/Makefile.in16
-rw-r--r--browser/devtools/tilt/TiltWorkerCrafter.js280
-rw-r--r--browser/devtools/tilt/TiltWorkerPicker.js186
-rw-r--r--browser/devtools/tilt/moz.build8
-rw-r--r--browser/devtools/tilt/test/Makefile.in63
-rw-r--r--browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js14
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js100
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js173
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications.js132
-rw-r--r--browser/devtools/tilt/test/browser_tilt_03_tab_switch.js106
-rw-r--r--browser/devtools/tilt/test/browser_tilt_04_initialization.js57
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction-url.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js131
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball-reset.js129
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball.js496
-rw-r--r--browser/devtools/tilt/test/browser_tilt_controller.js134
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl01.js155
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl02.js44
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl03.js67
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl04.js124
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl05.js40
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl06.js57
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl07.js58
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl08.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math01.js59
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math02.js104
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math03.js33
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math04.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math05.js101
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math06.js42
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math07.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking.js54
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_delete.js76
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js75
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight01.js75
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight02.js70
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight03.js70
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_inspector.js61
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_miv.js76
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils01.js69
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils02.js21
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils03.js18
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils04.js54
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils05.js100
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils06.js44
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils07.js158
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils08.js85
-rw-r--r--browser/devtools/tilt/test/browser_tilt_visualizer.js126
-rw-r--r--browser/devtools/tilt/test/browser_tilt_zoom.js91
-rw-r--r--browser/devtools/tilt/test/head.js206
-rw-r--r--browser/devtools/tilt/test/moz.build6
-rw-r--r--browser/devtools/tilt/tilt-gl.js1595
-rw-r--r--browser/devtools/tilt/tilt-math.js2322
-rw-r--r--browser/devtools/tilt/tilt-utils.js612
-rw-r--r--browser/devtools/tilt/tilt-visualizer-style.js46
-rw-r--r--browser/devtools/tilt/tilt-visualizer.js2260
-rw-r--r--browser/devtools/tilt/tilt.js263
-rw-r--r--browser/devtools/webconsole/HUDService.jsm744
-rw-r--r--browser/devtools/webconsole/Makefile.in21
-rw-r--r--browser/devtools/webconsole/NetworkPanel.jsm847
-rw-r--r--browser/devtools/webconsole/NetworkPanel.xhtml124
-rw-r--r--browser/devtools/webconsole/WebConsolePanel.jsm112
-rw-r--r--browser/devtools/webconsole/moz.build8
-rw-r--r--browser/devtools/webconsole/test/Makefile.in245
-rw-r--r--browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js113
-rw-r--r--browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js107
-rw-r--r--browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js34
-rw-r--r--browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js82
-rw-r--r--browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js98
-rw-r--r--browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js94
-rw-r--r--browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js66
-rw-r--r--browser/devtools/webconsole/test/browser_cached_messages.js76
-rw-r--r--browser/devtools/webconsole/test/browser_console.js102
-rw-r--r--browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js93
-rw-r--r--browser/devtools/webconsole/test/browser_console_clear_on_reload.js73
-rw-r--r--browser/devtools/webconsole/test/browser_console_consolejsm_output.js135
-rw-r--r--browser/devtools/webconsole/test/browser_console_dead_objects.js79
-rw-r--r--browser/devtools/webconsole/test/browser_console_error_source_click.js75
-rw-r--r--browser/devtools/webconsole/test/browser_console_filters.js71
-rw-r--r--browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js69
-rw-r--r--browser/devtools/webconsole/test/browser_console_log_inspectable_object.js58
-rw-r--r--browser/devtools/webconsole/test/browser_console_native_getters.js121
-rw-r--r--browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js95
-rw-r--r--browser/devtools/webconsole/test/browser_console_private_browsing.js199
-rw-r--r--browser/devtools/webconsole/test/browser_console_variables_view.js177
-rw-r--r--browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js132
-rw-r--r--browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js127
-rw-r--r--browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js150
-rw-r--r--browser/devtools/webconsole/test/browser_jsterm_inspect.js35
-rw-r--r--browser/devtools/webconsole/test/browser_longstring_hang.js73
-rw-r--r--browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js309
-rw-r--r--browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js56
-rw-r--r--browser/devtools/webconsole/test/browser_output_longstring_expand.js153
-rw-r--r--browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js120
-rw-r--r--browser/devtools/webconsole/test/browser_result_format_as_string.js49
-rw-r--r--browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js79
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js21
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js45
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js38
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js25
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js44
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js67
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js78
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js36
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js68
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js35
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js109
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js50
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js283
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js95
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js81
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js91
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js39
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js49
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js44
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js62
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js41
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js75
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js132
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js157
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js54
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js101
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js209
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js106
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js43
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js47
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js81
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js63
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js275
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js76
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js53
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js82
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js75
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js146
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js182
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js39
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_611795.js93
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js52
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js85
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js104
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js101
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js68
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js70
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js89
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js68
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js123
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js32
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js70
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js129
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js44
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js88
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_632817.js196
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js89
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js102
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js217
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js56
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js106
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js144
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js103
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js29
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js48
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js133
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_704295.js42
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js39
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js96
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js119
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js113
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_770099_bad_policyuri.js55
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js46
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js150
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js214
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js61
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_821877_csp_errors.js28
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js34
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_change_font_size.js44
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_chrome.js35
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_completion.js120
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_console_extras.js39
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js146
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js67
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_execution_scope.js45
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_for_of.js35
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_history.js66
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js48
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js60
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_jsterm.js194
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js66
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js106
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js71
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_message_node_id.js29
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_netlogging.js211
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_network_panel.js543
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_notifications.js70
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js62
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_output_order.js44
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_property_provider.js42
-rw-r--r--browser/devtools/webconsole/test/browser_webconsole_view_source.js66
-rw-r--r--browser/devtools/webconsole/test/head.js1247
-rw-r--r--browser/devtools/webconsole/test/moz.build6
-rw-r--r--browser/devtools/webconsole/test/test-bug-585956-console-trace.html27
-rw-r--r--browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html13
-rw-r--r--browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html14
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-canvas-css.html17
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-canvas-css.js10
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-canvas.html15
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-canvas.js11
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-css-loader.css10
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^1
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-css-loader.html13
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-css-parser.css10
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-css-parser.html14
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html16
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js8
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-html.html16
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-image.html15
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-image.jpgbin0 -> 2532 bytes
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-imagemap.html17
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html19
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml8
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml10
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-svg.xhtml17
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-workers.html18
-rw-r--r--browser/devtools/webconsole/test/test-bug-595934-workers.js9
-rw-r--r--browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html25
-rw-r--r--browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js14
-rw-r--r--browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html18
-rw-r--r--browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs25
-rw-r--r--browser/devtools/webconsole/test/test-bug-600183-charset.html9
-rw-r--r--browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^1
-rw-r--r--browser/devtools/webconsole/test/test-bug-601177-log-levels.html20
-rw-r--r--browser/devtools/webconsole/test/test-bug-601177-log-levels.js8
-rw-r--r--browser/devtools/webconsole/test/test-bug-603750-websocket.html14
-rw-r--r--browser/devtools/webconsole/test/test-bug-603750-websocket.js18
-rw-r--r--browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html21
-rw-r--r--browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html24
-rw-r--r--browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html23
-rw-r--r--browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs16
-rw-r--r--browser/devtools/webconsole/test/test-bug-632275-getters.html20
-rw-r--r--browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html54
-rw-r--r--browser/devtools/webconsole/test/test-bug-644419-log-limits.html21
-rw-r--r--browser/devtools/webconsole/test/test-bug-646025-console-file-location.html12
-rw-r--r--browser/devtools/webconsole/test/test-bug-658368-time-methods.html24
-rw-r--r--browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html15
-rw-r--r--browser/devtools/webconsole/test/test-bug-766001-console-log.js8
-rw-r--r--browser/devtools/webconsole/test/test-bug-766001-js-console-links.html14
-rw-r--r--browser/devtools/webconsole/test/test-bug-766001-js-errors.js7
-rw-r--r--browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css10
-rw-r--r--browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css10
-rw-r--r--browser/devtools/webconsole/test/test-bug-782653-css-errors.html14
-rw-r--r--browser/devtools/webconsole/test/test-bug-821877-csperrors.html12
-rw-r--r--browser/devtools/webconsole/test/test-bug-821877-csperrors.html^headers^1
-rw-r--r--browser/devtools/webconsole/test/test-bug-837351-security-errors.html15
-rw-r--r--browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html23
-rw-r--r--browser/devtools/webconsole/test/test-bug-869003-iframe.html20
-rw-r--r--browser/devtools/webconsole/test/test-bug-869003-top-window.html14
-rw-r--r--browser/devtools/webconsole/test/test-console-extras.html25
-rw-r--r--browser/devtools/webconsole/test/test-console-replaced-api.html12
-rw-r--r--browser/devtools/webconsole/test/test-console.html23
-rw-r--r--browser/devtools/webconsole/test/test-data.json1
-rw-r--r--browser/devtools/webconsole/test/test-data.json^headers^1
-rw-r--r--browser/devtools/webconsole/test/test-duplicate-error.html21
-rw-r--r--browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html7
-rw-r--r--browser/devtools/webconsole/test/test-error.html21
-rw-r--r--browser/devtools/webconsole/test/test-eval-in-stackframe.html39
-rw-r--r--browser/devtools/webconsole/test/test-file-location.js9
-rw-r--r--browser/devtools/webconsole/test/test-filter.html11
-rw-r--r--browser/devtools/webconsole/test/test-for-of.html8
-rw-r--r--browser/devtools/webconsole/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--browser/devtools/webconsole/test/test-mutation.html16
-rw-r--r--browser/devtools/webconsole/test/test-network-request.html36
-rw-r--r--browser/devtools/webconsole/test/test-network.html11
-rw-r--r--browser/devtools/webconsole/test/test-observe-http-ajax.html17
-rw-r--r--browser/devtools/webconsole/test/test-own-console.html24
-rw-r--r--browser/devtools/webconsole/test/test-property-provider.html14
-rw-r--r--browser/devtools/webconsole/test/test-repeated-messages.html35
-rw-r--r--browser/devtools/webconsole/test/test-result-format-as-string.html24
-rw-r--r--browser/devtools/webconsole/test/test-webconsole-error-observer.html25
-rw-r--r--browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html12
-rw-r--r--browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html^headers^2
-rw-r--r--browser/devtools/webconsole/test/test_bug_770099_violation.html13
-rw-r--r--browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^1
-rw-r--r--browser/devtools/webconsole/test/testscript.js1
-rw-r--r--browser/devtools/webconsole/webconsole.js5114
-rw-r--r--browser/devtools/webconsole/webconsole.xul183
953 files changed, 161818 insertions, 0 deletions
diff --git a/browser/devtools/Makefile.in b/browser/devtools/Makefile.in
new file mode 100644
index 000000000..5f0adb705
--- /dev/null
+++ b/browser/devtools/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(topsrcdir)/config/config.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/main.js $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/commandline/BuiltinCommands.jsm b/browser/devtools/commandline/BuiltinCommands.jsm
new file mode 100644
index 000000000..fa1e3cc7d
--- /dev/null
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -0,0 +1,2169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands", "DEFAULT_DEBUG_PORT", "connect" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+let telemetry = new Telemetry();
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppCacheUtils",
+ "resource:///modules/devtools/AppCacheUtils.jsm");
+
+/* CmdAddon ---------------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+ // We need to use an object in which to store any flags because a primitive
+ // would remain undefined.
+ module.CmdAddonFlags = {
+ addonsLoaded: false
+ };
+
+ /**
+ * 'addon' command.
+ */
+ gcli.addCommand({
+ name: "addon",
+ description: gcli.lookup("addonDesc")
+ });
+
+ /**
+ * 'addon list' command.
+ */
+ gcli.addCommand({
+ name: "addon list",
+ description: gcli.lookup("addonListDesc"),
+ returnType: "addonsInfo",
+ params: [{
+ name: 'type',
+ type: {
+ name: 'selection',
+ data: ["dictionary", "extension", "locale", "plugin", "theme", "all"]
+ },
+ defaultValue: 'all',
+ description: gcli.lookup("addonListTypeDesc")
+ }],
+ exec: function(aArgs, context) {
+ let deferred = context.defer();
+ function pendingOperations(aAddon) {
+ let allOperations = ["PENDING_ENABLE",
+ "PENDING_DISABLE",
+ "PENDING_UNINSTALL",
+ "PENDING_INSTALL",
+ "PENDING_UPGRADE"];
+ return allOperations.reduce(function(operations, opName) {
+ return aAddon.pendingOperations & AddonManager[opName] ?
+ operations.concat(opName) :
+ operations;
+ }, []);
+ }
+ let types = aArgs.type === "all" ? null : [aArgs.type];
+ AddonManager.getAddonsByTypes(types, function(addons) {
+ deferred.resolve({
+ addons: addons.map(function(addon) {
+ return {
+ name: addon.name,
+ version: addon.version,
+ isActive: addon.isActive,
+ pendingOperations: pendingOperations(addon)
+ };
+ }),
+ type: aArgs.type
+ });
+ });
+ return deferred.promise;
+ }
+ });
+
+ gcli.addConverter({
+ from: "addonsInfo",
+ to: "view",
+ exec: function(addonsInfo, context) {
+ if (!addonsInfo.addons.length) {
+ return context.createView({
+ html: "<p>${message}</p>",
+ data: { message: gcli.lookup("addonNoneOfType") }
+ });
+ }
+
+ let headerLookups = {
+ "dictionary": "addonListDictionaryHeading",
+ "extension": "addonListExtensionHeading",
+ "locale": "addonListLocaleHeading",
+ "plugin": "addonListPluginHeading",
+ "theme": "addonListThemeHeading",
+ "all": "addonListAllHeading"
+ };
+ let header = gcli.lookup(headerLookups[addonsInfo.type] ||
+ "addonListUnknownHeading");
+
+ let operationLookups = {
+ "PENDING_ENABLE": "addonPendingEnable",
+ "PENDING_DISABLE": "addonPendingDisable",
+ "PENDING_UNINSTALL": "addonPendingUninstall",
+ "PENDING_INSTALL": "addonPendingInstall",
+ "PENDING_UPGRADE": "addonPendingUpgrade"
+ };
+ function lookupOperation(opName) {
+ let lookupName = operationLookups[opName];
+ return lookupName ? gcli.lookup(lookupName) : opName;
+ }
+
+ function arrangeAddons(addons) {
+ let enabledAddons = [];
+ let disabledAddons = [];
+ addons.forEach(function(aAddon) {
+ if (aAddon.isActive) {
+ enabledAddons.push(aAddon);
+ } else {
+ disabledAddons.push(aAddon);
+ }
+ });
+
+ function compareAddonNames(aNameA, aNameB) {
+ return String.localeCompare(aNameA.name, aNameB.name);
+ }
+ enabledAddons.sort(compareAddonNames);
+ disabledAddons.sort(compareAddonNames);
+
+ return enabledAddons.concat(disabledAddons);
+ }
+
+ function isActiveForToggle(addon) {
+ return (addon.isActive && ~~addon.pendingOperations.indexOf("PENDING_DISABLE"));
+ }
+
+ return context.createView({
+ html: addonsListHtml,
+ data: {
+ header: header,
+ addons: arrangeAddons(addonsInfo.addons).map(function(addon) {
+ return {
+ name: addon.name,
+ label: addon.name.replace(/\s/g, "_") +
+ (addon.version ? "_" + addon.version : ""),
+ status: addon.isActive ? "enabled" : "disabled",
+ version: addon.version,
+ pendingOperations: addon.pendingOperations.length ?
+ (" (" + gcli.lookup("addonPending") + ": "
+ + addon.pendingOperations.map(lookupOperation).join(", ")
+ + ")") :
+ "",
+ toggleActionName: isActiveForToggle(addon) ? "disable": "enable",
+ toggleActionMessage: isActiveForToggle(addon) ?
+ gcli.lookup("addonListOutDisable") :
+ gcli.lookup("addonListOutEnable")
+ };
+ }),
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ });
+
+ var addonsListHtml = "" +
+ "<table>" +
+ " <caption>${header}</caption>" +
+ " <tbody>" +
+ " <tr foreach='addon in ${addons}'" +
+ " class=\"gcli-addon-${addon.status}\">" +
+ " <td>${addon.name} ${addon.version}</td>" +
+ " <td>${addon.pendingOperations}</td>" +
+ " <td>" +
+ " <span class='gcli-out-shortcut'" +
+ " data-command='addon ${addon.toggleActionName} ${addon.label}'" +
+ " onclick='${onclick}'" +
+ " ondblclick='${ondblclick}'" +
+ " >${addon.toggleActionMessage}</span>" +
+ " </td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>" +
+ "";
+
+ // We need a list of addon names for the enable and disable commands. Because
+ // getting the name list is async we do not add the commands until we have the
+ // list.
+ AddonManager.getAllAddons(function addonAsync(aAddons) {
+ // We listen for installs to keep our addon list up to date. There is no need
+ // to listen for uninstalls because uninstalled addons are simply disabled
+ // until restart (to enable undo functionality).
+ AddonManager.addAddonListener({
+ onInstalled: function(aAddon) {
+ addonNameCache.push({
+ name: representAddon(aAddon).replace(/\s/g, "_"),
+ value: aAddon.name
+ });
+ },
+ onUninstalled: function(aAddon) {
+ let name = representAddon(aAddon).replace(/\s/g, "_");
+
+ for (let i = 0; i < addonNameCache.length; i++) {
+ if(addonNameCache[i].name == name) {
+ addonNameCache.splice(i, 1);
+ break;
+ }
+ }
+ },
+ });
+
+ /**
+ * Returns a string that represents the passed add-on.
+ */
+ function representAddon(aAddon) {
+ let name = aAddon.name + " " + aAddon.version;
+ return name.trim();
+ }
+
+ let addonNameCache = [];
+
+ // The name parameter, used in "addon enable" and "addon disable."
+ let nameParameter = {
+ name: "name",
+ type: {
+ name: "selection",
+ lookup: addonNameCache
+ },
+ description: gcli.lookup("addonNameDesc")
+ };
+
+ for (let addon of aAddons) {
+ addonNameCache.push({
+ name: representAddon(addon).replace(/\s/g, "_"),
+ value: addon.name
+ });
+ }
+
+ /**
+ * 'addon enable' command.
+ */
+ gcli.addCommand({
+ name: "addon enable",
+ description: gcli.lookup("addonEnableDesc"),
+ params: [nameParameter],
+ exec: function(aArgs, context) {
+ /**
+ * Enables the addon in the passed list which has a name that matches
+ * according to the passed name comparer, and resolves the promise which
+ * is the scope (this) of this function to display the result of this
+ * enable attempt.
+ */
+ function enable(aName, addons) {
+ // Find the add-on.
+ let addon = null;
+ addons.some(function(candidate) {
+ if (candidate.name == aName) {
+ addon = candidate;
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ let name = representAddon(addon);
+ let message = "";
+
+ if (!addon.userDisabled) {
+ message = gcli.lookupFormat("addonAlreadyEnabled", [name]);
+ } else {
+ addon.userDisabled = false;
+ message = gcli.lookupFormat("addonEnabled", [name]);
+ }
+ this.resolve(message);
+ }
+
+ let deferred = context.defer();
+ // List the installed add-ons, enable one when done listing.
+ AddonManager.getAllAddons(enable.bind(deferred, aArgs.name));
+ return deferred.promise;
+ }
+ });
+
+ /**
+ * 'addon disable' command.
+ */
+ gcli.addCommand({
+ name: "addon disable",
+ description: gcli.lookup("addonDisableDesc"),
+ params: [nameParameter],
+ exec: function(aArgs, context) {
+ /**
+ * Like enable, but ... you know ... the exact opposite.
+ */
+ function disable(aName, addons) {
+ // Find the add-on.
+ let addon = null;
+ addons.some(function(candidate) {
+ if (candidate.name == aName) {
+ addon = candidate;
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ let name = representAddon(addon);
+ let message = "";
+
+ if (addon.userDisabled) {
+ message = gcli.lookupFormat("addonAlreadyDisabled", [name]);
+ } else {
+ addon.userDisabled = true;
+ message = gcli.lookupFormat("addonDisabled", [name]);
+ }
+ this.resolve(message);
+ }
+
+ let deferred = context.defer();
+ // List the installed add-ons, disable one when done listing.
+ AddonManager.getAllAddons(disable.bind(deferred, aArgs.name));
+ return deferred.promise;
+ }
+ });
+ module.CmdAddonFlags.addonsLoaded = true;
+ Services.obs.notifyObservers(null, "gcli_addon_commands_ready", null);
+ });
+
+}(this));
+
+/* CmdCalllog -------------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyGetter(this, "Debugger", function() {
+ let JsDebugger = {};
+ Components.utils.import("resource://gre/modules/jsdebugger.jsm", JsDebugger);
+
+ let global = Components.utils.getGlobalForObject({});
+ JsDebugger.addDebuggerToGlobal(global);
+
+ return global.Debugger;
+ });
+
+ let debuggers = [];
+
+ /**
+ * 'calllog' command
+ */
+ gcli.addCommand({
+ name: "calllog",
+ description: gcli.lookup("calllogDesc")
+ })
+
+ /**
+ * 'calllog start' command
+ */
+ gcli.addCommand({
+ name: "calllog start",
+ description: gcli.lookup("calllogStartDesc"),
+
+ exec: function(args, context) {
+ let contentWindow = context.environment.window;
+
+ let dbg = new Debugger(contentWindow);
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log("Method call: " + this.callDescription(frame));
+ }.bind(this);
+
+ debuggers.push(dbg);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return gcli.lookup("calllogStartReply");
+ },
+
+ callDescription: function(frame) {
+ let name = "<anonymous>";
+ if (frame.callee.name) {
+ name = frame.callee.name;
+ }
+ else {
+ let desc = frame.callee.getOwnPropertyDescriptor("displayName");
+ if (desc && desc.value && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null) {
+ return uneval(value);
+ }
+ return "[object " + value.class + "]";
+ }
+ });
+
+ /**
+ * 'calllog stop' command
+ */
+ gcli.addCommand({
+ name: "calllog stop",
+ description: gcli.lookup("calllogStopDesc"),
+
+ exec: function(args, context) {
+ let numDebuggers = debuggers.length;
+ if (numDebuggers == 0) {
+ return gcli.lookup("calllogStopNoLogging");
+ }
+
+ for (let dbg of debuggers) {
+ dbg.onEnterFrame = undefined;
+ }
+ debuggers = [];
+
+ return gcli.lookupFormat("calllogStopReply", [ numDebuggers ]);
+ }
+ });
+}(this));
+
+/* CmdCalllogChrome -------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyGetter(this, "Debugger", function() {
+ let JsDebugger = {};
+ Cu.import("resource://gre/modules/jsdebugger.jsm", JsDebugger);
+
+ let global = Components.utils.getGlobalForObject({});
+ JsDebugger.addDebuggerToGlobal(global);
+
+ return global.Debugger;
+ });
+
+ let debuggers = [];
+ let sandboxes = [];
+
+ /**
+ * 'calllog chromestart' command
+ */
+ gcli.addCommand({
+ name: "calllog chromestart",
+ description: gcli.lookup("calllogChromeStartDesc"),
+ get hidden() gcli.hiddenByChromePref(),
+ params: [
+ {
+ name: "sourceType",
+ type: {
+ name: "selection",
+ data: ["content-variable", "chrome-variable", "jsm", "javascript"]
+ }
+ },
+ {
+ name: "source",
+ type: "string",
+ description: gcli.lookup("calllogChromeSourceTypeDesc"),
+ manual: gcli.lookup("calllogChromeSourceTypeManual"),
+ }
+ ],
+ exec: function(args, context) {
+ let globalObj;
+ let contentWindow = context.environment.window;
+
+ if (args.sourceType == "jsm") {
+ try {
+ globalObj = Cu.import(args.source);
+ }
+ catch (e) {
+ return gcli.lookup("callLogChromeInvalidJSM");
+ }
+ } else if (args.sourceType == "content-variable") {
+ if (args.source in contentWindow) {
+ globalObj = Cu.getGlobalForObject(contentWindow[args.source]);
+ } else {
+ throw new Error(gcli.lookup("callLogChromeVarNotFoundContent"));
+ }
+ } else if (args.sourceType == "chrome-variable") {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ if (args.source in chromeWin) {
+ globalObj = Cu.getGlobalForObject(chromeWin[args.source]);
+ } else {
+ return gcli.lookup("callLogChromeVarNotFoundChrome");
+ }
+ } else {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ let sandbox = new Cu.Sandbox(chromeWin,
+ {
+ sandboxPrototype: chromeWin,
+ wantXrays: false,
+ sandboxName: "gcli-cmd-calllog-chrome"
+ });
+ let returnVal;
+ try {
+ returnVal = Cu.evalInSandbox(args.source, sandbox, "ECMAv5");
+ sandboxes.push(sandbox);
+ } catch(e) {
+ // We need to save the message before cleaning up else e contains a dead
+ // object.
+ let msg = gcli.lookup("callLogChromeEvalException") + ": " + e;
+ Cu.nukeSandbox(sandbox);
+ return msg;
+ }
+
+ if (typeof returnVal == "undefined") {
+ return gcli.lookup("callLogChromeEvalNeedsObject");
+ }
+
+ globalObj = Cu.getGlobalForObject(returnVal);
+ }
+
+ let dbg = new Debugger(globalObj);
+ debuggers.push(dbg);
+
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log(gcli.lookup("callLogChromeMethodCall") +
+ ": " + this.callDescription(frame));
+ }.bind(this);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return gcli.lookup("calllogChromeStartReply");
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null)
+ return uneval(value);
+ return "[object " + value.class + "]";
+ },
+
+ callDescription: function(frame) {
+ let name = frame.callee.name || gcli.lookup("callLogChromeAnonFunction");
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ }
+ });
+
+ /**
+ * 'calllog chromestop' command
+ */
+ gcli.addCommand({
+ name: "calllog chromestop",
+ description: gcli.lookup("calllogChromeStopDesc"),
+ get hidden() gcli.hiddenByChromePref(),
+ exec: function(args, context) {
+ let numDebuggers = debuggers.length;
+ if (numDebuggers == 0) {
+ return gcli.lookup("calllogChromeStopNoLogging");
+ }
+
+ for (let dbg of debuggers) {
+ dbg.onEnterFrame = undefined;
+ dbg.enabled = false;
+ }
+ for (let sandbox of sandboxes) {
+ Cu.nukeSandbox(sandbox);
+ }
+ debuggers = [];
+ sandboxes = [];
+
+ return gcli.lookupFormat("calllogChromeStopReply", [ numDebuggers ]);
+ }
+ });
+}(this));
+
+/* CmdCmd ------------------------------------------------------------------ */
+
+(function(module) {
+ let prefSvc = "@mozilla.org/preferences-service;1";
+ XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
+ let prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
+ return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+ });
+
+ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+ XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+ const PREF_DIR = "devtools.commands.dir";
+
+ /**
+ * A place to store the names of the commands that we have added as a result of
+ * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
+ * added commands.
+ */
+ let commands = [];
+
+ /**
+ * Exported API
+ */
+ this.CmdCommands = {
+ /**
+ * Called to look in a directory pointed at by the devtools.commands.dir pref
+ * for *.mozcmd files which are then loaded.
+ * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+ * we eval the script from the .mozcmd file. This should be a chrome window.
+ */
+ refreshAutoCommands: function GC_refreshAutoCommands(aSandboxPrincipal) {
+ // First get rid of the last set of commands
+ commands.forEach(function(name) {
+ gcli.removeCommand(name);
+ });
+
+ let dirName = prefBranch.getComplexValue(PREF_DIR,
+ Ci.nsISupportsString).data.trim();
+ if (dirName == "") {
+ return;
+ }
+
+ // replaces ~ with the home directory path in unix and windows
+ if (dirName.indexOf("~") == 0) {
+ let dirService = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+ let homeDirFile = dirService.get("Home", Ci.nsIFile);
+ let homeDir = homeDirFile.path;
+ dirName = dirName.substr(1);
+ dirName = homeDir + dirName;
+ }
+
+ let promise = OS.File.stat(dirName);
+ promise = promise.then(
+ function onSuccess(stat) {
+ if (!stat.isDir) {
+ throw new Error('\'' + dirName + '\' is not a directory.');
+ } else {
+ return dirName;
+ }
+ },
+ function onFailure(reason) {
+ if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) {
+ throw new Error('\'' + dirName + '\' does not exist.');
+ } else {
+ throw reason;
+ }
+ }
+ );
+
+ promise.then(
+ function onSuccess() {
+ let iterator = new OS.File.DirectoryIterator(dirName);
+ let iterPromise = iterator.forEach(
+ function onEntry(entry) {
+ if (entry.name.match(/.*\.mozcmd$/) && !entry.isDir) {
+ loadCommandFile(entry, aSandboxPrincipal);
+ }
+ }
+ );
+
+ iterPromise.then(
+ function onSuccess() {
+ iterator.close();
+ },
+ function onFailure(reason) {
+ iterator.close();
+ throw reason;
+ }
+ );
+ }
+ );
+ }
+ };
+
+ /**
+ * Load the commands from a single file
+ * @param OS.File.DirectoryIterator.Entry aFileEntry The DirectoryIterator
+ * Entry of the file containing the commands that we should read
+ * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+ * we eval the script from the .mozcmd file. This should be a chrome window.
+ */
+ function loadCommandFile(aFileEntry, aSandboxPrincipal) {
+ let promise = OS.File.read(aFileEntry.path);
+ promise = promise.then(
+ function onSuccess(array) {
+ let decoder = new TextDecoder();
+ let source = decoder.decode(array);
+
+ let sandbox = new Cu.Sandbox(aSandboxPrincipal, {
+ sandboxPrototype: aSandboxPrincipal,
+ wantXrays: false,
+ sandboxName: aFileEntry.path
+ });
+ let data = Cu.evalInSandbox(source, sandbox, "1.8", aFileEntry.name, 1);
+
+ if (!Array.isArray(data)) {
+ console.error("Command file '" + aFileEntry.name + "' does not have top level array.");
+ return;
+ }
+
+ data.forEach(function(commandSpec) {
+ gcli.addCommand(commandSpec);
+ commands.push(commandSpec.name);
+ });
+
+ },
+ function onError(reason) {
+ console.error("OS.File.read(" + aFileEntry.path + ") failed.");
+ throw reason;
+ }
+ );
+ }
+
+ /**
+ * 'cmd' command
+ */
+ gcli.addCommand({
+ name: "cmd",
+ get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
+ description: gcli.lookup("cmdDesc")
+ });
+
+ /**
+ * 'cmd refresh' command
+ */
+ gcli.addCommand({
+ name: "cmd refresh",
+ description: gcli.lookup("cmdRefreshDesc"),
+ get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
+ exec: function Command_cmdRefresh(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ CmdCommands.refreshAutoCommands(chromeWindow);
+ }
+ });
+}(this));
+
+/* CmdConsole -------------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+ "resource:///modules/HUDService.jsm");
+
+ /**
+ * 'console' command
+ */
+ gcli.addCommand({
+ name: "console",
+ description: gcli.lookup("consoleDesc"),
+ manual: gcli.lookup("consoleManual")
+ });
+
+ /**
+ * 'console clear' command
+ */
+ gcli.addCommand({
+ name: "console clear",
+ description: gcli.lookup("consoleclearDesc"),
+ exec: function Command_consoleClear(args, context) {
+ let hud = HUDService.getHudByWindow(context.environment.window);
+ // hud will be null if the web console has not been opened for this window
+ if (hud) {
+ hud.jsterm.clearOutput();
+ }
+ }
+ });
+
+ /**
+ * 'console close' command
+ */
+ gcli.addCommand({
+ name: "console close",
+ description: gcli.lookup("consolecloseDesc"),
+ exec: function Command_consoleClose(args, context) {
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.closeToolbox(target);
+ }
+ });
+
+ /**
+ * 'console open' command
+ */
+ gcli.addCommand({
+ name: "console open",
+ description: gcli.lookup("consoleopenDesc"),
+ exec: function Command_consoleOpen(args, context) {
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.showToolbox(target, "webconsole");
+ }
+ });
+}(this));
+
+/* CmdCookie --------------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+ const cookieMgr = Cc["@mozilla.org/cookiemanager;1"]
+ .getService(Ci.nsICookieManager2);
+
+ /**
+ * The template for the 'cookie list' command.
+ */
+ let cookieListHtml = "" +
+ "<ul class='gcli-cookielist-list'>" +
+ " <li foreach='cookie in ${cookies}'>" +
+ " <div>${cookie.name}=${cookie.value}</div>" +
+ " <table class='gcli-cookielist-detail'>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("cookieListOutHost") + "</td>" +
+ " <td>${cookie.host}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("cookieListOutPath") + "</td>" +
+ " <td>${cookie.path}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("cookieListOutExpires") + "</td>" +
+ " <td>${cookie.expires}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("cookieListOutAttributes") + "</td>" +
+ " <td>${cookie.attrs}</td>" +
+ " </tr>" +
+ " <tr><td colspan='2'>" +
+ " <span class='gcli-out-shortcut' onclick='${onclick}'" +
+ " data-command='cookie set ${cookie.name} '" +
+ " >" + gcli.lookup("cookieListOutEdit") + "</span>" +
+ " <span class='gcli-out-shortcut'" +
+ " onclick='${onclick}' ondblclick='${ondblclick}'" +
+ " data-command='cookie remove ${cookie.name}'" +
+ " >" + gcli.lookup("cookieListOutRemove") + "</span>" +
+ " </td></tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>" +
+ "";
+
+ gcli.addConverter({
+ from: "cookies",
+ to: "view",
+ exec: function(cookies, context) {
+ if (cookies.length == 0) {
+ let host = context.environment.document.location.host;
+ let msg = gcli.lookupFormat("cookieListOutNoneHost", [ host ]);
+ return context.createView({ html: "<span>" + msg + "</span>" });
+ }
+
+ for (let cookie of cookies) {
+ cookie.expires = translateExpires(cookie.expires);
+
+ let noAttrs = !cookie.secure && !cookie.httpOnly && !cookie.sameDomain;
+ cookie.attrs = (cookie.secure ? 'secure' : ' ') +
+ (cookie.httpOnly ? 'httpOnly' : ' ') +
+ (cookie.sameDomain ? 'sameDomain' : ' ') +
+ (noAttrs ? gcli.lookup("cookieListOutNone") : ' ');
+ }
+
+ return context.createView({
+ html: cookieListHtml,
+ data: {
+ options: { allowEval: true },
+ cookies: cookies,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ });
+
+ /**
+ * The cookie 'expires' value needs converting into something more readable
+ */
+ function translateExpires(expires) {
+ if (expires == 0) {
+ return gcli.lookup("cookieListOutSession");
+ }
+ return new Date(expires).toLocaleString();
+ }
+
+ /**
+ * Check if a given cookie matches a given host
+ */
+ function isCookieAtHost(cookie, host) {
+ if (cookie.host == null) {
+ return host == null;
+ }
+ if (cookie.host.startsWith(".")) {
+ return cookie.host === "." + host;
+ }
+ else {
+ return cookie.host == host;
+ }
+ }
+
+ /**
+ * 'cookie' command
+ */
+ gcli.addCommand({
+ name: "cookie",
+ description: gcli.lookup("cookieDesc"),
+ manual: gcli.lookup("cookieManual")
+ });
+
+ /**
+ * 'cookie list' command
+ */
+ gcli.addCommand({
+ name: "cookie list",
+ description: gcli.lookup("cookieListDesc"),
+ manual: gcli.lookup("cookieListManual"),
+ returnType: "cookies",
+ exec: function(args, context) {
+ let host = context.environment.document.location.host;
+ if (host == null || host == "") {
+ throw new Error(gcli.lookup("cookieListOutNonePage"));
+ }
+
+ let enm = cookieMgr.getCookiesFromHost(host);
+
+ let cookies = [];
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ cookies.push({
+ host: cookie.host,
+ name: cookie.name,
+ value: cookie.value,
+ path: cookie.path,
+ expires: cookie.expires,
+ secure: cookie.secure,
+ httpOnly: cookie.httpOnly,
+ sameDomain: cookie.sameDomain
+ });
+ }
+ }
+
+ return cookies;
+ }
+ });
+
+ /**
+ * 'cookie remove' command
+ */
+ gcli.addCommand({
+ name: "cookie remove",
+ description: gcli.lookup("cookieRemoveDesc"),
+ manual: gcli.lookup("cookieRemoveManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: gcli.lookup("cookieRemoveKeyDesc"),
+ }
+ ],
+ exec: function(args, context) {
+ let host = context.environment.document.location.host;
+ let enm = cookieMgr.getCookiesFromHost(host);
+
+ let cookies = [];
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ if (cookie.name == args.name) {
+ cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
+ }
+ }
+ }
+ }
+ });
+
+ /**
+ * 'cookie set' command
+ */
+ gcli.addCommand({
+ name: "cookie set",
+ description: gcli.lookup("cookieSetDesc"),
+ manual: gcli.lookup("cookieSetManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: gcli.lookup("cookieSetKeyDesc")
+ },
+ {
+ name: "value",
+ type: "string",
+ description: gcli.lookup("cookieSetValueDesc")
+ },
+ {
+ group: gcli.lookup("cookieSetOptionsDesc"),
+ params: [
+ {
+ name: "path",
+ type: { name: "string", allowBlank: true },
+ defaultValue: "/",
+ description: gcli.lookup("cookieSetPathDesc")
+ },
+ {
+ name: "domain",
+ type: "string",
+ defaultValue: null,
+ description: gcli.lookup("cookieSetDomainDesc")
+ },
+ {
+ name: "secure",
+ type: "boolean",
+ description: gcli.lookup("cookieSetSecureDesc")
+ },
+ {
+ name: "httpOnly",
+ type: "boolean",
+ description: gcli.lookup("cookieSetHttpOnlyDesc")
+ },
+ {
+ name: "session",
+ type: "boolean",
+ description: gcli.lookup("cookieSetSessionDesc")
+ },
+ {
+ name: "expires",
+ type: "string",
+ defaultValue: "Jan 17, 2038",
+ description: gcli.lookup("cookieSetExpiresDesc")
+ },
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ let host = context.environment.document.location.host;
+ let time = Date.parse(args.expires) / 1000;
+
+ cookieMgr.add(args.domain ? "." + args.domain : host,
+ args.path ? args.path : "/",
+ args.name,
+ args.value,
+ args.secure,
+ args.httpOnly,
+ args.session,
+ time);
+ }
+ });
+}(this));
+
+/* CmdExport --------------------------------------------------------------- */
+
+(function(module) {
+ /**
+ * 'export' command
+ */
+ gcli.addCommand({
+ name: "export",
+ description: gcli.lookup("exportDesc"),
+ });
+
+ /**
+ * The 'export html' command. This command allows the user to export the page to
+ * HTML after they do DOM changes.
+ */
+ gcli.addCommand({
+ name: "export html",
+ description: gcli.lookup("exportHtmlDesc"),
+ exec: function(args, context) {
+ let html = context.environment.document.documentElement.outerHTML;
+ let url = 'data:text/plain;charset=utf8,' + encodeURIComponent(html);
+ context.environment.window.open(url);
+ }
+ });
+}(this));
+
+/* CmdJsb ------------------------------------------------------------------ */
+
+(function(module) {
+ const XMLHttpRequest =
+ Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+ XPCOMUtils.defineLazyModuleGetter(this, "js_beautify",
+ "resource:///modules/devtools/Jsbeautify.jsm");
+
+ /**
+ * jsb command.
+ */
+ gcli.addCommand({
+ name: 'jsb',
+ description: gcli.lookup('jsbDesc'),
+ returnValue:'string',
+ params: [
+ {
+ name: 'url',
+ type: 'string',
+ description: gcli.lookup('jsbUrlDesc')
+ },
+ {
+ group: gcli.lookup("jsbOptionsDesc"),
+ params: [
+ {
+ name: 'indentSize',
+ type: 'number',
+ description: gcli.lookup('jsbIndentSizeDesc'),
+ manual: gcli.lookup('jsbIndentSizeManual'),
+ defaultValue: 2
+ },
+ {
+ name: 'indentChar',
+ type: {
+ name: 'selection',
+ lookup: [
+ { name: "space", value: " " },
+ { name: "tab", value: "\t" }
+ ]
+ },
+ description: gcli.lookup('jsbIndentCharDesc'),
+ manual: gcli.lookup('jsbIndentCharManual'),
+ defaultValue: ' ',
+ },
+ {
+ name: 'doNotPreserveNewlines',
+ type: 'boolean',
+ description: gcli.lookup('jsbDoNotPreserveNewlinesDesc')
+ },
+ {
+ name: 'preserveMaxNewlines',
+ type: 'number',
+ description: gcli.lookup('jsbPreserveMaxNewlinesDesc'),
+ manual: gcli.lookup('jsbPreserveMaxNewlinesManual'),
+ defaultValue: -1
+ },
+ {
+ name: 'jslintHappy',
+ type: 'boolean',
+ description: gcli.lookup('jsbJslintHappyDesc'),
+ manual: gcli.lookup('jsbJslintHappyManual')
+ },
+ {
+ name: 'braceStyle',
+ type: {
+ name: 'selection',
+ data: ['collapse', 'expand', 'end-expand', 'expand-strict']
+ },
+ description: gcli.lookup('jsbBraceStyleDesc2'),
+ manual: gcli.lookup('jsbBraceStyleManual2'),
+ defaultValue: "collapse"
+ },
+ {
+ name: 'noSpaceBeforeConditional',
+ type: 'boolean',
+ description: gcli.lookup('jsbNoSpaceBeforeConditionalDesc')
+ },
+ {
+ name: 'unescapeStrings',
+ type: 'boolean',
+ description: gcli.lookup('jsbUnescapeStringsDesc'),
+ manual: gcli.lookup('jsbUnescapeStringsManual')
+ }
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ let opts = {
+ indent_size: args.indentSize,
+ indent_char: args.indentChar,
+ preserve_newlines: !args.doNotPreserveNewlines,
+ max_preserve_newlines: args.preserveMaxNewlines == -1 ?
+ undefined : args.preserveMaxNewlines,
+ jslint_happy: args.jslintHappy,
+ brace_style: args.braceStyle,
+ space_before_conditional: !args.noSpaceBeforeConditional,
+ unescape_strings: args.unescapeStrings
+ };
+
+ let xhr = new XMLHttpRequest();
+
+ try {
+ xhr.open("GET", args.url, true);
+ } catch(e) {
+ return gcli.lookup('jsbInvalidURL');
+ }
+
+ let deferred = context.defer();
+
+ xhr.onreadystatechange = function(aEvt) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status == 0) {
+ let browserDoc = context.environment.chromeDocument;
+ let browserWindow = browserDoc.defaultView;
+ let gBrowser = browserWindow.gBrowser;
+ let result = js_beautify(xhr.responseText, opts);
+
+ browserWindow.Scratchpad.ScratchpadManager.openScratchpad({text: result});
+
+ deferred.resolve();
+ } else {
+ deferred.resolve("Unable to load page to beautify: " + args.url + " " +
+ xhr.status + " " + xhr.statusText);
+ }
+ };
+ }
+ xhr.send(null);
+ return deferred.promise;
+ }
+ });
+}(this));
+
+/* CmdPagemod -------------------------------------------------------------- */
+
+(function(module) {
+ /**
+ * 'pagemod' command
+ */
+ gcli.addCommand({
+ name: "pagemod",
+ description: gcli.lookup("pagemodDesc"),
+ });
+
+ /**
+ * The 'pagemod replace' command. This command allows the user to search and
+ * replace within text nodes and attributes.
+ */
+ gcli.addCommand({
+ name: "pagemod replace",
+ description: gcli.lookup("pagemodReplaceDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: gcli.lookup("pagemodReplaceSearchDesc"),
+ },
+ {
+ name: "replace",
+ type: "string",
+ description: gcli.lookup("pagemodReplaceReplaceDesc"),
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: gcli.lookup("pagemodReplaceIgnoreCaseDesc"),
+ },
+ {
+ name: "selector",
+ type: "string",
+ description: gcli.lookup("pagemodReplaceSelectorDesc"),
+ defaultValue: "*:not(script):not(style):not(embed):not(object):not(frame):not(iframe):not(frameset)",
+ },
+ {
+ name: "root",
+ type: "node",
+ description: gcli.lookup("pagemodReplaceRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "attrOnly",
+ type: "boolean",
+ description: gcli.lookup("pagemodReplaceAttrOnlyDesc"),
+ },
+ {
+ name: "contentOnly",
+ type: "boolean",
+ description: gcli.lookup("pagemodReplaceContentOnlyDesc"),
+ },
+ {
+ name: "attributes",
+ type: "string",
+ description: gcli.lookup("pagemodReplaceAttributesDesc"),
+ defaultValue: null,
+ },
+ ],
+ exec: function(args, context) {
+ let searchTextNodes = !args.attrOnly;
+ let searchAttributes = !args.contentOnly;
+ let regexOptions = args.ignoreCase ? 'ig' : 'g';
+ let search = new RegExp(escapeRegex(args.search), regexOptions);
+ let attributeRegex = null;
+ if (args.attributes) {
+ attributeRegex = new RegExp(args.attributes, regexOptions);
+ }
+
+ let root = args.root || context.environment.document;
+ let elements = root.querySelectorAll(args.selector);
+ elements = Array.prototype.slice.call(elements);
+
+ let replacedTextNodes = 0;
+ let replacedAttributes = 0;
+
+ function replaceAttribute() {
+ replacedAttributes++;
+ return args.replace;
+ }
+ function replaceTextNode() {
+ replacedTextNodes++;
+ return args.replace;
+ }
+
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (searchTextNodes) {
+ for (let y = 0; y < element.childNodes.length; y++) {
+ let node = element.childNodes[y];
+ if (node.nodeType == node.TEXT_NODE) {
+ node.textContent = node.textContent.replace(search, replaceTextNode);
+ }
+ }
+ }
+
+ if (searchAttributes) {
+ if (!element.attributes) {
+ continue;
+ }
+ for (let y = 0; y < element.attributes.length; y++) {
+ let attr = element.attributes[y];
+ if (!attributeRegex || attributeRegex.test(attr.name)) {
+ attr.value = attr.value.replace(search, replaceAttribute);
+ }
+ }
+ }
+ }
+
+ return gcli.lookupFormat("pagemodReplaceResult",
+ [elements.length, replacedTextNodes,
+ replacedAttributes]);
+ }
+ });
+
+ /**
+ * 'pagemod remove' command
+ */
+ gcli.addCommand({
+ name: "pagemod remove",
+ description: gcli.lookup("pagemodRemoveDesc"),
+ });
+
+
+ /**
+ * The 'pagemod remove element' command.
+ */
+ gcli.addCommand({
+ name: "pagemod remove element",
+ description: gcli.lookup("pagemodRemoveElementDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: gcli.lookup("pagemodRemoveElementSearchDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: gcli.lookup("pagemodRemoveElementRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: 'stripOnly',
+ type: 'boolean',
+ description: gcli.lookup("pagemodRemoveElementStripOnlyDesc"),
+ },
+ {
+ name: 'ifEmptyOnly',
+ type: 'boolean',
+ description: gcli.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ let parentNode = element.parentNode;
+ if (!parentNode || !element.removeChild) {
+ continue;
+ }
+ if (args.stripOnly) {
+ while (element.hasChildNodes()) {
+ parentNode.insertBefore(element.childNodes[0], element);
+ }
+ }
+ if (!args.ifEmptyOnly || !element.hasChildNodes()) {
+ element.parentNode.removeChild(element);
+ removed++;
+ }
+ }
+
+ return gcli.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
+ [elements.length, removed]);
+ }
+ });
+
+ /**
+ * The 'pagemod remove attribute' command.
+ */
+ gcli.addCommand({
+ name: "pagemod remove attribute",
+ description: gcli.lookup("pagemodRemoveAttributeDesc"),
+ params: [
+ {
+ name: "searchAttributes",
+ type: "string",
+ description: gcli.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
+ },
+ {
+ name: "searchElements",
+ type: "string",
+ description: gcli.lookup("pagemodRemoveAttributeSearchElementsDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: gcli.lookup("pagemodRemoveAttributeRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: gcli.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let regexOptions = args.ignoreCase ? 'ig' : 'g';
+ let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
+ let elements = root.querySelectorAll(args.searchElements);
+ elements = Array.prototype.slice.call(elements);
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (!element.attributes) {
+ continue;
+ }
+
+ var attrs = Array.prototype.slice.call(element.attributes);
+ for (let y = 0; y < attrs.length; y++) {
+ let attr = attrs[y];
+ if (attributeRegex.test(attr.name)) {
+ element.removeAttribute(attr.name);
+ removed++;
+ }
+ }
+ }
+
+ return gcli.lookupFormat("pagemodRemoveAttributeResult",
+ [elements.length, removed]);
+ }
+ });
+
+ /**
+ * Make a given string safe to use in a regular expression.
+ *
+ * @param string aString
+ * The string you want to use in a regex.
+ * @return string
+ * The equivalent of |aString| but safe to use in a regex.
+ */
+ function escapeRegex(aString) {
+ return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ }
+}(this));
+
+/* CmdTools -------------------------------------------------------------- */
+
+(function(module) {
+ gcli.addCommand({
+ name: "tools",
+ description: gcli.lookupFormat("toolsDesc2", [BRAND_SHORT_NAME]),
+ manual: gcli.lookupFormat("toolsManual2", [BRAND_SHORT_NAME]),
+ get hidden() gcli.hiddenByChromePref(),
+ });
+
+ gcli.addCommand({
+ name: "tools srcdir",
+ description: gcli.lookup("toolsSrcdirDesc"),
+ manual: gcli.lookupFormat("toolsSrcdirManual2", [BRAND_SHORT_NAME]),
+ get hidden() gcli.hiddenByChromePref(),
+ params: [
+ {
+ name: "srcdir",
+ type: "string",
+ description: gcli.lookup("toolsSrcdirDir")
+ }
+ ],
+ returnType: "string",
+ exec: function(args, context) {
+ let clobber = OS.Path.join(args.srcdir, "CLOBBER");
+ return OS.File.exists(clobber).then(function(exists) {
+ if (exists) {
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = args.srcdir;
+ Services.prefs.setComplexValue("devtools.loader.srcdir",
+ Ci.nsISupportsString, str);
+ devtools.reload();
+
+ let msg = gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir]);
+ throw new Error(msg);
+ }
+
+ return gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir]);
+ });
+ }
+ });
+
+ gcli.addCommand({
+ name: "tools builtin",
+ description: gcli.lookup("toolsBuiltinDesc"),
+ manual: gcli.lookup("toolsBuiltinManual"),
+ get hidden() gcli.hiddenByChromePref(),
+ returnType: "string",
+ exec: function(args, context) {
+ Services.prefs.clearUserPref("devtools.loader.srcdir");
+ devtools.reload();
+ return gcli.lookup("toolsBuiltinReloaded");
+ }
+ });
+
+ gcli.addCommand({
+ name: "tools reload",
+ description: gcli.lookup("toolsReloadDesc"),
+ get hidden() gcli.hiddenByChromePref() || !Services.prefs.prefHasUserValue("devtools.loader.srcdir"),
+
+ returnType: "string",
+ exec: function(args, context) {
+ devtools.reload();
+ return gcli.lookup("toolsReloaded2");
+ }
+ });
+}(this));
+
+/* CmdRestart -------------------------------------------------------------- */
+
+(function(module) {
+ /**
+ * Restart command
+ *
+ * @param boolean nocache
+ * Disables loading content from cache upon restart.
+ *
+ * Examples :
+ * >> restart
+ * - restarts browser immediately
+ * >> restart --nocache
+ * - restarts immediately and starts Firefox without using cache
+ */
+ gcli.addCommand({
+ name: "restart",
+ description: gcli.lookupFormat("restartBrowserDesc", [BRAND_SHORT_NAME]),
+ params: [
+ {
+ name: "nocache",
+ type: "boolean",
+ description: gcli.lookup("restartBrowserNocacheDesc")
+ }
+ ],
+ returnType: "string",
+ exec: function Restart(args, context) {
+ let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
+ if (canceled.data) {
+ return gcli.lookup("restartBrowserRequestCancelled");
+ }
+
+ // disable loading content from cache.
+ if (args.nocache) {
+ Services.appinfo.invalidateCachesOnRestart();
+ }
+
+ // restart
+ Cc['@mozilla.org/toolkit/app-startup;1']
+ .getService(Ci.nsIAppStartup)
+ .quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+ return gcli.lookupFormat("restartBrowserRestarting", [BRAND_SHORT_NAME]);
+ }
+ });
+}(this));
+
+/* CmdScreenshot ----------------------------------------------------------- */
+
+(function(module) {
+ XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
+ "resource:///modules/devtools/LayoutHelpers.jsm");
+
+ // String used as an indication to generate default file name in the following
+ // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
+ const FILENAME_DEFAULT_VALUE = " ";
+
+ /**
+ * 'screenshot' command
+ */
+ gcli.addCommand({
+ name: "screenshot",
+ description: gcli.lookup("screenshotDesc"),
+ manual: gcli.lookup("screenshotManual"),
+ returnType: "dom",
+ params: [
+ {
+ name: "filename",
+ type: "string",
+ defaultValue: FILENAME_DEFAULT_VALUE,
+ description: gcli.lookup("screenshotFilenameDesc"),
+ manual: gcli.lookup("screenshotFilenameManual")
+ },
+ {
+ group: gcli.lookup("screenshotGroupOptions"),
+ params: [
+ {
+ name: "clipboard",
+ type: "boolean",
+ description: gcli.lookup("screenshotClipboardDesc"),
+ manual: gcli.lookup("screenshotClipboardManual")
+ },
+ {
+ name: "chrome",
+ type: "boolean",
+ description: gcli.lookupFormat("screenshotChromeDesc2", [BRAND_SHORT_NAME]),
+ manual: gcli.lookupFormat("screenshotChromeManual2", [BRAND_SHORT_NAME])
+ },
+ {
+ name: "delay",
+ type: { name: "number", min: 0 },
+ defaultValue: 0,
+ description: gcli.lookup("screenshotDelayDesc"),
+ manual: gcli.lookup("screenshotDelayManual")
+ },
+ {
+ name: "fullpage",
+ type: "boolean",
+ description: gcli.lookup("screenshotFullPageDesc"),
+ manual: gcli.lookup("screenshotFullPageManual")
+ },
+ {
+ name: "selector",
+ type: "node",
+ defaultValue: null,
+ description: gcli.lookup("inspectNodeDesc"),
+ manual: gcli.lookup("inspectNodeManual")
+ }
+ ]
+ }
+ ],
+ exec: function Command_screenshot(args, context) {
+ if (args.chrome && args.selector) {
+ // Node screenshot with chrome option does not work as inteded
+ // Refer https://bugzilla.mozilla.org/show_bug.cgi?id=659268#c7
+ // throwing for now.
+ throw new Error(gcli.lookup("screenshotSelectorChromeConflict"));
+ }
+ var document = args.chrome? context.environment.chromeDocument
+ : context.environment.document;
+ if (args.delay > 0) {
+ var deferred = context.defer();
+ document.defaultView.setTimeout(function Command_screenshotDelay() {
+ let reply = this.grabScreen(document, args.filename, args.clipboard,
+ args.fullpage);
+ deferred.resolve(reply);
+ }.bind(this), args.delay * 1000);
+ return deferred.promise;
+ }
+ else {
+ return this.grabScreen(document, args.filename, args.clipboard,
+ args.fullpage, args.selector);
+ }
+ },
+ grabScreen: function(document, filename, clipboard, fullpage, node) {
+ let window = document.defaultView;
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ let left = 0;
+ let top = 0;
+ let width;
+ let height;
+ let div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+
+ if (!fullpage) {
+ if (!node) {
+ left = window.scrollX;
+ top = window.scrollY;
+ width = window.innerWidth;
+ height = window.innerHeight;
+ } else {
+ let rect = LayoutHelpers.getRect(node, window);
+ top = rect.top;
+ left = rect.left;
+ width = rect.width;
+ height = rect.height;
+ }
+ } else {
+ width = window.innerWidth + window.scrollMaxX;
+ height = window.innerHeight + window.scrollMaxY;
+ }
+ canvas.width = width;
+ canvas.height = height;
+
+ let ctx = canvas.getContext("2d");
+ ctx.drawWindow(window, left, top, width, height, "#fff");
+ let data = canvas.toDataURL("image/png", "");
+
+ let loadContext = document.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+
+ try {
+ if (clipboard) {
+ let io = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+ let channel = io.newChannel(data, null, null);
+ let input = channel.open();
+ let imgTools = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools);
+
+ let container = {};
+ imgTools.decodeImageData(input, channel.contentType, container);
+
+ let wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Ci.nsISupportsInterfacePointer);
+ wrapped.data = container.value;
+
+ let trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(loadContext);
+ trans.addDataFlavor(channel.contentType);
+ trans.setTransferData(channel.contentType, wrapped, -1);
+
+ let clipid = Ci.nsIClipboard;
+ let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
+ clip.setData(trans, null, clipid.kGlobalClipboard);
+ div.textContent = gcli.lookup("screenshotCopied");
+ return div;
+ }
+ }
+ catch (ex) {
+ div.textContent = gcli.lookup("screenshotErrorCopying");
+ return div;
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+
+ // Create a name for the file if not present
+ if (filename == FILENAME_DEFAULT_VALUE) {
+ let date = new Date();
+ let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
+ "-" + date.getDate();
+ dateString = dateString.split("-").map(function(part) {
+ if (part.length == 1) {
+ part = "0" + part;
+ }
+ return part;
+ }).join("-");
+ let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ filename = gcli.lookupFormat("screenshotGeneratedFilename",
+ [dateString, timeString]) + ".png";
+ }
+ // Check there is a .png extension to filename
+ else if (!filename.match(/.png$/i)) {
+ filename += ".png";
+ }
+
+ // If the filename is relative, tack it onto the download directory
+ if (!filename.match(/[\\\/]/)) {
+ let downloadMgr = Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ let tempfile = downloadMgr.userDownloadsDirectory;
+ tempfile.append(filename);
+ filename = tempfile.path;
+ }
+
+ try {
+ file.initWithPath(filename);
+ } catch (ex) {
+ div.textContent = gcli.lookup("screenshotErrorSavingToFile") + " " + filename;
+ return div;
+ }
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+ let Persist = Ci.nsIWebBrowserPersist;
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Persist);
+ persist.persistFlags = Persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Persist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ let source = ioService.newURI(data, "UTF8", null);
+ persist.saveURI(source, null, null, null, null, file, loadContext);
+
+ div.textContent = gcli.lookup("screenshotSavedToFile") + " \"" + filename +
+ "\"";
+ div.addEventListener("click", function openFile() {
+ div.removeEventListener("click", openFile);
+ file.reveal();
+ });
+ div.style.cursor = "pointer";
+ let image = document.createElement("div");
+ let previewHeight = parseInt(256*height/width);
+ image.setAttribute("style",
+ "width:256px; height:" + previewHeight + "px;" +
+ "max-height: 256px;" +
+ "background-image: url('" + data + "');" +
+ "background-size: 256px " + previewHeight + "px;" +
+ "margin: 4px; display: block");
+ div.appendChild(image);
+ return div;
+ }
+ });
+}(this));
+
+
+/* Remoting ----------------------------------------------------------- */
+
+const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
+/**
+ * 'listen' command
+ */
+gcli.addCommand({
+ name: "listen",
+ description: gcli.lookup("listenDesc"),
+ manual: gcli.lookupFormat("listenManual2", [BRAND_SHORT_NAME]),
+ params: [
+ {
+ name: "port",
+ type: "number",
+ get defaultValue() {
+ return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+ },
+ description: gcli.lookup("listenPortDesc"),
+ }
+ ],
+ exec: function Command_screenshot(args, context) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ var reply = DebuggerServer.openListener(args.port);
+ if (!reply) {
+ throw new Error(gcli.lookup("listenDisabledOutput"));
+ }
+
+ if (DebuggerServer.initialized) {
+ return gcli.lookupFormat("listenInitOutput", [ '' + args.port ]);
+ }
+
+ return gcli.lookup("listenNoInitOutput");
+ },
+});
+
+
+/* CmdPaintFlashing ------------------------------------------------------- */
+
+(function(module) {
+ /**
+ * 'paintflashing' command
+ */
+ gcli.addCommand({
+ name: 'paintflashing',
+ description: gcli.lookup('paintflashingDesc')
+ });
+
+ gcli.addCommand({
+ name: 'paintflashing on',
+ description: gcli.lookup('paintflashingOnDesc'),
+ manual: gcli.lookup('paintflashingManual'),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() gcli.hiddenByChromePref(),
+ description: gcli.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function(args, context) {
+ var window = args.chrome ?
+ context.environment.chromeWindow :
+ context.environment.window;
+
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .paintFlashing = true;
+ onPaintFlashingChanged(context);
+ }
+ });
+
+ gcli.addCommand({
+ name: 'paintflashing off',
+ description: gcli.lookup('paintflashingOffDesc'),
+ manual: gcli.lookup('paintflashingManual'),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() gcli.hiddenByChromePref(),
+ description: gcli.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function(args, context) {
+ var window = args.chrome ?
+ context.environment.chromeWindow :
+ context.environment.window;
+
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .paintFlashing = false;
+ onPaintFlashingChanged(context);
+ }
+ });
+
+ gcli.addCommand({
+ name: 'paintflashing toggle',
+ hidden: true,
+ buttonId: "command-button-paintflashing",
+ buttonClass: "command-button",
+ state: {
+ isChecked: function(aTarget) {
+ if (aTarget.isLocalTab) {
+ let window = aTarget.tab.linkedBrowser.contentWindow;
+ let wUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ return wUtils.paintFlashing;
+ } else {
+ throw new Error("Unsupported target");
+ }
+ },
+ onChange: function(aTarget, aChangeHandler) {
+ eventEmitter.on("changed", aChangeHandler);
+ },
+ offChange: function(aTarget, aChangeHandler) {
+ eventEmitter.off("changed", aChangeHandler);
+ },
+ },
+ tooltipText: gcli.lookup("paintflashingTooltip"),
+ description: gcli.lookup('paintflashingToggleDesc'),
+ manual: gcli.lookup('paintflashingManual'),
+ exec: function(args, context) {
+ var window = context.environment.window;
+ var wUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ wUtils.paintFlashing = !wUtils.paintFlashing;
+ onPaintFlashingChanged(context);
+ }
+ });
+
+ let eventEmitter = new EventEmitter();
+ function onPaintFlashingChanged(context) {
+ var gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ var tab = gBrowser.selectedTab;
+ eventEmitter.emit("changed", tab);
+ function fireChange() {
+ eventEmitter.emit("changed", tab);
+ }
+ var target = devtools.TargetFactory.forTab(tab);
+ target.off("navigate", fireChange);
+ target.once("navigate", fireChange);
+
+ var window = context.environment.window;
+ var wUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ if (wUtils.paintFlashing) {
+ telemetry.toolOpened("paintflashing");
+ } else {
+ telemetry.toolClosed("paintflashing");
+ }
+ }
+}(this));
+
+/* CmdAppCache ------------------------------------------------------- */
+
+(function(module) {
+ /**
+ * 'appcache' command
+ */
+
+ gcli.addCommand({
+ name: 'appcache',
+ description: gcli.lookup('appCacheDesc')
+ });
+
+ gcli.addConverter({
+ from: "appcacheerrors",
+ to: "view",
+ exec: function([errors, manifestURI], context) {
+ if (errors.length == 0) {
+ return context.createView({
+ html: "<span>" + gcli.lookup("appCacheValidatedSuccessfully") + "</span>"
+ });
+ }
+
+ let appcacheValidateHtml =
+ "<h4>Manifest URI: ${manifestURI}</h4>" +
+ "<ol>" +
+ " <li foreach='error in ${errors}'>" +
+ " ${error.msg}" +
+ " </li>" +
+ "</ol>";
+
+ return context.createView({
+ html: "<div>" + appcacheValidateHtml + "</div>",
+ data: {
+ errors: errors,
+ manifestURI: manifestURI
+ }
+ });
+ }
+ });
+
+ gcli.addCommand({
+ name: 'appcache validate',
+ description: gcli.lookup('appCacheValidateDesc'),
+ manual: gcli.lookup('appCacheValidateManual'),
+ returnType: 'appcacheerrors',
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "uri",
+ description: gcli.lookup("appCacheValidateUriDesc"),
+ defaultValue: null,
+ }
+ ]
+ }],
+ exec: function(args, context) {
+ let utils;
+ let deferred = context.defer();
+
+ if (args.uri) {
+ utils = new AppCacheUtils(args.uri);
+ } else {
+ utils = new AppCacheUtils(context.environment.document);
+ }
+
+ utils.validateManifest().then(function(errors) {
+ deferred.resolve([errors, utils.manifestURI || "-"]);
+ });
+
+ return deferred.promise;
+ }
+ });
+
+ gcli.addCommand({
+ name: 'appcache clear',
+ description: gcli.lookup('appCacheClearDesc'),
+ manual: gcli.lookup('appCacheClearManual'),
+ exec: function(args, context) {
+ let utils = new AppCacheUtils(args.uri);
+ utils.clearAll();
+
+ return gcli.lookup("appCacheClearCleared");
+ }
+ });
+
+ let appcacheListEntries = "" +
+ "<ul class='gcli-appcache-list'>" +
+ " <li foreach='entry in ${entries}'>" +
+ " <table class='gcli-appcache-detail'>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListKey") + "</td>" +
+ " <td>${entry.key}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListFetchCount") + "</td>" +
+ " <td>${entry.fetchCount}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListLastFetched") + "</td>" +
+ " <td>${entry.lastFetched}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListLastModified") + "</td>" +
+ " <td>${entry.lastModified}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListExpirationTime") + "</td>" +
+ " <td>${entry.expirationTime}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListDataSize") + "</td>" +
+ " <td>${entry.dataSize}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + gcli.lookup("appCacheListDeviceID") + "</td>" +
+ " <td>${entry.deviceID} <span class='gcli-out-shortcut' " +
+ "onclick='${onclick}' ondblclick='${ondblclick}' " +
+ "data-command='appcache viewentry ${entry.key}'" +
+ ">" + gcli.lookup("appCacheListViewEntry") + "</span>" +
+ " </td>" +
+ " </tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>";
+
+ gcli.addConverter({
+ from: "appcacheentries",
+ to: "view",
+ exec: function(entries, context) {
+ return context.createView({
+ html: appcacheListEntries,
+ data: {
+ entries: entries,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ });
+
+ gcli.addCommand({
+ name: 'appcache list',
+ description: gcli.lookup('appCacheListDesc'),
+ manual: gcli.lookup('appCacheListManual'),
+ returnType: "appcacheentries",
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "search",
+ description: gcli.lookup("appCacheListSearchDesc"),
+ defaultValue: null,
+ },
+ ]
+ }],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.listEntries(args.search);
+ }
+ });
+
+ gcli.addCommand({
+ name: 'appcache viewentry',
+ description: gcli.lookup('appCacheViewEntryDesc'),
+ manual: gcli.lookup('appCacheViewEntryManual'),
+ params: [
+ {
+ type: "string",
+ name: "key",
+ description: gcli.lookup("appCacheViewEntryKey"),
+ defaultValue: null,
+ }
+ ],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.viewEntry(args.key);
+ }
+ });
+}(this));
diff --git a/browser/devtools/commandline/Commands.jsm b/browser/devtools/commandline/Commands.jsm
new file mode 100644
index 000000000..bf1759c2d
--- /dev/null
+++ b/browser/devtools/commandline/Commands.jsm
@@ -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/. */
+
+
+this.EXPORTED_SYMBOLS = [ ];
+
+const Cu = Components.utils;
+
+Cu.import("resource:///modules/devtools/BuiltinCommands.jsm");
+Cu.import("resource:///modules/devtools/CmdDebugger.jsm");
+Cu.import("resource:///modules/devtools/CmdEdit.jsm");
+Cu.import("resource:///modules/devtools/CmdInspect.jsm");
+Cu.import("resource:///modules/devtools/CmdResize.jsm");
+Cu.import("resource:///modules/devtools/CmdTilt.jsm");
+Cu.import("resource:///modules/devtools/CmdScratchpad.jsm");
+Cu.import("resource:///modules/devtools/cmd-profiler.jsm");
diff --git a/browser/devtools/commandline/Makefile.in b/browser/devtools/commandline/Makefile.in
new file mode 100644
index 000000000..8890154f7
--- /dev/null
+++ b/browser/devtools/commandline/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/commandline/commandline.css b/browser/devtools/commandline/commandline.css
new file mode 100644
index 000000000..075fa37e8
--- /dev/null
+++ b/browser/devtools/commandline/commandline.css
@@ -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/. */
+
+.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-option {
+ overflow: hidden;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.gcli-menu-template {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.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;
+}
diff --git a/browser/devtools/commandline/commandlineoutput.xhtml b/browser/devtools/commandline/commandlineoutput.xhtml
new file mode 100644
index 000000000..88b7607f8
--- /dev/null
+++ b/browser/devtools/commandline/commandlineoutput.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://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/content/devtools/commandline.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/commandline.css" type="text/css"/>
+</head>
+<body class="gcli-body">
+<div id="gcli-output-root"></div>
+</body>
+</html>
diff --git a/browser/devtools/commandline/commandlinetooltip.xhtml b/browser/devtools/commandline/commandlinetooltip.xhtml
new file mode 100644
index 000000000..1c0231e69
--- /dev/null
+++ b/browser/devtools/commandline/commandlinetooltip.xhtml
@@ -0,0 +1,19 @@
+<?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://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/content/devtools/commandline.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/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/browser/devtools/commandline/gcli.jsm b/browser/devtools/commandline/gcli.jsm
new file mode 100644
index 000000000..d018c5f68
--- /dev/null
+++ b/browser/devtools/commandline/gcli.jsm
@@ -0,0 +1,20 @@
+/*
+ * 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.EXPORTED_SYMBOLS = [ "gcli" ];
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
diff --git a/browser/devtools/commandline/moz.build b/browser/devtools/commandline/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/commandline/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/commandline/test/Makefile.in b/browser/devtools/commandline/test/Makefile.in
new file mode 100644
index 000000000..b4c944914
--- /dev/null
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_cmd_addon.js \
+ $(browser_cmd_calllog.js disabled until bug 845831 is fixed) \
+ $(browser_cmd_calllog_chrome.js disabled until bug 845831 is fixed) \
+ browser_cmd_appcache_invalid.js \
+ 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 \
+ 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.html \
+ browser_cmd_cookie.js \
+ browser_cmd_jsb.js \
+ browser_cmd_jsb_script.jsi \
+ browser_cmd_pagemod_export.html \
+ browser_cmd_pagemod_export.js \
+ browser_cmd_pref.js \
+ browser_cmd_restart.js \
+ browser_cmd_screenshot.html \
+ browser_cmd_screenshot.js \
+ browser_cmd_settings.js \
+ browser_gcli_canon.js \
+ browser_gcli_cli.js \
+ browser_gcli_completion.js \
+ browser_gcli_exec.js \
+ browser_gcli_focus.js \
+ browser_gcli_history.js \
+ browser_gcli_incomplete.js \
+ browser_gcli_inputter.js \
+ browser_gcli_intro.js \
+ browser_gcli_js.js \
+ browser_gcli_keyboard1.js \
+ browser_gcli_keyboard2.js \
+ browser_gcli_keyboard3.js \
+ browser_gcli_menu.js \
+ browser_gcli_node.js \
+ browser_gcli_remote.js \
+ browser_gcli_resource.js \
+ browser_gcli_scratchpad.js \
+ browser_gcli_spell.js \
+ browser_gcli_split.js \
+ browser_gcli_tokenize.js \
+ browser_gcli_tooltip.js \
+ browser_gcli_types.js \
+ browser_gcli_util.js \
+ head.js \
+ helpers.js \
+ mockCommands.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/commandline/test/browser_cmd_addon.js b/browser/devtools/commandline/test/browser_cmd_addon.js
new file mode 100644
index 000000000..1ea8fc4d0
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_addon.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
+let CmdAddonFlags = (Cu.import("resource:///modules/devtools/BuiltinCommands.jsm", {})).CmdAddonFlags;
+
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar("about:blank", function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
+
+tests.gatTest = function(options) {
+ let deferred = Promise.defer();
+
+ let onGatReady = function() {
+ Services.obs.removeObserver(onGatReady, "gcli_addon_commands_ready");
+ info("gcli_addon_commands_ready notification received, running tests");
+
+ let auditDone = 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' },
+ name: { value: 'Test Plug-in', status: 'VALID' },
+ }
+ },
+ exec: {
+ output: 'Test Plug-in 1.0.0.0 enabled.'
+ }
+ }
+ ]);
+
+ auditDone.then(function() {
+ deferred.resolve();
+ });
+ };
+
+ Services.obs.addObserver(onGatReady, "gcli_addon_commands_ready", false);
+
+ if (CmdAddonFlags.addonsLoaded) {
+ info("The call to AddonManager.getAllAddons in BuiltinCommands.jsm is done.");
+ info("Send the gcli_addon_commands_ready notification ourselves.");
+
+ Services.obs.notifyObservers(null, "gcli_addon_commands_ready", null);
+ } else {
+ info("Waiting for gcli_addon_commands_ready notification.");
+ }
+
+ return deferred.promise;
+};
diff --git a/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js b/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js
new file mode 100644
index 000000000..28f8ce80d
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js
@@ -0,0 +1,102 @@
+/* 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/browser/devtools/commandline/" +
+ "test/browser_cmd_appcache_invalid_index.html";
+
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ let deferred = Promise.defer();
+
+ // Wait for site to be cached.
+ gBrowser.contentWindow.applicationCache.addEventListener('error', function BCAI_error() {
+ gBrowser.contentWindow.applicationCache.removeEventListener('error', BCAI_error);
+
+ info("Site now cached, running tests.");
+
+ deferred.resolve(helpers.audit(options, [
+ {
+ setup: 'appcache validate',
+ check: {
+ input: 'appcache validate',
+ markup: 'VVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {}
+ },
+ exec: {
+ completed: false,
+ output: [
+ /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 "CACHE MANIFEST" at line 1\./,
+ /"CACHE MANIFEST" 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\./,
+ /NETWORK section line 13 \(\/checking\.cgi\) prevents caching of line 13 \(\/checking\.cgi\) in the NETWORK section\./,
+ /\/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\/browser\/devtools\/commandline\/test\/browser_cmd_appcache_invalid_page3\.html was modified after http:\/\/sub1\.test1\.example\.com\/browser\/browser\/devtools\/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, "prefer-online" or "fast" 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\./
+ ]
+ },
+ },
+ ]));
+ });
+
+ acceptOfflineCachePrompt();
+
+ return deferred.promise;
+ }).then(finish);
+
+ function acceptOfflineCachePrompt() {
+ // 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.");
+ }
+ }
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache
new file mode 100644
index 000000000..75b5d7bad
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^ b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_appcache.appcache^headers^
new file mode 100644
index 000000000..af95ed1f5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_index.html b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_index.html
new file mode 100644
index 000000000..67f9aa675
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page1.html b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page1.html
new file mode 100644
index 000000000..5ff36f102
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page2.html b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page2.html
new file mode 100644
index 000000000..7d4a0c44d
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html
new file mode 100644
index 000000000..6777e59f8
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^ b/browser/devtools/commandline/test/browser_cmd_appcache_invalid_page3.html^headers^
new file mode 100644
index 000000000..177130b43
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid.js b/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
new file mode 100644
index 000000000..d52b5a3c9
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
@@ -0,0 +1,179 @@
+/* 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/browser/devtools/" +
+ "commandline/test/browser_cmd_appcache_valid_index.html";
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ let deferred = Promise.defer();
+
+ info("adding cache listener.");
+
+ // Wait for site to be cached.
+ gBrowser.contentWindow.applicationCache.addEventListener('cached', function BCAV_cached() {
+ gBrowser.contentWindow.applicationCache.removeEventListener('cached', BCAV_cached);
+
+ info("Site now cached, running tests.");
+
+ deferred.resolve(helpers.audit(options, [
+ {
+ setup: 'appcache',
+ check: {
+ input: 'appcache',
+ markup: 'IIIIIIII',
+ status: 'ERROR',
+ args: {}
+ },
+ },
+
+ {
+ setup: function() {
+ Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+ 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.contains("index"), "index is not contained in output");
+ }
+ },
+
+ {
+ setup: 'appcache validate',
+ check: {
+ input: 'appcache validate',
+ markup: 'VVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {}
+ },
+ exec: {
+ completed: false,
+ output: [ /successfully/ ]
+ },
+ },
+
+ {
+ setup: 'appcache validate ' + TEST_URI,
+ check: {
+ input: 'appcache validate ' + TEST_URI,
+ // appcache validate http://sub1.test2.example.com/browser/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ uri: {
+ value: TEST_URI
+ },
+ }
+ },
+ exec: {
+ completed: false,
+ 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.contains("index"), "index is not contained in output");
+ ok(!text.contains("page1"), "page1 is not contained in output");
+ ok(!text.contains("page2"), "page1 is not contained in output");
+ ok(!text.contains("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/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {}
+ },
+ },
+ ]));
+ });
+
+ acceptOfflineCachePrompt();
+
+ return deferred.promise;
+ }).then(finish);
+
+ function acceptOfflineCachePrompt() {
+ // 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.");
+ }
+ }
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache b/browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache
new file mode 100644
index 000000000..4f62825e9
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^ b/browser/devtools/commandline/test/browser_cmd_appcache_valid_appcache.appcache^headers^
new file mode 100644
index 000000000..d1a0abd3f
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html b/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html
new file mode 100644
index 000000000..1ab3f3e31
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid_page1.html b/browser/devtools/commandline/test/browser_cmd_appcache_valid_page1.html
new file mode 100644
index 000000000..e0bb429e7
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid_page2.html b/browser/devtools/commandline/test/browser_cmd_appcache_valid_page2.html
new file mode 100644
index 000000000..1ce36b319
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_appcache_valid_page3.html b/browser/devtools/commandline/test/browser_cmd_appcache_valid_page3.html
new file mode 100644
index 000000000..074ff7d41
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_calllog.js b/browser/devtools/commandline/test/browser_cmd_calllog.js
new file mode 100644
index 000000000..baf32c629
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_calllog.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the calllog commands works as they should
+
+let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog";
+
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
+
+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.hudId in HUDService.hudReferences, "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");
+ deferred.resolve();
+ });
+ return deferred.promise;
+ },
+ exec: {
+ output: /Call logging started/,
+ },
+ },
+ ]);
+
+ return deferred.promise;
+};
diff --git a/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js b/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
new file mode 100644
index 000000000..66569b38c
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the calllog commands works as they should
+
+let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
+
+const TEST_URI = "data:text/html;charset=utf-8,cmd-calllog-chrome";
+
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
+
+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.hudId in HUDService.hudReferences, "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: '',
+ completed: false,
+ },
+ },
+ ]).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/browser/devtools/commandline/test/browser_cmd_commands.js b/browser/devtools/commandline/test/browser_cmd_commands.js
new file mode 100644
index 000000000..4b8bc8203
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test various GCLI commands
+
+let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-commands";
+
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
+
+tests.testConsole = function(options) {
+ let deferred = Promise.defer();
+ let hud = null;
+
+ let onWebConsoleOpen = function(subject) {
+ Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
+
+ subject.QueryInterface(Ci.nsISupportsString);
+ hud = HUDService.getHudReferenceById(subject.data);
+ ok(hud.hudId in HUDService.hudReferences, "console open");
+
+ hud.jsterm.execute("pprint(window)", onExecute);
+ }
+ Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
+
+ let onExecute = function() {
+ let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+ ok(labels.length > 0, "output for pprint(window)");
+
+ helpers.audit(options, [
+ {
+ setup: "console clear",
+ exec: {
+ output: ""
+ },
+ post: function() {
+ let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+ // Bug 845827 - The GCLI "console clear" command doesn't always work
+ // is(labels.length, 0, "no output in console");
+ }
+ },
+ {
+ setup: "console close",
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(!(hud.hudId in HUDService.hudReferences), "console closed");
+ }
+ }
+ ]).then(function() {
+ deferred.resolve();
+ });
+ };
+
+ helpers.audit(options, [
+ {
+ setup: "console open",
+ exec: { }
+ }
+ ]);
+
+ return deferred.promise;
+};
diff --git a/browser/devtools/commandline/test/browser_cmd_cookie.html b/browser/devtools/commandline/test/browser_cmd_cookie.html
new file mode 100644
index 000000000..e9b385a35
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.html
@@ -0,0 +1,18 @@
+<!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.getElementById("result").innerHTML = document.cookie;
+ </script>
+
+</body>
+</html>
diff --git a/browser/devtools/commandline/test/browser_cmd_cookie.js b/browser/devtools/commandline/test/browser_cmd_cookie.js
new file mode 100644
index 000000000..520cbf064
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/"+
+ "test/browser_cmd_cookie.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'cookie',
+ check: {
+ input: 'cookie',
+ hints: '',
+ 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.contains("zip"), "");
+ ok(!text.contains("zop"), "");
+ }
+ },
+ {
+ setup: "cookie remove zap",
+ exec: { },
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: [ /zup=banana/, /Edit/ ]
+ },
+ post: function(output, text) {
+ ok(!text.contains("zap"), "");
+ ok(!text.contains("zep"), "");
+ ok(!text.contains("zip"), "");
+ ok(!text.contains("zop"), "");
+ }
+ },
+ {
+ setup: "cookie remove zup",
+ exec: { }
+ },
+ {
+ setup: "cookie list",
+ exec: {
+ output: 'No cookies found for host example.com'
+ },
+ post: function(output, text) {
+ ok(!text.contains("zap"), "");
+ ok(!text.contains("zep"), "");
+ ok(!text.contains("zip"), "");
+ ok(!text.contains("zop"), "");
+ ok(!text.contains("zup"), "");
+ ok(!text.contains("banana"), "");
+ ok(!text.contains("Edit"), "");
+ }
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_jsb.js b/browser/devtools/commandline/test/browser_cmd_jsb.js
new file mode 100644
index 000000000..4ec09906a
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_jsb.js
@@ -0,0 +1,86 @@
+/* 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/browser/devtools/commandline/" +
+ "test/browser_cmd_jsb_script.jsi";
+
+let scratchpadWin = null;
+let scratchpad = null;
+let tests = {};
+
+function test() {
+ helpers.addTabWithToolbar("about:blank", function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
+
+tests.jsbTest = function(options) {
+ let deferred = Promise.defer();
+
+ let observer = {
+ onReady: function() {
+ scratchpad.removeObserver(observer);
+
+ 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;
+ }
+ deferred.resolve();
+ },
+ };
+
+ let onLoad = function GDT_onLoad() {
+ scratchpadWin.removeEventListener("load", onLoad, false);
+ scratchpad = scratchpadWin.Scratchpad;
+
+ scratchpad.addObserver(observer);
+ };
+
+ let onNotify = function(subject, topic, data) {
+ if (topic == "domwindowopened") {
+ Services.ww.unregisterNotification(onNotify);
+
+ scratchpadWin = subject.QueryInterface(Ci.nsIDOMWindow);
+ scratchpadWin.addEventListener("load", onLoad, false);
+ }
+ };
+
+ Services.ww.registerNotification(onNotify);
+
+ helpers.audit(options, [
+ {
+ setup: 'jsb',
+ check: {
+ input: 'jsb',
+ hints: ' <url> [options]',
+ markup: 'VVV',
+ status: 'ERROR'
+ }
+ },
+ {
+ setup: 'jsb ' + TEST_URI,
+ // Should result in a new window, which should fire onReady (eventually)
+ exec: {
+ completed: false
+ }
+ }
+ ]);
+
+ return deferred.promise;
+};
diff --git a/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi b/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi
new file mode 100644
index 000000000..dcaac807c
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_pagemod_export.html b/browser/devtools/commandline/test/browser_cmd_pagemod_export.html
new file mode 100644
index 000000000..a7d28828c
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/commandline/test/browser_cmd_pagemod_export.js b/browser/devtools/commandline/test/browser_cmd_pagemod_export.js
new file mode 100644
index 000000000..374479abd
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_pagemod_export.js
@@ -0,0 +1,376 @@
+/* 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/browser/devtools/commandline/"+
+ "test/browser_cmd_pagemod_export.html";
+
+function test() {
+ let initialHtml = "";
+
+ var tests = {};
+
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ initialHtml = content.document.documentElement.innerHTML;
+
+ return helpers.runTests(options, tests);
+ }).then(finish);
+
+ function getContent(options) {
+ return options.document.documentElement.innerHTML;
+ }
+
+ function resetContent(options) {
+ options.document.documentElement.innerHTML = initialHtml;
+ }
+
+ tests.testExportHtml = function(options) {
+ let oldOpen = options.window.open;
+ let openURL = "";
+ options.window.open = function(url) {
+ // The URL is a data: URL that contains the document source
+ openURL = decodeURIComponent(url);
+ };
+
+ return helpers.audit(options, [
+ {
+ setup: 'export html',
+ check: {
+ input: 'export html',
+ hints: '',
+ markup: 'VVVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ''
+ },
+ post: function() {
+ 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>");
+
+ options.window.open = oldOpen;
+ }
+ }
+ ]);
+ };
+
+ tests.testPageModReplace = function(options) {
+ return 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: function() {
+ is(getContent(options), initialHtml, "no change in the page");
+ }
+ },
+ {
+ setup: 'pagemod replace sOme foOBar true',
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 2\. [^:]+: 2\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ isnot(html.indexOf('<p class="foOBarclass">.foOBarclass'), -1,
+ ".someclass changed to .foOBarclass");
+ isnot(html.indexOf('<p id="foOBarid">#foOBarid'), -1,
+ "#someid changed to #foOBarid");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod replace some foobar --contentOnly',
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 2\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ 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)");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod replace some foobar --attrOnly',
+ exec: {
+ output: /^[^:]+: 13\. [^:]+: 0\. [^:]+: 2\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ 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)");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod replace some foobar --root head',
+ exec: {
+ output: /^[^:]+: 2\. [^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed");
+ }
+ },
+ {
+ setup: 'pagemod replace some foobar --selector .someclass,div,span',
+ exec: {
+ output: /^[^:]+: 4\. [^:]+: 1\. [^:]+: 1\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ isnot(html.indexOf('<p class="foobarclass">.foobarclass'), -1,
+ ".someclass changed to .foobarclass");
+ isnot(html.indexOf('<p id="someid">#someid'), -1,
+ "#someid did not change");
+
+ resetContent(options);
+ }
+ },
+ ]);
+ };
+
+ tests.testPageModRemoveElement = function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pagemod remove',
+ check: {
+ input: 'pagemod remove',
+ hints: '',
+ 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: function() {
+ let html = getContent(options);
+
+ 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");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod remove element p head',
+ exec: {
+ output: /^[^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed in the page");
+ }
+ },
+ {
+ setup: 'pagemod remove element p --ifEmptyOnly',
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed in the page");
+ }
+ },
+ {
+ setup: 'pagemod remove element meta,title --ifEmptyOnly',
+ exec: {
+ output: /^[^:]+: 2\. [^:]+: 1\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ is(html.indexOf("<meta charset="), -1, "<meta> removed");
+ isnot(html.indexOf("<title>"), -1, "<title> not removed");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod remove element p --stripOnly',
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 3\.\s*$/
+ },
+ post: function() {
+ let html = getContent(options);
+
+ 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");
+
+ resetContent(options);
+ }
+ },
+ ]);
+ };
+
+ tests.testPageModRemoveAttribute = function(options) {
+ return 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 },
+ 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 },
+ ignoreCase: { value: false },
+ }
+ },
+ post: function() {
+ let deferred = Promise.defer();
+ executeSoon(function() {
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+ },
+ {
+ setup: 'pagemod remove attribute foo bar',
+ exec: {
+ output: /^[^:]+: 0\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed in the page");
+ }
+ },
+ {
+ setup: 'pagemod remove attribute foo p',
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed in the page");
+ }
+ },
+ {
+ setup: 'pagemod remove attribute id p,span',
+ exec: {
+ output: /^[^:]+: 5\. [^:]+: 1\.\s*$/
+ },
+ post: function() {
+ is(getContent(options).indexOf('<p id="someid">#someid'), -1,
+ "p#someid attribute removed");
+ isnot(getContent(options).indexOf("<p>#someid"), -1,
+ "p with someid content still exists");
+
+ resetContent(options);
+ }
+ },
+ {
+ setup: 'pagemod remove attribute Class p',
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 0\.\s*$/
+ },
+ post: function() {
+ is(getContent(options), initialHtml, "nothing changed in the page");
+ }
+ },
+ {
+ setup: 'pagemod remove attribute Class p --ignoreCase',
+ exec: {
+ output: /^[^:]+: 3\. [^:]+: 1\.\s*$/
+ },
+ post: function() {
+ is(getContent(options).indexOf('<p class="someclass">.someclass'), -1,
+ "p.someclass attribute removed");
+ isnot(getContent(options).indexOf("<p>.someclass"), -1,
+ "p with someclass content still exists");
+
+ resetContent(options);
+ }
+ },
+ ]);
+ };
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_pref.js b/browser/devtools/commandline/test/browser_cmd_pref.js
new file mode 100644
index 000000000..9aa6237fa
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_pref.js
@@ -0,0 +1,502 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+let prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+let supportsString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString)
+
+let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
+
+let settings = require("gcli/settings");
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-pref";
+
+let tiltEnabledOrig;
+let tabSizeOrig;
+let remoteHostOrig;
+
+let tests = {
+ setup: function(options) {
+ tiltEnabledOrig = prefBranch.getBoolPref("devtools.tilt.enabled");
+ tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
+ remoteHostOrig = prefBranch.getComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString).data;
+
+ info("originally: devtools.tilt.enabled = " + tiltEnabledOrig);
+ info("originally: devtools.editor.tabsize = " + tabSizeOrig);
+ info("originally: devtools.debugger.remote-host = " + remoteHostOrig);
+ },
+
+ shutdown: function(options) {
+ prefBranch.setBoolPref("devtools.tilt.enabled", tiltEnabledOrig);
+ prefBranch.setIntPref("devtools.editor.tabsize", tabSizeOrig);
+ supportsString.data = remoteHostOrig;
+ prefBranch.setComplexValue("devtools.debugger.remote-host",
+ Ci.nsISupportsString, supportsString);
+ },
+
+ testPrefStatus: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref',
+ check: {
+ input: 'pref',
+ hints: '',
+ 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.til',
+ check: {
+ input: 'pref show devtools.til',
+ hints: 't.enabled',
+ markup: 'VVVVVVVVVVIIIIIIIIIIII',
+ status: 'ERROR',
+ tooltipState: 'true:importantFieldFlag',
+ args: {
+ setting: { value: undefined, status: 'INCOMPLETE' },
+ }
+ },
+ },
+ {
+ setup: 'pref reset devtools.tilt.enabled',
+ check: {
+ input: 'pref reset devtools.tilt.enabled',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID'
+ },
+ },
+ {
+ setup: 'pref show devtools.tilt.enabled 4',
+ check: {
+ input: 'pref show devtools.tilt.enabled 4',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE',
+ status: 'ERROR'
+ },
+ },
+ {
+ setup: 'pref set devtools.tilt.enabled 4',
+ check: {
+ input: 'pref set devtools.tilt.enabled 4',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE',
+ status: 'ERROR',
+ args: {
+ setting: { arg: ' devtools.tilt.enabled' },
+ value: { status: 'ERROR', message: 'Can\'t use \'4\'.' },
+ }
+ },
+ },
+ {
+ 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'
+ },
+ },
+ ]);
+ },
+
+ testPrefSetEnable: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref set devtools.editor.tabsize 9',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 9 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: [ /void your warranty/, /I promise/ ],
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"),
+ tabSizeOrig,
+ "devtools.editor.tabsize is unchanged");
+ }
+ },
+ {
+ setup: 'pref set devtools.gcli.allowSet true',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.gcli.allowSet") },
+ value: { value: true }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ post: function() {
+ is(prefBranch.getBoolPref("devtools.gcli.allowSet"), true,
+ "devtools.gcli.allowSet is true");
+ }
+ },
+ {
+ setup: 'pref set devtools.editor.tabsize 10',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 10 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 10,
+ "devtools.editor.tabsize is 10");
+ }
+ },
+ ]);
+ },
+
+ testPrefBoolExec: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref show devtools.tilt.enabled',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.tilt.enabled") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.tilt\.enabled: " + tiltEnabledOrig + "$"),
+ },
+ },
+ {
+ setup: 'pref set devtools.tilt.enabled true',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.tilt.enabled") },
+ value: { value: true }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ post: function() {
+ is(prefBranch.getBoolPref("devtools.tilt.enabled"), true,
+ "devtools.tilt.enabled is true");
+ }
+ },
+ {
+ setup: 'pref show devtools.tilt.enabled',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.tilt.enabled") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.tilt\.enabled: true$"),
+ },
+ },
+ {
+ setup: 'pref set devtools.tilt.enabled false',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.tilt.enabled") },
+ value: { value: false }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ },
+ {
+ setup: 'pref show devtools.tilt.enabled',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.tilt.enabled") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.tilt\.enabled: false$"),
+ },
+ post: function() {
+ is(prefBranch.getBoolPref("devtools.tilt.enabled"), false,
+ "devtools.tilt.enabled is false");
+ }
+ },
+ ]);
+ },
+
+ testPrefNumberExec: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref show devtools.editor.tabsize',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.editor\.tabsize: 10$"),
+ },
+ },
+ {
+ setup: 'pref set devtools.editor.tabsize 20',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 20 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ },
+ {
+ setup: 'pref show devtools.editor.tabsize',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.editor\.tabsize: 20$"),
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 20,
+ "devtools.editor.tabsize is 20");
+ }
+ },
+ {
+ setup: 'pref set devtools.editor.tabsize 1',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 1 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ },
+ {
+ setup: 'pref show devtools.editor.tabsize',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.editor\.tabsize: 1$"),
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 1,
+ "devtools.editor.tabsize is 1");
+ }
+ },
+ ]);
+ },
+
+ testPrefStringExec: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref show devtools.debugger.remote-host',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.debugger.remote-host") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: new RegExp("^devtools\.debugger\.remote-host: " + remoteHostOrig + "$"),
+ },
+ },
+ {
+ setup: 'pref set devtools.debugger.remote-host e.com',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.debugger.remote-host") },
+ value: { value: "e.com" }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ },
+ {
+ setup: 'pref show devtools.debugger.remote-host',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.debugger.remote-host") }
+ },
+ },
+ exec: {
+ completed: true,
+ 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: settings.getSetting("devtools.debugger.remote-host") },
+ value: { value: "moz.foo" }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ },
+ {
+ setup: 'pref show devtools.debugger.remote-host',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.debugger.remote-host") }
+ },
+ },
+ exec: {
+ completed: true,
+ 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");
+ }
+ },
+ ]);
+ },
+
+ testPrefSetDisable: function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'pref set devtools.editor.tabsize 32',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 32 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 32,
+ "devtools.editor.tabsize is 32");
+ }
+ },
+ {
+ setup: 'pref reset devtools.gcli.allowSet',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.gcli.allowSet") }
+ },
+ },
+ exec: {
+ completed: true,
+ output: '',
+ },
+ post: function() {
+ is(prefBranch.getBoolPref("devtools.gcli.allowSet"), false,
+ "devtools.gcli.allowSet is false");
+ }
+ },
+ {
+ setup: 'pref set devtools.editor.tabsize 33',
+ check: {
+ args: {
+ setting: { value: settings.getSetting("devtools.editor.tabsize") },
+ value: { value: 33 }
+ },
+ },
+ exec: {
+ completed: true,
+ output: [ /void your warranty/, /I promise/ ],
+ },
+ post: function() {
+ is(prefBranch.getIntPref("devtools.editor.tabsize"), 32,
+ "devtools.editor.tabsize is still 32");
+ }
+ },
+ ]);
+ },
+};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_restart.js b/browser/devtools/commandline/test/browser_cmd_restart.js
new file mode 100644
index 000000000..a08ae3e37
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_restart.js
@@ -0,0 +1,35 @@
+/* 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 },
+ }
+ },
+ },
+ {
+ setup: 'restart --nocache',
+ check: {
+ input: 'restart --nocache',
+ markup: 'VVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ nocache: { value: true },
+ }
+ },
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_screenshot.html b/browser/devtools/commandline/test/browser_cmd_screenshot.html
new file mode 100644
index 000000000..8e30016f1
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_screenshot.html
@@ -0,0 +1,6 @@
+<html>
+ <head></head>
+ <body>
+ <img id="testImage" ></img>
+ </body>
+</html>
diff --git a/browser/devtools/commandline/test/browser_cmd_screenshot.js b/browser/devtools/commandline/test/browser_cmd_screenshot.js
new file mode 100644
index 000000000..a44aa0731
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_screenshot.js
@@ -0,0 +1,195 @@
+/* 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/browser/devtools/commandline/" +
+ "test/browser_cmd_screenshot.html";
+
+let FileUtils = (Cu.import("resource://gre/modules/FileUtils.jsm", {})).FileUtils;
+
+let tests = {
+ testInput: function(options) {
+ return 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',
+ args: {
+ selector: {
+ value: options.window.document.getElementById("testImage")
+ },
+ }
+ },
+ },
+ ]);
+ },
+
+ testCaptureFile: function(options) {
+ let file = FileUtils.getFile("TmpD", [ "TestScreenshotFile.png" ]);
+
+ return helpers.audit(options, [
+ {
+ setup: 'screenshot ' + file.path,
+ check: {
+ args: {
+ filename: { value: "" + file.path },
+ fullpage: { value: false },
+ clipboard: { value: false },
+ chrome: { 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);
+ }
+ }
+ },
+ ]);
+ },
+
+ testCaptureClipboard: function(options) {
+ 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);
+ trans.init(null);
+ trans.addDataFlavor("image/png");
+
+ return helpers.audit(options, [
+ {
+ setup: 'screenshot --fullpage --clipboard',
+ check: {
+ args: {
+ fullpage: { value: true },
+ clipboard: { value: true },
+ chrome: { value: false },
+ },
+ },
+ exec: {
+ output: new RegExp("^Copied to clipboard.$"),
+ },
+ post: function() {
+ try {
+ clip.getData(trans, clipid.kGlobalClipboard);
+ let str = new Object();
+ let strLength = new Object();
+ trans.getTransferData("image/png", str, strLength);
+
+ ok(str.value, "screenshot exists");
+ ok(strLength.value > 0, "screenshot has length");
+ }
+ finally {
+ Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session", true);
+
+ // Recent PB changes to the test I'm modifying removed the 'pb'
+ // variable, but left this line in tact. This seems so obviously
+ // wrong that I'm leaving this in in case the analysis is wrong
+ // pb.privateBrowsingEnabled = true;
+ }
+ }
+ },
+ ]);
+ },
+};
+
+function test() {
+ info("RUN TEST: non-private window");
+ let nonPrivDone = addWindow({ private: false }, addTabWithToolbarRunTests);
+
+ let privDone = nonPrivDone.then(function() {
+ info("RUN TEST: private window");
+ return addWindow({ private: true }, addTabWithToolbarRunTests);
+ });
+
+ privDone.then(finish, function(error) {
+ ok(false, 'Promise fail: ' + error);
+ });
+}
+
+function addTabWithToolbarRunTests(win) {
+ return helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }, { chromeWindow: win });
+}
+
+function addWindow(windowOptions, callback) {
+ waitForExplicitFinish();
+ let deferred = Promise.defer();
+
+ let win = OpenBrowserWindow(windowOptions);
+
+ let onLoad = function() {
+ win.removeEventListener("load", onLoad, false);
+
+ // Would like to get rid of this executeSoon, but without it the url
+ // (TEST_URI) provided in addTabWithToolbarRunTests hasn't loaded
+ executeSoon(function() {
+ try {
+ let reply = callback(win);
+ Promise.resolve(reply).then(function() {
+ win.close();
+ deferred.resolve();
+ });
+ }
+ catch (ex) {
+ deferred.reject(ex);
+ }
+ });
+ };
+
+ win.addEventListener("load", onLoad, false);
+
+ return deferred.promise;
+}
diff --git a/browser/devtools/commandline/test/browser_cmd_settings.js b/browser/devtools/commandline/test/browser_cmd_settings.js
new file mode 100644
index 000000000..165102b8d
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_settings.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the pref commands work
+
+let prefBranch = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+
+let supportsString = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString)
+
+let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
+
+let settings = require("gcli/settings");
+
+const TEST_URI = "data:text/html;charset=utf-8,gcli-settings";
+
+let tiltEnabled = undefined;
+let tabSize = undefined;
+let remoteHost = undefined;
+
+let tiltEnabledOrig = undefined;
+let tabSizeOrig = undefined;
+let remoteHostOrig = undefined;
+
+let tests = {};
+
+tests.setup = function() {
+ tiltEnabled = settings.getSetting("devtools.tilt.enabled");
+ tabSize = settings.getSetting("devtools.editor.tabsize");
+ remoteHost = settings.getSetting("devtools.debugger.remote-host");
+
+ tiltEnabledOrig = prefBranch.getBoolPref("devtools.tilt.enabled");
+ tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
+ remoteHostOrig = prefBranch.getComplexValue(
+ "devtools.debugger.remote-host",
+ Components.interfaces.nsISupportsString).data;
+
+ info("originally: devtools.tilt.enabled = " + tiltEnabledOrig);
+ info("originally: devtools.editor.tabsize = " + tabSizeOrig);
+ info("originally: devtools.debugger.remote-host = " + remoteHostOrig);
+};
+
+tests.shutdown = function() {
+ prefBranch.setBoolPref("devtools.tilt.enabled", tiltEnabledOrig);
+ prefBranch.setIntPref("devtools.editor.tabsize", tabSizeOrig);
+ supportsString.data = remoteHostOrig;
+ prefBranch.setComplexValue("devtools.debugger.remote-host",
+ Components.interfaces.nsISupportsString,
+ supportsString);
+
+ tiltEnabled = undefined;
+ tabSize = undefined;
+ remoteHost = undefined;
+
+ tiltEnabledOrig = undefined;
+ tabSizeOrig = undefined;
+ remoteHostOrig = undefined;
+};
+
+tests.testSettings = function() {
+ is(tiltEnabled.value, tiltEnabledOrig, "tiltEnabled default");
+ is(tabSize.value, tabSizeOrig, "tabSize default");
+ is(remoteHost.value, remoteHostOrig, "remoteHost default");
+
+ tiltEnabled.setDefault();
+ tabSize.setDefault();
+ remoteHost.setDefault();
+
+ let tiltEnabledDefault = tiltEnabled.value;
+ let tabSizeDefault = tabSize.value;
+ let remoteHostDefault = remoteHost.value;
+
+ tiltEnabled.value = false;
+ tabSize.value = 42;
+ remoteHost.value = "example.com"
+
+ is(tiltEnabled.value, false, "tiltEnabled basic");
+ is(tabSize.value, 42, "tabSize basic");
+ is(remoteHost.value, "example.com", "remoteHost basic");
+
+ function tiltEnabledCheck(ev) {
+ is(ev.setting, tiltEnabled, "tiltEnabled event setting");
+ is(ev.value, true, "tiltEnabled event value");
+ is(ev.setting.value, true, "tiltEnabled event setting value");
+ }
+ tiltEnabled.onChange.add(tiltEnabledCheck);
+ tiltEnabled.value = true;
+ is(tiltEnabled.value, true, "tiltEnabled 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");
+
+ tiltEnabled.onChange.remove(tiltEnabledCheck);
+ 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);
+
+ tiltEnabled.setDefault();
+ tabSize.setDefault();
+ remoteHost.setDefault();
+
+ remoteHost.onChange.remove(remoteHostReCheck);
+
+ is(tiltEnabled.value, tiltEnabledDefault, "tiltEnabled reset");
+ is(tabSize.value, tabSizeDefault, "tabSize reset");
+ is(remoteHost.value, remoteHostDefault, "remoteHost reset");
+};
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, tests);
+ }).then(finish);
+}
diff --git a/browser/devtools/commandline/test/browser_gcli_async.js b/browser/devtools/commandline/test/browser_gcli_async.js
new file mode 100644
index 000000000..172f0bf00
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_async.js
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testAsync.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+var canon = require('gcli/canon');
+var Promise = require('util/promise');
+
+exports.testBasic = function(options) {
+ var getData = function() {
+ var deferred = Promise.defer();
+
+ var resolve = function() {
+ deferred.resolve([
+ 'Shalom', 'Namasté', 'Hallo', 'Dydd-da',
+ 'Chào', 'Hej', 'Saluton', 'Sawubona'
+ ]);
+ };
+
+ setTimeout(resolve, 10);
+ return deferred.promise;
+ };
+
+ var tsslow = {
+ name: 'tsslow',
+ params: [
+ {
+ name: 'hello',
+ type: {
+ name: 'selection',
+ data: getData
+ }
+ }
+ ],
+ exec: function(args, context) {
+ return 'Test completed';
+ }
+ };
+
+ canon.addCommand(tsslow);
+
+ 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: [ ],
+ tooltipState: 'true:importantFieldFlag',
+ args: {
+ command: { name: 'tsslow' },
+ hello: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ },
+ }
+ }
+ },
+ {
+ setup: 'tsslow S',
+ check: {
+ input: 'tsslow S',
+ hints: 'halom',
+ markup: 'VVVVVVVI',
+ cursor: 8,
+ current: 'hello',
+ status: 'ERROR',
+ predictions: [ 'Shalom', 'Saluton', 'Sawubona', 'Namasté' ],
+ unassigned: [ ],
+ tooltipState: 'true:importantFieldFlag',
+ args: {
+ command: { name: 'tsslow' },
+ hello: {
+ value: undefined,
+ arg: ' S',
+ status: 'INCOMPLETE',
+ message: ''
+ },
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ setup: 'tsslow S<TAB>',
+ check: {
+ input: 'tsslow Shalom ',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVV',
+ cursor: 14,
+ current: 'hello',
+ status: 'VALID',
+ predictions: [ 'Shalom' ],
+ unassigned: [ ],
+ tooltipState: 'true:importantFieldFlag',
+ args: {
+ command: { name: 'tsslow' },
+ hello: {
+ value: 'Shalom',
+ arg: ' Shalom ',
+ status: 'VALID',
+ message: ''
+ },
+ }
+ },
+ post: function() {
+ canon.removeCommand(tsslow);
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ setup: 'tsslow ',
+ check: {
+ input: 'tsslow ',
+ markup: 'EEEEEEV',
+ cursor: 7,
+ status: 'ERROR'
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_canon.js b/browser/devtools/commandline/test/browser_gcli_canon.js
new file mode 100644
index 000000000..296cf1907
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_canon.js
@@ -0,0 +1,271 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCanon.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+var canon = require('gcli/canon');
+// var assert = require('test/assert');
+var Canon = canon.Canon;
+
+var startCount = undefined;
+var events = undefined;
+
+var canonChange = function(ev) {
+ events++;
+};
+
+exports.setup = function(options) {
+ startCount = canon.getCommands().length;
+ events = 0;
+};
+
+exports.shutdown = function(options) {
+ startCount = undefined;
+ events = undefined;
+};
+
+exports.testAddRemove1 = function(options) {
+ return helpers.audit(options, [
+ {
+ name: 'testadd add',
+ setup: function() {
+ canon.onCanonChange.add(canonChange);
+
+ canon.addCommand({
+ name: 'testadd',
+ exec: function() {
+ return 1;
+ }
+ });
+
+ assert.is(canon.getCommands().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() {
+ canon.addCommand({
+ name: 'testadd',
+ exec: function() {
+ return 2;
+ }
+ });
+
+ assert.is(canon.getCommands().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() {
+ canon.removeCommand('testadd');
+
+ assert.is(canon.getCommands().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) {
+ canon.addCommand({
+ name: 'testadd',
+ exec: function() {
+ return 3;
+ }
+ });
+
+ assert.is(canon.getCommands().length,
+ startCount + 1,
+ 'rereadd command success');
+ assert.is(events, 4, 'rereadd event');
+
+ return helpers.audit(options, [
+ {
+ setup: 'testadd',
+ exec: {
+ output: /^3$/
+ },
+ post: function() {
+ canon.removeCommand({
+ name: 'testadd'
+ });
+
+ assert.is(canon.getCommands().length,
+ startCount,
+ 'reremove command success');
+ assert.is(events, 5, 'reremove event');
+ }
+ },
+ {
+ setup: 'testadd',
+ check: {
+ typed: 'testadd',
+ status: 'ERROR'
+ }
+ }
+ ]);
+};
+
+exports.testAddRemove3 = function(options) {
+ canon.removeCommand({ name: 'nonexistant' });
+ assert.is(canon.getCommands().length,
+ startCount,
+ 'nonexistant1 command success');
+ assert.is(events, 5, 'nonexistant1 event');
+
+ canon.removeCommand('nonexistant');
+ assert.is(canon.getCommands().length,
+ startCount,
+ 'nonexistant2 command success');
+ assert.is(events, 5, 'nonexistant2 event');
+
+ canon.onCanonChange.remove(canonChange);
+};
+
+exports.testAltCanon = function(options) {
+ var altCanon = new Canon();
+
+ var tss = {
+ name: 'tss',
+ params: [
+ { name: 'str', type: 'string' },
+ { name: 'num', type: 'number' },
+ { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } },
+ ],
+ exec: function(args, context) {
+ return context.commandName + ':' +
+ args.str + ':' + args.num + ':' + args.opt;
+ }
+ };
+ altCanon.addCommand(tss);
+
+ var commandSpecs = altCanon.getCommandSpecs();
+ assert.is(JSON.stringify(commandSpecs),
+ '{"tss":{"name":"tss","params":[' +
+ '{"name":"str","type":"string"},' +
+ '{"name":"num","type":"number"},' +
+ '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}]}}',
+ 'JSON.stringify(commandSpecs)');
+
+ var remoter = function(args, context) {
+ assert.is(context.commandName, 'tss', 'commandName is tss');
+
+ var cmd = altCanon.getCommand(context.commandName);
+ return cmd.exec(args, context);
+ };
+
+ canon.addProxyCommands('proxy', commandSpecs, remoter, 'test');
+
+ var parent = canon.getCommand('proxy');
+ assert.is(parent.name, 'proxy', 'Parent command called proxy');
+
+ var child = canon.getCommand('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() {
+ canon.removeCommand('proxy');
+ canon.removeCommand('proxy tss');
+
+ assert.is(canon.getCommand('proxy'), undefined, 'remove proxy');
+ assert.is(canon.getCommand('proxy tss'), undefined, 'remove proxy tss');
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_cli.js b/browser/devtools/commandline/test/browser_gcli_cli.js
new file mode 100644
index 000000000..4001e3332
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_cli.js
@@ -0,0 +1,1322 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCli.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+// var assert = require('test/assert');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testBlank = function(options) {
+ var requisition = options.display.requisition;
+
+ return helpers.audit(options, [
+ {
+ setup: '',
+ check: {
+ input: '',
+ hints: '',
+ markup: '',
+ cursor: 0,
+ current: '__command',
+ status: 'ERROR'
+ },
+ post: function() {
+ assert.is(requisition.commandAssignment.value, undefined);
+ }
+ },
+ {
+ setup: ' ',
+ check: {
+ input: ' ',
+ hints: '',
+ markup: 'V',
+ cursor: 1,
+ current: '__command',
+ status: 'ERROR'
+ },
+ post: function() {
+ assert.is(requisition.commandAssignment.value, undefined);
+ }
+ },
+ {
+ name: '| ',
+ setup: function() {
+ helpers.setInput(options, ' ', 0);
+ },
+ check: {
+ input: ' ',
+ hints: '',
+ markup: 'V',
+ cursor: 0,
+ current: '__command',
+ status: 'ERROR'
+ },
+ post: function() {
+ assert.is(requisition.commandAssignment.value, undefined);
+ }
+ }
+ ]);
+};
+
+exports.testIncompleteMultiMatch = function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 't',
+ skipIf: options.isFirefox, // 't' hints at 'tilt' in firefox
+ check: {
+ input: 't',
+ hints: 'est',
+ markup: 'I',
+ cursor: 1,
+ current: '__command',
+ status: 'ERROR',
+ predictionsContains: [ 'tsb' ]
+ }
+ },
+ {
+ 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', message: '' },
+ optionValue: { arg: '', status: 'INCOMPLETE', message: '' }
+ }
+ }
+ },
+ {
+ 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', message: '' },
+ optionValue: { arg: '', status: 'INCOMPLETE', message: '' }
+ }
+ }
+ },
+ {
+ name: 'ts|v',
+ setup: function() {
+ 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', message: '' },
+ optionValue: { arg: '', status: 'INCOMPLETE', message: '' }
+ }
+ }
+ },
+ {
+ 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: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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\'t use \'option\'.'
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: mockCommands.option1,
+ arg: ' option1',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: mockCommands.option1,
+ arg: ' option1 ',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: mockCommands.option2,
+ arg: ' option2',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: mockCommands.option1,
+ arg: ' option1',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: '6',
+ 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: mockCommands.option2,
+ arg: ' option2',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: 6,
+ arg: ' 6',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ }
+ }
+ ]);
+};
+
+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'
+ }
+ }
+ ]);
+};
+
+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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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\'t convert "x" 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\'t convert "1.5" 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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\'t convert "x" to a number.',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'tsf' },
+ num: {
+ value: undefined,
+ arg: ' x',
+ status: 'ERROR',
+ message: 'Can\'t convert "x" to a number.'
+ }
+ }
+ }
+ }
+ ]);
+};
+
+exports.testElementDom = function(options) {
+ return helpers.audit(options, [
+ {
+ skipIf: options.isJsdom,
+ setup: 'tse :root',
+ check: {
+ input: 'tse :root',
+ hints: ' [options]',
+ markup: 'VVVVVVVVV',
+ cursor: 9,
+ current: 'node',
+ status: 'VALID',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'tse' },
+ node: {
+ value: options.window.document.documentElement,
+ arg: ' :root',
+ status: 'VALID',
+ message: ''
+ },
+ nodes: { arg: '', status: 'VALID', message: '' },
+ nodes2: { arg: '', status: 'VALID', message: '' },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testElementWeb = function(options) {
+ var inputElement = options.window.document.getElementById('gcli-input');
+
+ return helpers.audit(options, [
+ {
+ skipIf: function gcliInputElementExists() {
+ return inputElement == null || options.isJsdom;
+ },
+ setup: 'tse #gcli-input',
+ check: {
+ input: 'tse #gcli-input',
+ hints: ' [options]',
+ markup: 'VVVVVVVVVVVVVVV',
+ cursor: 15,
+ current: 'node',
+ status: 'VALID',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'tse' },
+ node: {
+ value: inputElement,
+ arg: ' #gcli-input',
+ 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: { value: undefined, arg: '', status: 'INCOMPLETE', message: '' },
+ nodes: { arg: '', status: 'VALID', message: '' },
+ nodes2: { arg: '', status: 'VALID', message: '' },
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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: '' },
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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: '',
+ 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: '',
+ markup: 'IIIV',
+ cursor: 4,
+ current: '__command',
+ status: 'ERROR',
+ unassigned: [ ]
+ }
+ },
+ {
+ skipIf: options.isPhantomjs,
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ 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: '',
+ markup: 'IIIVIIIIVIIIIVIIIIII',
+ cursor: 20,
+ current: '__command',
+ status: 'ERROR',
+ predictions: [ 'tsn deep down nested', '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/browser/devtools/commandline/test/browser_gcli_completion.js b/browser/devtools/commandline/test/browser_gcli_completion.js
new file mode 100644
index 000000000..224babe0e
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_completion.js
@@ -0,0 +1,537 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCompletion.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+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,
+ 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() {
+ helpers.setInput(options, 'tselar 1', 7);
+ },
+ check: {
+ hints: ''
+ }
+ },
+ {
+ name: 'tselar| 1',
+ setup: function() {
+ helpers.setInput(options, 'tselar 1', 6);
+ },
+ check: {
+ hints: ' -> tselarr'
+ }
+ },
+ {
+ name: 'tsela|r 1',
+ setup: function() {
+ helpers.setInput(options, 'tselar 1', 5);
+ },
+ check: {
+ hints: ' -> tselarr'
+ }
+ },
+ ]);
+};
+
+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'
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ setup: 'tslong --num 42 --se<TAB>',
+ check: {
+ input: 'tslong --num 42 --sel ',
+ hints: 'space <msg> [options]',
+ markup: 'VVVVVVVVVVVVVVVVIIIIIV'
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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', message: '' },
+ 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' }
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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', message: '' },
+ 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/browser/devtools/commandline/test/browser_gcli_context.js b/browser/devtools/commandline/test/browser_gcli_context.js
new file mode 100644
index 000000000..e3c55ec4b
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_context.js
@@ -0,0 +1,276 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testContext.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+ mockCommands.setup();
+
+ origLogErrors = cli.logErrors;
+ cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+
+ cli.logErrors = origLogErrors;
+ origLogErrors = undefined;
+};
+
+exports.testBaseline = function(options) {
+ 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: '',
+ markup: 'III',
+ cursor: 3,
+ current: '__command',
+ status: 'ERROR',
+ predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+ args: {
+ command: { name: 'tsn' },
+ }
+ }
+ }
+ ]);
+};
+
+exports.testContext = function(options) {
+ helpers.audit(options, [
+ // Use the 'tsn' context
+ {
+ setup: 'context tsn',
+ check: {
+ input: 'context tsn',
+ hints: '',
+ markup: 'VVVVVVVVVVV',
+ message: '',
+ predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+ args: {
+ command: { name: 'context' },
+ prefix: {
+ value: mockCommands.commands.tsn,
+ status: 'VALID',
+ message: ''
+ },
+ }
+ },
+ exec: {
+ output: 'Using tsn as a command prefix',
+ completed: true,
+ }
+ },
+ // 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',
+ message: ''
+ },
+ }
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ },
+ {
+ setup: 'tsn',
+ check: {
+ input: 'tsn',
+ hints: '',
+ markup: 'III',
+ message: '',
+ predictionsContains: [ 'tsn', 'tsn deep', '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: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'context' },
+ prefix: {
+ value: mockCommands.commands.tsnExt,
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: 'Error: Can\'t use \'tsn ext\' as a prefix because it is not a parent command.',
+ completed: true,
+ 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: mockCommands.commands.tsnDeep,
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: '',
+ completed: true,
+ }
+ },
+ */
+ {
+ 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',
+ completed: true,
+ type: 'string',
+ error: false
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_date.js b/browser/devtools/commandline/test/browser_gcli_date.js
new file mode 100644
index 000000000..43359c202
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_date.js
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testDate.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+
+var types = require('gcli/types');
+var Argument = require('gcli/argument').Argument;
+var Status = require('gcli/types').Status;
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+
+exports.testParse = function(options) {
+ var date = types.createType('date');
+ return date.parse(new Argument('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 date = types.createType({ name: 'date', max: max, min: min });
+ assert.is(date.getMax(), max, 'max setup');
+
+ var incremented = date.increment(min);
+ assert.is(incremented, max, 'incremented');
+};
+
+exports.testIncrement = function(options) {
+ var date = types.createType('date');
+ return date.parse(new Argument('now')).then(function(conversion) {
+ var plusOne = date.increment(conversion.value);
+ var minusOne = date.decrement(plusOne);
+
+ // 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) {
+ 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, 'd1 year');
+ assert.is(d2.getMonth(), 0, 'd1 month');
+ assert.is(d2.getDate(), 3, 'd1 date');
+ assert.is(d2.getHours(), 0, 'd1 hours');
+ assert.is(d2.getMinutes(), 0, 'd1 minutes');
+ assert.is(d2.getSeconds(), 0, 'd1 seconds');
+ assert.is(d2.getMilliseconds(), 0, 'd1 millis');
+ },
+ arg: ' 1980-01-03',
+ status: 'VALID',
+ message: ''
+ },
+ }
+ },
+ exec: {
+ output: [ /^Exec: tsdate/, /2001/, /1980/ ],
+ completed: true,
+ type: 'string',
+ error: false
+ }
+ }
+ ]);
+};
+
+exports.testIncrDecr = function(options) {
+ helpers.audit(options, [
+ {
+ 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',
+ message: ''
+ },
+ }
+ }
+ },
+ {
+ // 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',
+ message: ''
+ },
+ }
+ }
+ },
+ {
+ // 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(d1) {
+ assert.is(d1.getFullYear(), 2000, 'd1 year');
+ assert.is(d1.getMonth(), 1, 'd1 month');
+ assert.is(d1.getDate(), 28, '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: ' "2000-02-28"',
+ status: 'VALID',
+ message: ''
+ },
+ }
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_exec.js b/browser/devtools/commandline/test/browser_gcli_exec.js
new file mode 100644
index 000000000..719442a0b
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_exec.js
@@ -0,0 +1,657 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testExec.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+var nodetype = require('gcli/types/node');
+var canon = require('gcli/canon');
+// var assert = require('test/assert');
+// var mockCommands = require('gclitest/mockCommands');
+// var helpers = require('gclitest/helpers');
+
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+var mockBody = {
+ style: {}
+};
+
+var mockDoc = {
+ querySelectorAll: function(css) {
+ if (css === ':root') {
+ return {
+ length: 1,
+ item: function(i) {
+ return mockBody;
+ }
+ };
+ }
+ else {
+ return {
+ length: 0,
+ item: function() { return null; }
+ };
+ }
+ }
+};
+
+exports.testParamGroup = function(options) {
+ var tsg = canon.getCommand('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, [
+ {
+ skipIf: options.isJsdom,
+ setup: 'tss',
+ check: {
+ input: 'tss',
+ hints: '',
+ markup: 'VVV',
+ cursor: 3,
+ current: '__command',
+ status: 'VALID',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'tss' },
+ }
+ },
+ exec: {
+ output: 'Exec: tss',
+ completed: true,
+ }
+ },
+ {
+ 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: mockCommands.option1,
+ arg: ' option1',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: '10',
+ arg: ' 10',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: 'Exec: tsv optionType=[object Object], optionValue=10',
+ completed: true
+ }
+ },
+ {
+ 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: mockCommands.option2,
+ arg: ' option2',
+ status: 'VALID',
+ message: ''
+ },
+ optionValue: {
+ value: 10,
+ arg: ' 10',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: 'Exec: tsv optionType=[object Object], optionValue=10',
+ completed: true
+ }
+ }
+ ]);
+};
+
+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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: 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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+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: {
+ value: '1 + 1',
+ arg: ' { 1 + 1 }',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: 'Exec: tsj javascript=1 + 1',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+exports.testExecNode = function(options) {
+ var origDoc = nodetype.getDocument();
+ nodetype.setDocument(mockDoc);
+
+ return helpers.audit(options, [
+ {
+ setup: 'tse :root',
+ check: {
+ input: 'tse :root',
+ hints: ' [options]',
+ markup: 'VVVVVVVVV',
+ cursor: 9,
+ current: 'node',
+ status: 'VALID',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'tse' },
+ node: { value: mockBody, arg: ' :root', status: 'VALID', message: '' },
+ nodes: { arg: '', status: 'VALID', message: '' },
+ nodes2: { arg: '', status: 'VALID', message: '' },
+ }
+ },
+ exec: {
+ output: 'Exec: tse node=[object Object], nodes=[object Object], nodes2=[object Object]',
+ completed: true,
+ },
+ post: function() {
+ nodetype.setDocument(origDoc);
+ }
+ }
+ ]);
+};
+
+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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+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=',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ },
+ {
+ 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',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+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',
+ completed: true,
+ }
+ }
+ ]);
+};
+
+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=null, bool=false, txt2=d, num=42',
+ completed: true,
+ }
+ }
+ ]);
+
+};
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_fail.js b/browser/devtools/commandline/test/browser_gcli_fail.js
new file mode 100644
index 000000000..da7b7db91
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_fail.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFail.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+ mockCommands.setup();
+
+ origLogErrors = cli.logErrors;
+ cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+
+ cli.logErrors = origLogErrors;
+ origLogErrors = undefined;
+};
+
+exports.testBasic = function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'tsfail reject',
+ exec: {
+ completed: false,
+ output: 'rejected promise',
+ type: 'error',
+ error: true
+ }
+ },
+ {
+ setup: 'tsfail rejecttyped',
+ exec: {
+ completed: false,
+ output: '54',
+ type: 'number',
+ error: true
+ }
+ },
+ {
+ setup: 'tsfail throwerror',
+ exec: {
+ completed: true,
+ output: 'Error: thrown error',
+ type: 'error',
+ error: true
+ }
+ },
+ {
+ setup: 'tsfail throwstring',
+ exec: {
+ completed: true,
+ output: 'thrown string',
+ type: 'error',
+ error: true
+ }
+ },
+ {
+ setup: 'tsfail noerror',
+ exec: {
+ completed: true,
+ output: 'no error',
+ type: 'string',
+ error: false
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_focus.js b/browser/devtools/commandline/test/browser_gcli_focus.js
new file mode 100644
index 000000000..67e3e722b
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_focus.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFocus.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testBasic = function(options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: options.isJsdom,
+ name: 'exec setup',
+ setup: function() {
+ // Just check that we've got focus, and everything is clear
+ helpers.focusInput(options);
+ return helpers.setInput(options, 'help');
+ },
+ check: { },
+ exec: { }
+ },
+ {
+ setup: 'tsn deep',
+ check: {
+ input: 'tsn deep',
+ hints: '',
+ markup: 'IIIVIIII',
+ cursor: 8,
+ status: 'ERROR',
+ outputState: 'false:default',
+ tooltipState: 'false:default'
+ }
+ },
+ {
+ setup: 'tsn deep<TAB><RETURN>',
+ check: {
+ input: 'tsn deep ',
+ hints: '',
+ markup: 'IIIIIIIIV',
+ cursor: 9,
+ status: 'ERROR',
+ outputState: 'false:default',
+ tooltipState: 'true:isError'
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_history.js b/browser/devtools/commandline/test/browser_gcli_history.js
new file mode 100644
index 000000000..c333e2f8a
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_history.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testHistory.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var History = require('gcli/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/browser/devtools/commandline/test/browser_gcli_incomplete.js b/browser/devtools/commandline/test/browser_gcli_incomplete.js
new file mode 100644
index 000000000..f0e986a46
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_incomplete.js
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testIncomplete.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testBasic = function(options) {
+ return helpers.audit(options, [
+ {
+ setup: 'tsu 2 extra',
+ check: {
+ args: {
+ num: { value: 2, type: 'Argument' }
+ }
+ },
+ post: function() {
+ var requisition = options.display.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' }
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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.display.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.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/browser/devtools/commandline/test/browser_gcli_inputter.js b/browser/devtools/commandline/test/browser_gcli_inputter.js
new file mode 100644
index 000000000..a065c4b74
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_inputter.js
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testInputter.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+var KeyEvent = require('util/util').KeyEvent;
+// var assert = require('test/assert');
+// var mockCommands = require('gclitest/mockCommands');
+
+var latestEvent = undefined;
+var latestData = undefined;
+
+var outputted = function(ev) {
+ latestEvent = ev;
+
+ ev.output.promise.then(function() {
+ latestData = ev.output.data;
+ ev.output.onClose();
+ });
+};
+
+
+exports.setup = function(options) {
+ mockCommands.setup();
+ options.display.requisition.commandOutputManager.onOutput.add(outputted);
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+ options.display.requisition.commandOutputManager.onOutput.remove(outputted);
+};
+
+exports.testOutput = function(options) {
+ latestEvent = undefined;
+ latestData = undefined;
+
+ var inputter = options.display.inputter;
+ var focusManager = options.display.focusManager;
+
+ inputter.setInput('tss');
+
+ var ev0 = { keyCode: KeyEvent.DOM_VK_RETURN };
+ inputter.onKeyDown(ev0);
+
+ assert.is(inputter.element.value, 'tss', 'inputter 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 inputter.handleKeyUp(ev1).then(function() {
+ assert.ok(latestEvent != null, 'events this test');
+ assert.is(latestData, 'Exec: tss ', 'last command is tss');
+
+ assert.is(inputter.element.value, '', 'inputter should exec on RETURN keyUp');
+
+ assert.ok(focusManager._recentOutput, 'recent output happened');
+
+ var ev2 = { keyCode: KeyEvent.DOM_VK_F1 };
+ return inputter.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 inputter.handleKeyUp(ev3).then(function() {
+ assert.ok(!focusManager._helpRequested, 'ESCAPE = anti help');
+ });
+ });
+
+ });
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_intro.js b/browser/devtools/commandline/test/browser_gcli_intro.js
new file mode 100644
index 000000000..6d4e227de
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_intro.js
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testIntro.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+var canon = require('gcli/canon');
+
+exports.testIntroStatus = function(options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: function commandIntroMissing() {
+ return canon.getCommand('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/browser/devtools/commandline/test/browser_gcli_js.js b/browser/devtools/commandline/test/browser_gcli_js.js
new file mode 100644
index 000000000..ef315d6fb
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_js.js
@@ -0,0 +1,475 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testJs.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+var javascript = require('gcli/types/javascript');
+var canon = require('gcli/canon');
+
+var tempWindow = undefined;
+
+exports.setup = function(options) {
+ tempWindow = javascript.getGlobalObject();
+ Object.defineProperty(options.window, 'donteval', {
+ get: function() {
+ assert.ok(false, 'donteval should not be used');
+ return { cant: '', touch: '', 'this': '' };
+ },
+ enumerable: true,
+ configurable : true
+ });
+ javascript.setGlobalObject(options.window);
+};
+
+exports.shutdown = function(options) {
+ javascript.setGlobalObject(tempWindow);
+ tempWindow = undefined;
+ delete options.window.donteval;
+};
+
+exports.testBasic = function(options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: function commandJsMissing() {
+ return canon.getCommand('{') == null;
+ },
+ setup: '{',
+ check: {
+ input: '{',
+ hints: '',
+ markup: 'V',
+ cursor: 1,
+ current: 'javascript',
+ status: 'ERROR',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: '{' },
+ javascript: {
+ value: undefined,
+ arg: '{',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ setup: '{ ',
+ check: {
+ input: '{ ',
+ hints: '',
+ markup: 'VV',
+ cursor: 2,
+ current: 'javascript',
+ status: 'ERROR',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: '{' },
+ javascript: {
+ value: undefined,
+ arg: '{ ',
+ status: 'INCOMPLETE',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: function commandJsMissing() {
+ return canon.getCommand('{') == null;
+ },
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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 ',
+ 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) {
+ if (!options.isJsdom) {
+ // jsdom causes an eval here, maybe that's node/v8?
+ assert.ok('donteval' in options.window, 'donteval exists');
+ }
+
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: function commandJsMissing() {
+ return canon.getCommand('{') == null;
+ },
+ 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',
+ message: ''
+ }
+ }
+ }
+ },
+ {
+ 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: ''
+ }
+ }
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_keyboard1.js b/browser/devtools/commandline/test/browser_gcli_keyboard1.js
new file mode 100644
index 000000000..ea9ad4a73
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard1.js
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard1.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testComplete = 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 ' }
+ },
+ {
+ 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/browser/devtools/commandline/test/browser_gcli_keyboard2.js b/browser/devtools/commandline/test/browser_gcli_keyboard2.js
new file mode 100644
index 000000000..e6ab441d9
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard2.js
@@ -0,0 +1,413 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard2.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+// Bug 664377: Add tests for internal completion. i.e. "tsela<TAB> 1"
+
+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.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' }
+ }
+ */
+ ]);
+};
+
+exports.testDecr = function(options) {
+ return helpers.audit(options, [
+ /*
+ // See notes at top of testIncr
+ {
+ 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' }
+ }
+ */
+ ]);
+};
+
+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/browser/devtools/commandline/test/browser_gcli_keyboard3.js b/browser/devtools/commandline/test/browser_gcli_keyboard3.js
new file mode 100644
index 000000000..90d577dfb
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard3.js
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard3.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+var javascript = require('gcli/types/javascript');
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var canon = require('gcli/canon');
+
+var tempWindow = undefined;
+
+exports.setup = function(options) {
+ mockCommands.setup();
+
+ tempWindow = javascript.getGlobalObject();
+ javascript.setGlobalObject(options.window);
+};
+
+exports.shutdown = function(options) {
+ javascript.setGlobalObject(tempWindow);
+ tempWindow = undefined;
+
+ mockCommands.shutdown();
+};
+
+exports.testScript = function(options) {
+ return helpers.audit(options, [
+ {
+ skipIf: function commandJsMissing() {
+ return canon.getCommand('{') == null;
+ },
+ setup: '{ wind<TAB>',
+ check: { input: '{ window' }
+ },
+ {
+ skipIf: function commandJsMissing() {
+ return canon.getCommand('{') == null;
+ },
+ setup: '{ window.docum<TAB>',
+ check: { input: '{ window.document' }
+ }
+ ]);
+};
+
+exports.testJsdom = function(options) {
+ return helpers.audit(options, [
+ {
+ skipIf: function jsDomOrCommandJsMissing() {
+ return options.isJsdom || canon.getCommand('{') == null;
+ },
+ setup: '{ window.document.titl<TAB>',
+ check: { input: '{ window.document.title ' }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_menu.js b/browser/devtools/commandline/test/browser_gcli_menu.js
new file mode 100644
index 000000000..5ab3d8a34
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_menu.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testMenu.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+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/browser/devtools/commandline/test/browser_gcli_node.js b/browser/devtools/commandline/test/browser_gcli_node.js
new file mode 100644
index 000000000..abddd94b4
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_node.js
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testNode.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+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', message: '' },
+ 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' }
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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) {
+ var requisition = options.display.requisition;
+
+ return helpers.audit(options, [
+ {
+ skipIf: options.isJsdom,
+ 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' }
+ }
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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' }
+ }
+ },
+ post: function() {
+ assert.is(requisition.getAssignment('node').value.tagName,
+ 'HTML',
+ 'root id');
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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) {
+ var requisition = options.display.requisition;
+
+ return helpers.audit(options, [
+ {
+ skipIf: options.isJsdom,
+ 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' }
+ }
+ },
+ post: function() {
+ assert.is(requisition.getAssignment('node').value.tagName,
+ 'HTML',
+ '#gcli-input id');
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ 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' }
+ }
+ },
+ post: function() {
+ assert.is(requisition.getAssignment('node').value.tagName,
+ 'HTML',
+ 'root id');
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ setup: 'tse --nodes ffff',
+ check: {
+ input: 'tse --nodes ffff',
+ hints: ' <node> [options]',
+ markup: 'VVVVIIIIIIIVIIII',
+ cursor: 16,
+ current: 'nodes',
+ status: 'ERROR',
+ outputState: 'false:default',
+ tooltipState: 'true:isError',
+ args: {
+ command: { name: 'tse' },
+ node: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ },
+ nodes: {
+ value: undefined,
+ arg: ' --nodes ffff',
+ status: 'INCOMPLETE',
+ message: 'No matches'
+ },
+ nodes2: { arg: '', status: 'VALID', message: '' }
+ }
+ },
+ post: function() {
+ /*
+ assert.is(requisition.getAssignment('nodes2').value.constructor.name,
+ 'NodeList',
+ '#gcli-input id');
+ */
+ }
+ },
+ {
+ skipIf: options.isJsdom,
+ setup: 'tse --nodes2 ffff',
+ check: {
+ input: 'tse --nodes2 ffff',
+ hints: ' <node> [options]',
+ markup: 'VVVVVVVVVVVVVVVVV',
+ cursor: 17,
+ current: 'nodes2',
+ status: 'ERROR',
+ outputState: 'false:default',
+ tooltipState: 'false:default',
+ args: {
+ command: { name: 'tse' },
+ node: {
+ value: undefined,
+ arg: '',
+ status: 'INCOMPLETE',
+ message: ''
+ },
+ nodes: { arg: '', status: 'VALID', message: '' },
+ nodes2: { arg: ' --nodes2 ffff', status: 'VALID', message: '' }
+ }
+ },
+ post: function() {
+ /*
+ assert.is(requisition.getAssignment('nodes').value.constructor.name,
+ 'NodeList',
+ '#gcli-input id');
+ assert.is(requisition.getAssignment('nodes2').value.constructor.name,
+ 'NodeList',
+ '#gcli-input id');
+ */
+ }
+ },
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_remote.js b/browser/devtools/commandline/test/browser_gcli_remote.js
new file mode 100644
index 000000000..4e5765d28
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_remote.js
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testRemote.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testRemote = function(options) {
+ return helpers.audit(options, [
+ {
+ skipRemainingIf: !options.isHttp,
+ 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: {
+ input: 'connect remote',
+ hints: ' [options]',
+ markup: 'VVVVVVVVVVVVVV',
+ cursor: 14,
+ current: 'prefix',
+ status: 'VALID',
+ options: [ ],
+ message: '',
+ predictions: [ ],
+ unassigned: [ ],
+ args: {
+ command: { name: 'connect' },
+ prefix: { value: 'remote', arg: ' remote', status: 'VALID', message: '' },
+ host: { value: undefined, arg: '', status: 'VALID', message: '' },
+ port: { value: undefined, arg: '', status: 'VALID', message: '' },
+ }
+ },
+ exec: {
+ output: /^Added [0-9]* commands.$/,
+ completed: false,
+ type: 'string',
+ error: false
+ }
+ },
+ {
+ setup: 'remote ',
+ check: {
+ input: 'remote ',
+ hints: '',
+ markup: 'IIIIIIV',
+ cursor: 7,
+ current: '__command',
+ status: 'ERROR',
+ optionsIncludes: [
+ 'remote', 'remote cd', 'remote context', 'remote echo',
+ 'remote exec', 'remote exit', 'remote firefox', 'remote help',
+ 'remote intro', 'remote make'
+ ],
+ message: '',
+ predictions: [ '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',
+ completed: false,
+ 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
+ completed: false,
+ type: 'string',
+ 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',
+ completed: false,
+ 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: '',
+ completed: false,
+ 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: [
+ /^This command line/,
+ /F1\/Escape/
+ ],
+ completed: false,
+ 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: {
+ /*value:[object Object],*/
+ arg: ' remote',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: 'Using remote as a command prefix',
+ completed: true,
+ 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
+ completed: false,
+ type: 'string',
+ 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',
+ completed: true,
+ 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: ''
+ }
+ }
+ }
+ },
+ {
+ setup: 'disconnect remote --force',
+ check: {
+ input: 'disconnect remote --force',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV',
+ cursor: 25,
+ current: 'force',
+ status: 'VALID',
+ message: '',
+ unassigned: [ ],
+ args: {
+ command: { name: 'disconnect' },
+ prefix: {
+ value: function(connection) {
+ assert.is(connection.prefix, 'remote', 'disconnecting remote');
+ },
+ arg: ' remote',
+ status: 'VALID',
+ message: ''
+ }
+ }
+ },
+ exec: {
+ output: /^Removed [0-9]* commands.$/,
+ completed: true,
+ 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/browser/devtools/commandline/test/browser_gcli_resource.js b/browser/devtools/commandline/test/browser_gcli_resource.js
new file mode 100644
index 000000000..672bb2fd7
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_resource.js
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testResource.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var util = require('util/util');
+
+var resource = require('gcli/types/resource');
+var types = require('gcli/types');
+var Status = require('gcli/types').Status;
+
+
+var tempDocument = undefined;
+
+exports.setup = function(options) {
+ tempDocument = resource.getDocument();
+ resource.setDocument(options.window.document);
+};
+
+exports.shutdown = function(options) {
+ resource.setDocument(tempDocument);
+ tempDocument = undefined;
+};
+
+exports.testAllPredictions1 = function(options) {
+ if (options.isFirefox || options.isJsdom) {
+ assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
+ return;
+ }
+
+ var resource = types.createType('resource');
+ return resource.getLookup().then(function(opts) {
+ assert.ok(opts.length > 1, 'have all resources');
+
+ return util.promiseEach(opts, function(prediction) {
+ return checkPrediction(resource, prediction);
+ });
+ });
+};
+
+exports.testScriptPredictions = function(options) {
+ if (options.isFirefox || options.isJsdom) {
+ assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
+ return;
+ }
+
+ var resource = types.createType({ name: 'resource', include: 'text/javascript' });
+ return resource.getLookup().then(function(opts) {
+ assert.ok(opts.length > 1, 'have js resources');
+
+ return util.promiseEach(opts, function(prediction) {
+ return checkPrediction(resource, prediction);
+ });
+ });
+};
+
+exports.testStylePredictions = function(options) {
+ if (options.isFirefox || options.isJsdom) {
+ assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
+ return;
+ }
+
+ var resource = types.createType({ name: 'resource', include: 'text/css' });
+ return resource.getLookup().then(function(opts) {
+ assert.ok(opts.length >= 1, 'have css resources');
+
+ return util.promiseEach(opts, function(prediction) {
+ return checkPrediction(resource, prediction);
+ });
+ });
+};
+
+exports.testAllPredictions2 = function(options) {
+ if (options.isJsdom) {
+ assert.log('Skipping checks due to jsdom document.stylsheets support.');
+ return;
+ }
+
+ var scriptRes = types.createType({ name: 'resource', include: 'text/javascript' });
+ return scriptRes.getLookup().then(function(scriptOptions) {
+ var styleRes = types.createType({ name: 'resource', include: 'text/css' });
+ return styleRes.getLookup().then(function(styleOptions) {
+ var allRes = types.createType({ name: 'resource' });
+ return allRes.getLookup().then(function(allOptions) {
+ assert.is(scriptOptions.length + styleOptions.length,
+ allOptions.length,
+ 'split');
+ });
+ });
+ });
+};
+
+exports.testAllPredictions3 = function(options) {
+ if (options.isJsdom) {
+ assert.log('Skipping checks due to jsdom document.stylsheets support.');
+ return;
+ }
+
+ var res1 = types.createType({ name: 'resource' });
+ return res1.getLookup().then(function(options1) {
+ var res2 = types.createType('resource');
+ return res2.getLookup().then(function(options2) {
+ assert.is(options1.length, options2.length, 'type spec');
+ });
+ });
+};
+
+function checkPrediction(res, prediction) {
+ var name = prediction.name;
+ var value = prediction.value;
+
+ // resources don't need context so cheat and pass in null
+ var context = null;
+ 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);
+
+ var strung = res.stringify(value, context);
+ assert.is(strung, name, 'stringify for ' + name);
+
+ assert.is(typeof value.loadContents, 'function', 'resource for ' + name);
+ assert.is(typeof value.element, 'object', 'resource for ' + name);
+ });
+}
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_scratchpad.js b/browser/devtools/commandline/test/browser_gcli_scratchpad.js
new file mode 100644
index 000000000..2e1189c4b
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_scratchpad.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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testScratchpad.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+
+var origScratchpad = undefined;
+
+exports.setup = function(options) {
+ origScratchpad = options.display.inputter.scratchpad;
+ options.display.inputter.scratchpad = stubScratchpad;
+};
+
+exports.shutdown = function(options) {
+ options.display.inputter.scratchpad = origScratchpad;
+};
+
+var stubScratchpad = {
+ shouldActivate: function(ev) {
+ return true;
+ },
+ activatedCount: 0,
+ linkText: 'scratchpad.linkText'
+};
+stubScratchpad.activate = function(value) {
+ stubScratchpad.activatedCount++;
+ return true;
+};
+
+
+exports.testActivate = function(options) {
+ var ev = {};
+ stubScratchpad.activatedCount = 0;
+ options.display.inputter.handleKeyUp(ev);
+ assert.is(stubScratchpad.activatedCount, 1, 'scratchpad is activated');
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_spell.js b/browser/devtools/commandline/test/browser_gcli_spell.js
new file mode 100644
index 000000000..f6670ed55
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_spell.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testSpell.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var spell = require('gcli/types/spell');
+
+exports.testSpellerSimple = function(options) {
+ var alternatives = Object.keys(options.window);
+
+ assert.is(spell.correct('document', alternatives), 'document');
+ assert.is(spell.correct('documen', alternatives), 'document');
+
+ if (options.isJsdom) {
+ assert.log('jsdom is weird, skipping some tests');
+ }
+ else {
+ assert.is(spell.correct('ocument', alternatives), 'document');
+ }
+ assert.is(spell.correct('odcument', alternatives), 'document');
+
+ assert.is(spell.correct('=========', alternatives), undefined);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_split.js b/browser/devtools/commandline/test/browser_gcli_split.js
new file mode 100644
index 000000000..743256b93
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_split.js
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testSplit.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var cli = require('gcli/cli');
+var Requisition = require('gcli/cli').Requisition;
+var canon = require('gcli/canon');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+
+exports.testSplitSimple = function(options) {
+ var args;
+ var requisition = new Requisition();
+
+ args = cli.tokenize('s');
+ requisition._split(args);
+ assert.is(args.length, 0);
+ assert.is(requisition.commandAssignment.arg.text, 's');
+};
+
+exports.testFlatCommand = function(options) {
+ var args;
+ var requisition = new Requisition();
+
+ args = cli.tokenize('tsv');
+ requisition._split(args);
+ assert.is(args.length, 0);
+ assert.is(requisition.commandAssignment.value.name, 'tsv');
+
+ args = cli.tokenize('tsv a b');
+ requisition._split(args);
+ assert.is(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 (!canon.getCommand('{')) {
+ assert.log('Skipping testJavascript because { is not registered');
+ return;
+ }
+
+ var args;
+ var requisition = new Requisition();
+
+ args = cli.tokenize('{');
+ requisition._split(args);
+ assert.is(args.length, 1);
+ assert.is(args[0].text, '');
+ assert.is(requisition.commandAssignment.arg.text, '');
+ assert.is(requisition.commandAssignment.value.name, '{');
+};
+
+// BUG 663081 - add tests for sub commands
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_string.js b/browser/devtools/commandline/test/browser_gcli_string.js
new file mode 100644
index 000000000..cd06360e7
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_string.js
@@ -0,0 +1,293 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testString.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testNewLine = function(options) {
+ 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) {
+ 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) {
+ 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) {
+ 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',
+ message: ''
+ },
+ 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) {
+ 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', message: '' },
+ 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', message: '' },
+ 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', message: '' },
+ 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', message: '' },
+ p3: { value: '', arg: ' --p3 ""', status: 'VALID', message: '' },
+ }
+ }
+ }
+ ]);
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_tokenize.js b/browser/devtools/commandline/test/browser_gcli_tokenize.js
new file mode 100644
index 000000000..7bdc7ef05
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_tokenize.js
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTokenize.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/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/browser/devtools/commandline/test/browser_gcli_tooltip.js b/browser/devtools/commandline/test/browser_gcli_tooltip.js
new file mode 100644
index 000000000..e6b659101
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_tooltip.js
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTooltip.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+ mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+ mockCommands.shutdown();
+};
+
+exports.testActivate = function(options) {
+ if (!options.display) {
+ assert.log('No display. Skipping activate tests');
+ return;
+ }
+
+ if (options.isJsdom) {
+ assert.log('Reduced checks due to JSDom.textContent');
+ }
+
+ 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/browser/devtools/commandline/test/browser_gcli_types.js b/browser/devtools/commandline/test/browser_gcli_types.js
new file mode 100644
index 000000000..d8bc19313
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_types.js
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTypes.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var types = require('gcli/types');
+
+function forEachType(options, typeSpec, callback) {
+ types.getTypeNames().forEach(function(name) {
+ typeSpec.name = name;
+ typeSpec.requisition = options.display.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 types.createType('string');
+ };
+ }
+ else if (name === 'array') {
+ typeSpec.subtype = 'string';
+ }
+
+ var type = types.createType(typeSpec);
+ callback(type);
+
+ // Clean up
+ delete typeSpec.name;
+ delete typeSpec.requisition;
+ delete typeSpec.data;
+ delete typeSpec.delegateType;
+ delete typeSpec.subtype;
+ });
+}
+
+exports.testDefault = function(options) {
+ if (options.isJsdom) {
+ assert.log('Skipping tests due to issues with resource type.');
+ return;
+ }
+
+ forEachType(options, {}, function(type) {
+ var context = options.display.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) {
+ forEachType(options, { defaultValue: null }, function(type) {
+ assert.is(type.stringify(null, null), '', 'stringify(null) for ' + type.name);
+ });
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/browser_gcli_util.js b/browser/devtools/commandline/test/browser_gcli_util.js
new file mode 100644
index 000000000..b1d531869
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_util.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testUtil.js</p>";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.runTests(options, exports);
+ }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+var util = require('util/util');
+// var assert = require('test/assert');
+
+exports.testFindCssSelector = function(options) {
+ var nodes = options.window.document.querySelectorAll('*');
+ for (var i = 0; i < nodes.length; i++) {
+ var selector = util.findCssSelector(nodes[i]);
+ var matches = options.window.document.querySelectorAll(selector);
+
+ assert.is(matches.length, 1, 'multiple matches for ' + selector);
+ assert.is(matches[0], nodes[i], 'non-matching selector: ' + selector);
+ }
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/head.js b/browser/devtools/commandline/test/head.js
new file mode 100644
index 000000000..c7e71fea3
--- /dev/null
+++ b/browser/devtools/commandline/test/head.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/. */
+
+const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/commandline/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/commandline/test/";
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
+Services.scriptloader.loadSubScript(testDir + "/mockCommands.js", this);
+
+/**
+ * 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/browser/devtools/commandline/test/helpers.js b/browser/devtools/commandline/test/helpers.js
new file mode 100644
index 000000000..b7b9bd490
--- /dev/null
+++ b/browser/devtools/commandline/test/helpers.js
@@ -0,0 +1,1038 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Use as a JSM
+// ------------
+// helpers._createDebugCheck() can be useful at runtime. To use as a JSM, copy
+// commandline/test/helpers.js to shared/helpers.jsm, and then import to
+// DeveloperToolbar.jsm with:
+// XPCOMUtils.defineLazyModuleGetter(this, "helpers",
+// "resource:///modules/devtools/helpers.jsm");
+// At the bottom of DeveloperToolbar.prototype._onload add this:
+// var options = { display: this.display };
+// this._input.onkeypress = function(ev) {
+// helpers.setup(options);
+// dump(helpers._createDebugCheck() + '\n\n');
+// };
+// Now GCLI will emit output on every keypress that both explains the state
+// of GCLI and can be run as a test case.
+
+this.EXPORTED_SYMBOLS = [ 'helpers' ];
+var helpers = {};
+this.helpers = helpers;
+let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
+
+let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
+let TargetFactory = (Cu.import("resource://gre/modules/devtools/Loader.jsm", {})).devtools.TargetFactory;
+
+let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
+let assert = { ok: ok, is: is, log: info };
+
+var util = require('util/util');
+
+var converters = require('gcli/converters');
+
+/**
+ * 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
+ * - window: Content window for the created tab. a.k.a 'content' in mochitest
+ * - 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) {
+ var deferred = Promise.defer();
+
+ 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 onPageLoad = function() {
+ options.browser.removeEventListener("load", onPageLoad, true);
+ options.document = options.browser.contentDocument;
+ options.window = options.document.defaultView;
+
+ var cleanUp = function() {
+ tabbrowser.removeTab(options.tab);
+
+ delete options.window;
+ delete options.document;
+
+ delete options.target;
+ delete options.browser;
+ delete options.tab;
+
+ delete options.chromeWindow;
+ delete options.isFirefox;
+
+ deferred.resolve();
+ };
+
+ var reply = callback(options);
+ Promise.resolve(reply).then(cleanUp, function(error) {
+ ok(false, error);
+ cleanUp();
+ });
+ };
+
+ options.browser.contentWindow.location = url;
+ options.browser.addEventListener("load", onPageLoad, true);
+
+ return deferred.promise;
+};
+
+/**
+ * Warning: For use with Firefox Mochitests only.
+ *
+ * As addTab, but that also opens the developer toolbar. In addition a new
+ * 'display' property is added to the options object with the display from GCLI
+ * in the developer toolbar
+ */
+helpers.addTabWithToolbar = function(url, callback, options) {
+ return helpers.addTab(url, function(innerOptions) {
+ var win = innerOptions.chromeWindow;
+ var deferred = Promise.defer();
+
+ win.DeveloperToolbar.show(true, function() {
+ innerOptions.display = win.DeveloperToolbar.display;
+
+ var cleanUp = function() {
+ win.DeveloperToolbar.hide();
+ delete innerOptions.display;
+ deferred.resolve();
+ };
+
+ var reply = callback(innerOptions);
+ Promise.resolve(reply).then(cleanUp, function(error) {
+ ok(false, error);
+ cleanUp();
+ });
+ });
+ return deferred.promise;
+ }, 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);
+ };
+
+ 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);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+function checkOptions(options) {
+ if (options == null) {
+ console.trace();
+ throw new Error('Missing options object');
+ }
+ if (options.display == null) {
+ console.trace();
+ throw new Error('options object does not contain a display property');
+ }
+ if (options.display.requisition == null) {
+ console.trace();
+ throw new Error('display object does not contain a requisition');
+ }
+}
+
+/**
+ * Various functions to return the actual state of the command line
+ */
+helpers._actual = {
+ input: function(options) {
+ return options.display.inputter.element.value;
+ },
+
+ hints: function(options) {
+ var templateData = options.display.completer._getCompleterTemplateData();
+ var join = function(directTabText, emptyParameters, arrowTabText) {
+ return (directTabText + emptyParameters.join('') + arrowTabText)
+ .replace(/\u00a0/g, ' ')
+ .replace(/\u21E5/, '->')
+ .replace(/ $/, '');
+ };
+
+ var promisedJoin = Promise.promised(join);
+ return promisedJoin(templateData.directTabText,
+ templateData.emptyParameters,
+ templateData.arrowTabText);
+ },
+
+ markup: function(options) {
+ var cursor = options.display.inputter.element.selectionStart;
+ var statusMarkup = options.display.requisition.getInputStatusMarkup(cursor);
+ return statusMarkup.map(function(s) {
+ return Array(s.string.length + 1).join(s.status.toString()[0]);
+ }).join('');
+ },
+
+ cursor: function(options) {
+ return options.display.inputter.element.selectionStart;
+ },
+
+ current: function(options) {
+ return options.display.requisition.getAssignmentAt(helpers._actual.cursor(options)).param.name;
+ },
+
+ status: function(options) {
+ return options.display.requisition.getStatus().toString();
+ },
+
+ predictions: function(options) {
+ var cursor = options.display.inputter.element.selectionStart;
+ var assignment = options.display.requisition.getAssignmentAt(cursor);
+ return assignment.getPredictions().then(function(predictions) {
+ return predictions.map(function(prediction) {
+ return prediction.name;
+ });
+ });
+ },
+
+ unassigned: function(options) {
+ return options.display.requisition._unassigned.map(function(assignment) {
+ return assignment.arg.toString();
+ }.bind(this));
+ },
+
+ outputState: function(options) {
+ var outputData = options.display.focusManager._shouldShowOutput();
+ return outputData.visible + ':' + outputData.reason;
+ },
+
+ tooltipState: function(options) {
+ var tooltipData = options.display.focusManager._shouldShowTooltip();
+ return tooltipData.visible + ':' + tooltipData.reason;
+ },
+
+ options: function(options) {
+ if (options.display.tooltip.field.menu == null) {
+ return [];
+ }
+ return options.display.tooltip.field.menu.items.map(function(item) {
+ return item.name.textContent ? item.name.textContent : item.name;
+ });
+ },
+
+ message: function(options) {
+ return options.display.tooltip.errorEle.textContent;
+ }
+};
+
+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.display.requisition;
+ var command = requisition.commandAssignment.value;
+ var cursor = helpers._actual.cursor(options);
+ var input = helpers._actual.input(options);
+ var padding = 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.getMessage() + '\'';
+ output += ' },\n';
+ });
+
+ output += ' }\n';
+ }
+
+ output += ' },\n';
+ output += ' exec: {\n';
+ output += ' output: \'\',\n';
+ output += ' completed: true,\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.display.inputter.focus();
+};
+
+/**
+ * Simulate pressing TAB in the input field
+ */
+helpers.pressTab = function(options) {
+ checkOptions(options);
+ return helpers.pressKey(options, 9 /*KeyEvent.DOM_VK_TAB*/);
+};
+
+/**
+ * Simulate pressing RETURN in the input field
+ */
+helpers.pressReturn = function(options) {
+ checkOptions(options);
+ return helpers.pressKey(options, 13 /*KeyEvent.DOM_VK_RETURN*/);
+};
+
+/**
+ * Simulate pressing a key by keyCode in the input field
+ */
+helpers.pressKey = function(options, keyCode) {
+ checkOptions(options);
+ var fakeEvent = {
+ keyCode: keyCode,
+ preventDefault: function() { },
+ timeStamp: new Date().getTime()
+ };
+ options.display.inputter.onKeyDown(fakeEvent);
+ return options.display.inputter.handleKeyUp(fakeEvent);
+};
+
+/**
+ * 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, 38 /*KeyEvent.DOM_VK_UP*/);
+ },
+ '<DOWN>': function(options) {
+ return helpers.pressKey(options, 40 /*KeyEvent.DOM_VK_DOWN*/);
+ }
+};
+
+/**
+ * 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 promise = undefined;
+ var inputter = options.display.inputter;
+ // 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) {
+ promise = inputter.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
+ promise = inputter.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 inputter.setInput(inputter.element.value + chunk);
+ }
+ });
+ });
+ }
+
+ return promise.then(function() {
+ if (cursor != null) {
+ options.display.inputter.setCursor({ start: cursor, end: cursor });
+ }
+ else {
+ // This is a hack because jsdom appears to not handle cursor updates
+ // in the same way as most browsers.
+ if (options.isJsdom) {
+ options.display.inputter.setCursor({
+ start: typed.length,
+ end: typed.length
+ });
+ }
+ }
+
+ options.display.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) {
+ if (checks == null) {
+ return Promise.resolve();
+ }
+
+ var outstanding = [];
+ var suffix = name ? ' (for \'' + name + '\')' : '';
+
+ if ('input' in checks) {
+ assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
+ }
+
+ if ('cursor' in checks) {
+ assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
+ }
+
+ if ('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 ('markup' in checks) {
+ assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
+ }
+
+ if ('hints' in checks) {
+ var hintCheck = function(actualHints) {
+ assert.is(actualHints, checks.hints, 'hints' + suffix);
+ };
+ outstanding.push(helpers._actual.hints(options).then(hintCheck));
+ }
+
+ if ('predictions' in checks) {
+ var predictionsCheck = function(actualPredictions) {
+ helpers._arrayIs(actualPredictions,
+ checks.predictions,
+ 'predictions' + suffix);
+ };
+ outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
+ }
+
+ if ('predictionsContains' in checks) {
+ var containsCheck = function(actualPredictions) {
+ checks.predictionsContains.forEach(function(prediction) {
+ var index = actualPredictions.indexOf(prediction);
+ assert.ok(index !== -1,
+ 'predictionsContains:' + prediction + suffix);
+ });
+ };
+ outstanding.push(helpers._actual.predictions(options).then(containsCheck));
+ }
+
+ if ('unassigned' in checks) {
+ helpers._arrayIs(helpers._actual.unassigned(options),
+ checks.unassigned,
+ 'unassigned' + suffix);
+ }
+
+ if ('tooltipState' in checks) {
+ if (options.isJsdom) {
+ assert.log('Skipped ' + name + '/tooltipState due to jsdom');
+ }
+ else {
+ assert.is(helpers._actual.tooltipState(options),
+ checks.tooltipState,
+ 'tooltipState' + suffix);
+ }
+ }
+
+ if ('outputState' in checks) {
+ if (options.isJsdom) {
+ assert.log('Skipped ' + name + '/outputState due to jsdom');
+ }
+ else {
+ assert.is(helpers._actual.outputState(options),
+ checks.outputState,
+ 'outputState' + suffix);
+ }
+ }
+
+ if ('options' in checks) {
+ helpers._arrayIs(helpers._actual.options(options),
+ checks.options,
+ 'options' + suffix);
+ }
+
+ if ('error' in checks) {
+ assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
+ }
+
+ if (checks.args != null) {
+ var requisition = options.display.requisition;
+ 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) {
+ if (options.isJsdom) {
+ assert.log('Skipped arg.' + paramName + '.name due to jsdom');
+ }
+ else {
+ 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 ('message' in check) {
+ if (typeof check.message.test === 'function') {
+ assert.ok(check.message.test(assignment.getMessage()),
+ 'arg.' + paramName + '.message' + suffix);
+ }
+ else {
+ assert.is(assignment.getMessage(),
+ 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) {
+ if (expected == null) {
+ return Promise.resolve({});
+ }
+
+ var output;
+ try {
+ output = options.display.requisition.exec({ hidden: true });
+ }
+ catch (ex) {
+ assert.ok(false, 'Failure executing \'' + name + '\': ' + ex);
+ util.errorHandler(ex);
+
+ return Promise.resolve({});
+ }
+
+ if ('completed' in expected) {
+ assert.is(output.completed,
+ expected.completed,
+ 'output.completed false for: ' + name);
+ }
+
+ if (!options.window.document.createElement) {
+ assert.log('skipping output tests (missing doc.createElement) for ' + name);
+ return Promise.resolve({ output: output });
+ }
+
+ if (!('output' in expected)) {
+ return Promise.resolve({ output: output });
+ }
+
+ var checkOutput = function() {
+ 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);
+ }
+
+ var conversionContext = options.display.requisition.conversionContext;
+ var convertPromise = converters.convert(output.data, output.type, 'dom',
+ conversionContext);
+ return convertPromise.then(function(node) {
+ var actualOutput = node.textContent.trim();
+
+ var doTest = function(match, against) {
+ if (match.test(against)) {
+ assert.ok(true, 'html output for ' + name + ' should match /' +
+ match.source + '/');
+ } else {
+ assert.ok(false, 'html output for ' + name + ' should match /' +
+ match.source +
+ '/. Actual textContent: "' + against + '"');
+ }
+ };
+
+ if (typeof expected.output === 'string') {
+ assert.is(actualOutput,
+ expected.output,
+ 'html output for ' + name);
+ }
+ else if (Array.isArray(expected.output)) {
+ expected.output.forEach(function(match) {
+ doTest(match, actualOutput);
+ });
+ }
+ else {
+ doTest(expected.output, actualOutput);
+ }
+
+ return { output: output, text: actualOutput };
+ });
+ };
+
+ return output.promise.then(checkOutput, checkOutput);
+};
+
+/**
+ * Helper to setup the test
+ */
+helpers._setup = function(options, name, action) {
+ if (typeof action === 'string') {
+ return helpers.setInput(options, action);
+ }
+
+ if (typeof action === 'function') {
+ return Promise.resolve(action());
+ }
+
+ return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
+};
+
+/**
+ * Helper to shutdown the test
+ */
+helpers._post = function(name, action, data) {
+ if (typeof action === 'function') {
+ return Promise.resolve(action(data.output, data.text));
+ }
+ return Promise.resolve(action);
+};
+
+/*
+ * 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 = undefined;
+var start = undefined;
+
+/**
+ * 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. jsdom, 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.getMessage
+ * - 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
+ * - completed: A boolean which declares that we should check to see if the
+ * command completed synchronously
+ * - post: Function to be called after the checks have been run
+ */
+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.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 (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);
+ 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.setup);
+ return setupDone.then(function(chunkLen) {
+
+ if (typeof chunkLen !== 'number') {
+ chunkLen = 1;
+ }
+
+ 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.post, data).then(function() {
+ if (assert.testLogging) {
+ log('- END \'' + name + '\' in ' + assert.currentTest);
+ }
+ });
+ });
+ });
+ });
+ }).then(function() {
+ return options.display.inputter.setInput('');
+ });
+};
+
+/**
+ * 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);
+ }
+}
+
+//});
diff --git a/browser/devtools/commandline/test/mockCommands.js b/browser/devtools/commandline/test/mockCommands.js
new file mode 100644
index 000000000..4d4beb5bb
--- /dev/null
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -0,0 +1,575 @@
+/*
+ * 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.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+let { require: require, define: define } = Cu.import("resource://gre/modules/devtools/Require.jsm", {});
+Cu.import("resource://gre/modules/devtools/gcli.jsm", {});
+
+// <INJECTED SOURCE:END>
+
+var mockCommands = {};
+
+// We use an alias for exports here because this module is used in Firefox
+// mochitests where we don't have define/require
+
+'use strict';
+
+var util = require('util/util');
+var canon = require('gcli/canon');
+var types = require('gcli/types');
+
+mockCommands.option1 = { };
+mockCommands.option2 = { };
+mockCommands.option3 = { };
+
+mockCommands.optionType = {
+ name: 'optionType',
+ parent: 'selection',
+ lookup: [
+ { name: 'option1', value: mockCommands.option1 },
+ { name: 'option2', value: mockCommands.option2 },
+ { name: 'option3', value: mockCommands.option3 }
+ ]
+};
+
+mockCommands.optionValue = {
+ name: 'optionValue',
+ parent: 'delegate',
+ delegateType: function(executionContext) {
+ if (executionContext != null) {
+ var option = executionContext.getArgsObject().optionType;
+ if (option != null) {
+ return option.type;
+ }
+ }
+ return types.createType('blank');
+ }
+};
+
+mockCommands.onCommandExec = util.createEvent('commands.onCommandExec');
+
+function createExec(name) {
+ return function(args, executionContext) {
+ var data = {
+ args: args,
+ context: executionContext
+ };
+ mockCommands.onCommandExec(data);
+ var argsOut = Object.keys(args).map(function(key) {
+ return key + '=' + args[key];
+ }).join(', ');
+ return 'Exec: ' + name + ' ' + argsOut;
+ };
+}
+
+var tsv = {
+ name: 'tsv',
+ params: [
+ { name: 'optionType', type: 'optionType' },
+ { name: 'optionValue', type: 'optionValue' }
+ ],
+ exec: createExec('tsv')
+};
+
+var tsr = {
+ name: 'tsr',
+ params: [ { name: 'text', type: 'string' } ],
+ exec: createExec('tsr')
+};
+
+var tsrsrsr = {
+ name: 'tsrsrsr',
+ params: [
+ { name: 'p1', type: 'string' },
+ { name: 'p2', type: 'string' },
+ { name: 'p3', type: { name: 'string', allowBlank: true} },
+ ],
+ exec: createExec('tsrsrsr')
+};
+
+var tso = {
+ name: 'tso',
+ params: [ { name: 'text', type: 'string', defaultValue: null } ],
+ exec: createExec('tso')
+};
+
+var tse = {
+ 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')
+};
+
+var tsj = {
+ name: 'tsj',
+ params: [ { name: 'javascript', type: 'javascript' } ],
+ exec: createExec('tsj')
+};
+
+var tsb = {
+ name: 'tsb',
+ params: [ { name: 'toggle', type: 'boolean' } ],
+ exec: createExec('tsb')
+};
+
+var tss = {
+ name: 'tss',
+ exec: createExec('tss')
+};
+
+var tsu = {
+ name: 'tsu',
+ params: [ { name: 'num', type: { name: 'number', max: 10, min: -5, step: 3 } } ],
+ exec: createExec('tsu')
+};
+
+var tsf = {
+ name: 'tsf',
+ params: [ { name: 'num', type: { name: 'number', allowFloat: true, max: 11.5, min: -6.5, step: 1.5 } } ],
+ exec: createExec('tsf')
+};
+
+var tsn = {
+ name: 'tsn'
+};
+
+var tsnDif = {
+ name: 'tsn dif',
+ description: 'tsn dif',
+ params: [ { name: 'text', type: 'string', description: 'tsn dif text' } ],
+ exec: createExec('tsnDif')
+};
+
+var tsnExt = {
+ name: 'tsn ext',
+ params: [ { name: 'text', type: 'string' } ],
+ exec: createExec('tsnExt')
+};
+
+var tsnExte = {
+ name: 'tsn exte',
+ params: [ { name: 'text', type: 'string' } ],
+ exec: createExec('tsnExte')
+};
+
+var tsnExten = {
+ name: 'tsn exten',
+ params: [ { name: 'text', type: 'string' } ],
+ exec: createExec('tsnExten')
+};
+
+var tsnExtend = {
+ name: 'tsn extend',
+ params: [ { name: 'text', type: 'string' } ],
+ exec: createExec('tsnExtend')
+};
+
+var tsnDeep = {
+ name: 'tsn deep'
+};
+
+var tsnDeepDown = {
+ name: 'tsn deep down'
+};
+
+var tsnDeepDownNested = {
+ name: 'tsn deep down nested'
+};
+
+var tsnDeepDownNestedCmd = {
+ name: 'tsn deep down nested cmd',
+ exec: createExec('tsnDeepDownNestedCmd')
+};
+
+var tshidden = {
+ name: 'tshidden',
+ hidden: true,
+ params: [
+ {
+ group: 'Options',
+ params: [
+ {
+ name: 'visible',
+ type: 'string',
+ defaultValue: null,
+ description: 'visible'
+ },
+ {
+ name: 'invisiblestring',
+ type: 'string',
+ description: 'invisiblestring',
+ defaultValue: null,
+ hidden: true
+ },
+ {
+ name: 'invisibleboolean',
+ type: 'boolean',
+ description: 'invisibleboolean',
+ hidden: true
+ }
+ ]
+ }
+ ],
+ exec: createExec('tshidden')
+};
+
+var tselarr = {
+ name: 'tselarr',
+ params: [
+ { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
+ { name: 'arr', type: { name: 'array', subtype: 'string' } }
+ ],
+ exec: createExec('tselarr')
+};
+
+var tsm = {
+ 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')
+};
+
+var tsg = {
+ 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')
+};
+
+var tscook = {
+ 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')
+};
+
+var tslong = {
+ 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',
+ type: 'number',
+ description: 'num Desc',
+ defaultValue: 2
+ },
+ {
+ name: 'sel',
+ type: {
+ name: 'selection',
+ lookup: [
+ { name: "space", value: " " },
+ { name: "tab", value: "\t" }
+ ]
+ },
+ description: 'sel Desc',
+ defaultValue: ' '
+ },
+ {
+ name: 'bool',
+ type: 'boolean',
+ description: 'bool Desc'
+ },
+ {
+ name: 'num2',
+ type: 'number',
+ description: 'num2 Desc',
+ defaultValue: -1
+ },
+ {
+ name: 'bool2',
+ type: 'boolean',
+ description: 'bool2 Desc'
+ },
+ {
+ name: 'sel2',
+ type: {
+ name: 'selection',
+ data: [ 'collapse', 'basic', 'with space', 'with two spaces' ]
+ },
+ description: 'sel2 Desc',
+ defaultValue: "collapse"
+ }
+ ]
+ }
+ ],
+ exec: createExec('tslong')
+};
+
+var tsdate = {
+ 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')
+};
+
+var tsfail = {
+ 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') {
+ var deferred = context.defer();
+ setTimeout(function() {
+ deferred.reject('rejected promise');
+ }, 10);
+ return deferred.promise;
+ }
+
+ if (args.method === 'rejecttyped') {
+ var deferred = context.defer();
+ setTimeout(function() {
+ deferred.reject(context.typedData('number', 54));
+ }, 10);
+ return deferred.promise;
+ }
+
+ if (args.method === 'throwinpromise') {
+ var deferred = context.defer();
+ setTimeout(function() {
+ deferred.resolve('should be lost');
+ }, 10);
+ return deferred.promise.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';
+ }
+};
+
+mockCommands.commands = {};
+
+/**
+ * Registration and de-registration.
+ */
+mockCommands.setup = function(opts) {
+ // setup/shutdown needs to register/unregister types, however that means we
+ // need to re-initialize mockCommands.option1 and mockCommands.option2 with
+ // the actual types
+ mockCommands.option1.type = types.createType('string');
+ mockCommands.option2.type = types.createType('number');
+ mockCommands.option3.type = types.createType({
+ name: 'selection',
+ lookup: [
+ { name: 'one', value: 1 },
+ { name: 'two', value: 2 },
+ { name: 'three', value: 3 }
+ ]
+ });
+
+ types.addType(mockCommands.optionType);
+ types.addType(mockCommands.optionValue);
+
+ mockCommands.commands.tsv = canon.addCommand(tsv);
+ mockCommands.commands.tsr = canon.addCommand(tsr);
+ mockCommands.commands.tsrsrsr = canon.addCommand(tsrsrsr);
+ mockCommands.commands.tso = canon.addCommand(tso);
+ mockCommands.commands.tse = canon.addCommand(tse);
+ mockCommands.commands.tsj = canon.addCommand(tsj);
+ mockCommands.commands.tsb = canon.addCommand(tsb);
+ mockCommands.commands.tss = canon.addCommand(tss);
+ mockCommands.commands.tsu = canon.addCommand(tsu);
+ mockCommands.commands.tsf = canon.addCommand(tsf);
+ mockCommands.commands.tsn = canon.addCommand(tsn);
+ mockCommands.commands.tsnDif = canon.addCommand(tsnDif);
+ mockCommands.commands.tsnExt = canon.addCommand(tsnExt);
+ mockCommands.commands.tsnExte = canon.addCommand(tsnExte);
+ mockCommands.commands.tsnExten = canon.addCommand(tsnExten);
+ mockCommands.commands.tsnExtend = canon.addCommand(tsnExtend);
+ mockCommands.commands.tsnDeep = canon.addCommand(tsnDeep);
+ mockCommands.commands.tsnDeepDown = canon.addCommand(tsnDeepDown);
+ mockCommands.commands.tsnDeepDownNested = canon.addCommand(tsnDeepDownNested);
+ mockCommands.commands.tsnDeepDownNestedCmd = canon.addCommand(tsnDeepDownNestedCmd);
+ mockCommands.commands.tselarr = canon.addCommand(tselarr);
+ mockCommands.commands.tsm = canon.addCommand(tsm);
+ mockCommands.commands.tsg = canon.addCommand(tsg);
+ mockCommands.commands.tshidden = canon.addCommand(tshidden);
+ mockCommands.commands.tscook = canon.addCommand(tscook);
+ mockCommands.commands.tslong = canon.addCommand(tslong);
+ mockCommands.commands.tsdate = canon.addCommand(tsdate);
+ mockCommands.commands.tsfail = canon.addCommand(tsfail);
+};
+
+mockCommands.shutdown = function(opts) {
+ canon.removeCommand(tsv);
+ canon.removeCommand(tsr);
+ canon.removeCommand(tsrsrsr);
+ canon.removeCommand(tso);
+ canon.removeCommand(tse);
+ canon.removeCommand(tsj);
+ canon.removeCommand(tsb);
+ canon.removeCommand(tss);
+ canon.removeCommand(tsu);
+ canon.removeCommand(tsf);
+ canon.removeCommand(tsn);
+ canon.removeCommand(tsnDif);
+ canon.removeCommand(tsnExt);
+ canon.removeCommand(tsnExte);
+ canon.removeCommand(tsnExten);
+ canon.removeCommand(tsnExtend);
+ canon.removeCommand(tsnDeep);
+ canon.removeCommand(tsnDeepDown);
+ canon.removeCommand(tsnDeepDownNested);
+ canon.removeCommand(tsnDeepDownNestedCmd);
+ canon.removeCommand(tselarr);
+ canon.removeCommand(tsm);
+ canon.removeCommand(tsg);
+ canon.removeCommand(tshidden);
+ canon.removeCommand(tscook);
+ canon.removeCommand(tslong);
+ canon.removeCommand(tsdate);
+ canon.removeCommand(tsfail);
+
+ types.removeType(mockCommands.optionType);
+ types.removeType(mockCommands.optionValue);
+
+ mockCommands.commands = {};
+};
+
+
+// });
diff --git a/browser/devtools/commandline/test/moz.build b/browser/devtools/commandline/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/commandline/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/debugger/CmdDebugger.jsm b/browser/devtools/debugger/CmdDebugger.jsm
new file mode 100644
index 000000000..397ecb39f
--- /dev/null
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -0,0 +1,454 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+/**
+ * Utility to get access to the current breakpoint list
+ * @param dbg The debugger panel
+ * @returns an array of object, one for each breakpoint, where each breakpoint
+ * object has the following properties:
+ * - id: A unique identifier for the breakpoint. This is not designed to be
+ * shown to the user.
+ * - label: A unique string identifier designed to be user visible. In theory
+ * the label of a breakpoint could change
+ * - url: The URL of the source file
+ * - 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.panelWin.DebuggerView.Sources;
+ let { trimUrlLength: tr } = dbg.panelWin.SourceUtils;
+
+ for (let source in sources) {
+ for (let { attachment: breakpoint } in source) {
+ breakpoints.push({
+ id: source.value + ":" + breakpoint.lineNumber,
+ label: source.label + ":" + breakpoint.lineNumber,
+ url: source.value,
+ lineNumber: breakpoint.lineNumber,
+ lineText: breakpoint.lineText,
+ truncatedLineText: tr(breakpoint.lineText, MAX_LINE_TEXT_LENGTH, "end")
+ });
+ }
+ }
+
+ return breakpoints;
+}
+
+/**
+ * 'break' command
+ */
+gcli.addCommand({
+ name: "break",
+ description: gcli.lookup("breakDesc"),
+ manual: gcli.lookup("breakManual")
+});
+
+/**
+ * 'break list' command
+ */
+gcli.addCommand({
+ name: "break list",
+ description: gcli.lookup("breaklistDesc"),
+ returnType: "breakpoints",
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger", { ensure_opened: true });
+ return dbg.then(function(dbg) {
+ return getAllBreakpoints(dbg);
+ });
+ }
+});
+
+gcli.addConverter({
+ 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: gcli.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}'>" +
+ " " + gcli.lookup("breaklistOutRemove") + "</span>" +
+ " </td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>" +
+ "";
+
+var MAX_LINE_TEXT_LENGTH = 30;
+var MAX_LABEL_LENGTH = 20;
+
+/**
+ * 'break add' command
+ */
+gcli.addCommand({
+ name: "break add",
+ description: gcli.lookup("breakaddDesc"),
+ manual: gcli.lookup("breakaddManual")
+});
+
+/**
+ * 'break add line' command
+ */
+gcli.addCommand({
+ name: "break add line",
+ description: gcli.lookup("breakaddlineDesc"),
+ params: [
+ {
+ name: "file",
+ type: {
+ name: "selection",
+ data: function(context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (dbg) {
+ return dbg.panelWin.DebuggerView.Sources.values;
+ }
+ return [];
+ }
+ },
+ description: gcli.lookup("breakaddlineFileDesc")
+ },
+ {
+ name: "line",
+ type: { name: "number", min: 1, step: 10 },
+ description: gcli.lookup("breakaddlineLineDesc")
+ }
+ ],
+ returnType: "string",
+ exec: function(args, context) {
+ args.type = "line";
+
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let deferred = context.defer();
+ let position = { url: args.file, line: args.line };
+ dbg.addBreakpoint(position, function(aBreakpoint, aError) {
+ if (aError) {
+ deferred.resolve(gcli.lookupFormat("breakaddFailed", [aError]));
+ return;
+ }
+ deferred.resolve(gcli.lookup("breakaddAdded"));
+ });
+ return deferred.promise;
+ }
+});
+
+/**
+ * 'break del' command
+ */
+gcli.addCommand({
+ name: "break del",
+ description: gcli.lookup("breakdelDesc"),
+ params: [
+ {
+ name: "breakpoint",
+ type: {
+ name: "selection",
+ lookup: function(context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (dbg == null) {
+ return [];
+ }
+ return getAllBreakpoints(dbg).map(breakpoint => {
+ return {
+ name: breakpoint.label,
+ value: breakpoint,
+ description: breakpoint.truncatedLineText
+ };
+ });
+ }
+ },
+ description: gcli.lookup("breakdelBreakidDesc")
+ }
+ ],
+ returnType: "string",
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let breakpoint = dbg.getBreakpoint(
+ args.breakpoint.url, args.breakpoint.lineNumber);
+
+ if (breakpoint == null) {
+ return gcli.lookup("breakNotFound");
+ }
+
+ let deferred = context.defer();
+ try {
+ dbg.removeBreakpoint(breakpoint, function() {
+ deferred.resolve(gcli.lookup("breakdelRemoved"));
+ });
+ } catch (ex) {
+ console.error('Error removing breakpoint, already removed?', ex);
+ // If the debugger has been closed already, don't scare the user.
+ deferred.resolve(gcli.lookup("breakdelRemoved"));
+ }
+ return deferred.promise;
+ }
+});
+
+/**
+ * 'dbg' command
+ */
+gcli.addCommand({
+ name: "dbg",
+ description: gcli.lookup("dbgDesc"),
+ manual: gcli.lookup("dbgManual")
+});
+
+/**
+ * 'dbg open' command
+ */
+gcli.addCommand({
+ name: "dbg open",
+ description: gcli.lookup("dbgOpen"),
+ params: [],
+ exec: function(args, context) {
+ return gDevTools.showToolbox(context.environment.target, "jsdebugger")
+ .then(() => null);
+ }
+});
+
+/**
+ * 'dbg close' command
+ */
+gcli.addCommand({
+ name: "dbg close",
+ description: gcli.lookup("dbgClose"),
+ params: [],
+ exec: function(args, context) {
+ if (!getPanel(context, "jsdebugger"))
+ return;
+
+ return gDevTools.closeToolbox(context.environment.target)
+ .then(() => null);
+ }
+});
+
+/**
+ * 'dbg interrupt' command
+ */
+gcli.addCommand({
+ name: "dbg interrupt",
+ description: gcli.lookup("dbgInterrupt"),
+ params: [],
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (!thread.paused) {
+ thread.interrupt();
+ }
+ }
+});
+
+/**
+ * 'dbg continue' command
+ */
+gcli.addCommand({
+ name: "dbg continue",
+ description: gcli.lookup("dbgContinue"),
+ params: [],
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.resume();
+ }
+ }
+});
+
+/**
+ * 'dbg step' command
+ */
+gcli.addCommand({
+ name: "dbg step",
+ description: gcli.lookup("dbgStepDesc"),
+ manual: gcli.lookup("dbgStepManual")
+});
+
+/**
+ * 'dbg step over' command
+ */
+gcli.addCommand({
+ name: "dbg step over",
+ description: gcli.lookup("dbgStepOverDesc"),
+ params: [],
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepOver();
+ }
+ }
+});
+
+/**
+ * 'dbg step in' command
+ */
+gcli.addCommand({
+ name: 'dbg step in',
+ description: gcli.lookup("dbgStepInDesc"),
+ params: [],
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepIn();
+ }
+ }
+});
+
+/**
+ * 'dbg step over' command
+ */
+gcli.addCommand({
+ name: 'dbg step out',
+ description: gcli.lookup("dbgStepOutDesc"),
+ params: [],
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ if (!dbg) {
+ return gcli.lookup("debuggerStopped");
+ }
+
+ let controller = dbg._controller;
+ let thread = controller.activeThread;
+ if (thread.paused) {
+ thread.stepOut();
+ }
+ }
+});
+
+/**
+ * 'dbg list' command
+ */
+gcli.addCommand({
+ name: "dbg list",
+ description: gcli.lookup("dbgListSourcesDesc"),
+ params: [],
+ returnType: "dom",
+ exec: function(args, context) {
+ let dbg = getPanel(context, "jsdebugger");
+ let doc = context.environment.chromeDocument;
+ if (!dbg) {
+ return gcli.lookup("debuggerClosed");
+ }
+
+ let sources = dbg._view.Sources.values;
+ let div = createXHTMLElement(doc, "div");
+ let ol = createXHTMLElement(doc, "ol");
+ sources.forEach(function(src) {
+ let li = createXHTMLElement(doc, "li");
+ li.textContent = src;
+ ol.appendChild(li);
+ });
+ div.appendChild(ol);
+
+ return div;
+ }
+});
+
+/**
+ * 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 == null) {
+ return undefined;
+ }
+
+ let target = context.environment.target;
+ if (options.ensure_opened) {
+ return gDevTools.showToolbox(target, id).then(function(toolbox) {
+ return toolbox.getPanel(id);
+ });
+ } else {
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ return toolbox.getPanel(id);
+ } else {
+ return undefined;
+ }
+ }
+}
diff --git a/browser/devtools/debugger/DebuggerPanel.jsm b/browser/devtools/debugger/DebuggerPanel.jsm
new file mode 100644
index 000000000..bd25b740f
--- /dev/null
+++ b/browser/devtools/debugger/DebuggerPanel.jsm
@@ -0,0 +1,101 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+this.EXPORTED_SYMBOLS = ["DebuggerPanel"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+this.DebuggerPanel = function DebuggerPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ this._view = this.panelWin.DebuggerView;
+ this._controller = this.panelWin.DebuggerController;
+ this._controller._target = this.target;
+ this._bkp = this._controller.Breakpoints;
+
+ this.highlightWhenPaused = this.highlightWhenPaused.bind(this);
+ this.unhighlightWhenResumed = this.unhighlightWhenResumed.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+DebuggerPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A Promise that is resolved when the Debugger completes opening.
+ */
+ open: function DebuggerPanel_open() {
+ let promise;
+
+ // Local debugging needs to make the target remote.
+ if (!this.target.isRemote) {
+ promise = this.target.makeRemote();
+ } else {
+ promise = Promise.resolve(this.target);
+ }
+
+ return promise
+ .then(() => this._controller.startupDebugger())
+ .then(() => this._controller.connect())
+ .then(() => {
+ this.target.on("thread-paused", this.highlightWhenPaused);
+ this.target.on("thread-resumed", this.unhighlightWhenResumed);
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ Cu.reportError("DebuggerPanel open failed. " +
+ reason.error + ": " + reason.message);
+ });
+ },
+
+ // DevToolPanel API
+ get target() this._toolbox.target,
+
+ destroy: function() {
+ this.target.off("thread-paused", this.highlightWhenPaused);
+ this.target.off("thread-resumed", this.unhighlightWhenResumed);
+ this.emit("destroyed");
+ return Promise.resolve(null);
+ },
+
+ // DebuggerPanel API
+
+ addBreakpoint: function() {
+ this._bkp.addBreakpoint.apply(this._bkp, arguments);
+ },
+
+ removeBreakpoint: function() {
+ this._bkp.removeBreakpoint.apply(this._bkp, arguments);
+ },
+
+ getBreakpoint: function() {
+ return this._bkp.getBreakpoint.apply(this._bkp, arguments);
+ },
+
+ getAllBreakpoints: function() {
+ return this._bkp.store;
+ },
+
+ highlightWhenPaused: function() {
+ this._toolbox.highlightTool("jsdebugger");
+ },
+
+ unhighlightWhenResumed: function() {
+ this._toolbox.unhighlightTool("jsdebugger");
+ }
+};
diff --git a/browser/devtools/debugger/DebuggerProcess.jsm b/browser/devtools/debugger/DebuggerProcess.jsm
new file mode 100644
index 000000000..962613836
--- /dev/null
+++ b/browser/devtools/debugger/DebuggerProcess.jsm
@@ -0,0 +1,164 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const DBG_XUL = "chrome://browser/content/devtools/debugger.xul";
+const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+this.EXPORTED_SYMBOLS = ["BrowserDebuggerProcess"];
+
+/**
+ * Constructor for creating a process that will hold a chrome debugger.
+ *
+ * @param function aOnClose [optional]
+ * A function called when the process stops running.
+ * @param function aOnRun [optional]
+ * A function called when the process starts running.
+ */
+this.BrowserDebuggerProcess = function BrowserDebuggerProcess(aOnClose, aOnRun) {
+ this._closeCallback = aOnClose;
+ this._runCallback = aOnRun;
+ this._telemetry = new Telemetry();
+
+ this._initServer();
+ this._initProfile();
+ this._create();
+}
+
+/**
+ * Initializes and starts a chrome debugger process.
+ * @return object
+ */
+BrowserDebuggerProcess.init = function(aOnClose, aOnRun) {
+ return new BrowserDebuggerProcess(aOnClose, aOnRun);
+};
+
+BrowserDebuggerProcess.prototype = {
+ /**
+ * Initializes the debugger server.
+ */
+ _initServer: function() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.openListener(Prefs.chromeDebuggingPort);
+ },
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ _initProfile: function() {
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"]
+ .createInstance(Ci.nsIToolkitProfileService);
+
+ let profileName;
+ try {
+ // Attempt to get the required chrome debugging profile name string.
+ profileName = profileService.selectedProfile.name + CHROME_DEBUGGER_PROFILE_NAME;
+ } catch (e) {
+ // Requested profile string could not be retrieved.
+ profileName = CHROME_DEBUGGER_PROFILE_NAME;
+ let msg = "Querying the current profile failed. " + e.name + ": " + e.message;
+ dumpn(msg);
+ Cu.reportError(msg);
+ }
+
+ let profileObject;
+ try {
+ // Attempt to get the required chrome debugging profile toolkit object.
+ profileObject = profileService.getProfileByName(profileName);
+
+ // The profile exists but the corresponding folder may have been deleted.
+ var enumerator = Services.dirsvc.get("ProfD", Ci.nsIFile).parent.directoryEntries;
+ while (enumerator.hasMoreElements()) {
+ let profileDir = enumerator.getNext().QueryInterface(Ci.nsIFile);
+ if (profileDir.leafName.contains(profileName)) {
+ // Requested profile was found and the folder exists.
+ this._dbgProfile = profileObject;
+ return;
+ }
+ }
+ // Requested profile was found but the folder was deleted. Cleanup needed.
+ profileObject.remove(true);
+ } catch (e) {
+ // Requested profile object was not found.
+ let msg = "Creating a profile failed. " + e.name + ": " + e.message;
+ dumpn(msg);
+ Cu.reportError(msg);
+ }
+
+ // Create a new chrome debugging profile.
+ this._dbgProfile = profileService.createProfile(null, null, profileName);
+ profileService.flush();
+ },
+
+ /**
+ * 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));
+
+ dumpn("Running chrome debugging process.");
+ let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", DBG_XUL];
+ process.runwAsync(args, args.length, { observe: () => this.close() });
+
+ this._telemetry.toolOpened("jsbrowserdebugger");
+
+ dumpn("Chrome debugger is now running...");
+ if (typeof this._runCallback == "function") {
+ this._runCallback.call({}, this);
+ }
+ },
+
+ /**
+ * Closes the remote debugger, removing the profile and killing the process.
+ */
+ close: function() {
+ if (this._dbgProcess.isRunning) {
+ dumpn("Killing chrome debugging process...");
+ this._dbgProcess.kill();
+ }
+
+ this._telemetry.toolClosed("jsbrowserdebugger");
+
+ dumpn("Chrome debugger is now closed...");
+ if (typeof this._closeCallback == "function") {
+ this._closeCallback.call({}, this);
+ }
+ }
+};
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+ chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+ chromeDebuggingPort: ["Int", "chrome-debugging-port"]
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/browser/devtools/debugger/Makefile.in b/browser/devtools/debugger/Makefile.in
new file mode 100644
index 000000000..42f17d7fe
--- /dev/null
+++ b/browser/devtools/debugger/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js
new file mode 100644
index 000000000..a3f86c592
--- /dev/null
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -0,0 +1,1540 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
+const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
+const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const FETCH_SOURCE_RESPONSE_DELAY = 50; // ms
+const FRAME_STEP_CLEAR_DELAY = 100; // ms
+const CALL_STACK_PAGE_SIZE = 25; // frames
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Parser",
+ "resource:///modules/devtools/Parser.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+/**
+ * Object defining the debugger controller components.
+ */
+let DebuggerController = {
+ /**
+ * Initializes the debugger controller.
+ */
+ initialize: function() {
+ dumpn("Initializing the DebuggerController");
+
+ this.startupDebugger = this.startupDebugger.bind(this);
+ this.shutdownDebugger = this.shutdownDebugger.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+
+ // Chrome debugging lives in a different process and needs to handle
+ // debugger startup and shutdown by itself.
+ if (window._isChromeDebugger) {
+ window.addEventListener("DOMContentLoaded", this.startupDebugger, true);
+ window.addEventListener("unload", this.shutdownDebugger, true);
+ }
+ },
+
+ /**
+ * Initializes the view.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes startup.
+ */
+ startupDebugger: function() {
+ if (this._isInitialized) {
+ return this._startup.promise;
+ }
+ this._isInitialized = true;
+
+ // Chrome debugging lives in a different process and needs to handle
+ // debugger startup by itself.
+ if (window._isChromeDebugger) {
+ window.removeEventListener("DOMContentLoaded", this.startupDebugger, true);
+ }
+
+ let deferred = this._startup = Promise.defer();
+
+ DebuggerView.initialize(() => {
+ DebuggerView._isInitialized = true;
+
+ // Chrome debugging needs to initiate the connection by itself.
+ if (window._isChromeDebugger) {
+ this.connect().then(deferred.resolve);
+ } else {
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Destroys the view and disconnects the debugger client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes shutdown.
+ */
+ shutdownDebugger: function() {
+ if (this._isDestroyed) {
+ return this._shutdown.promise;
+ }
+ this._isDestroyed = true;
+ this._startup = null;
+
+ // Chrome debugging lives in a different process and needs to handle
+ // debugger shutdown by itself.
+ if (window._isChromeDebugger) {
+ window.removeEventListener("unload", this.shutdownDebugger, true);
+ }
+
+ let deferred = this._shutdown = Promise.defer();
+
+ DebuggerView.destroy(() => {
+ DebuggerView._isDestroyed = true;
+
+ this.SourceScripts.disconnect();
+ this.StackFrames.disconnect();
+ this.ThreadState.disconnect();
+
+ this.disconnect();
+ deferred.resolve();
+
+ // Chrome debugging needs to close its parent process on shutdown.
+ window._isChromeDebugger && this._quitApp();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Initiates remote or chrome debugging based on the current target,
+ * wiring event handlers as necessary.
+ *
+ * In case of a chrome debugger living in a different process, a socket
+ * connection pipe is opened as well.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes connecting.
+ */
+ connect: function() {
+ if (this._connection) {
+ return this._connection.promise;
+ }
+
+ let deferred = this._connection = Promise.defer();
+
+ if (!window._isChromeDebugger) {
+ let target = this._target;
+ let { client, form, threadActor } = target;
+ target.on("close", this._onTabDetached);
+ target.on("navigate", this._onTabNavigated);
+ target.on("will-navigate", this._onTabNavigated);
+
+ if (target.chrome) {
+ this._startChromeDebugging(client, form.chromeDebugger, deferred.resolve);
+ } else {
+ this._startDebuggingTab(client, threadActor, deferred.resolve);
+ }
+
+ return deferred.promise;
+ }
+
+ // Chrome debugging needs to make its own connection to the debuggee.
+ let transport = debuggerSocketConnect(
+ Prefs.chromeDebuggingHost, Prefs.chromeDebuggingPort);
+
+ let client = new DebuggerClient(transport);
+ client.addListener("tabNavigated", this._onTabNavigated);
+ client.addListener("tabDetached", this._onTabDetached);
+
+ client.connect((aType, aTraits) => {
+ client.listTabs((aResponse) => {
+ this._startChromeDebugging(client, aResponse.chromeDebugger, deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * 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;
+ }
+
+ // When debugging local or a remote instance, the connection is closed by
+ // the RemoteTarget. Chrome debugging needs to specifically close its own
+ // connection to the debuggee.
+ if (window._isChromeDebugger) {
+ this.client.removeListener("tabNavigated", this._onTabNavigated);
+ this.client.removeListener("tabDetached", this._onTabDetached);
+ this.client.close();
+ }
+
+ this._connection = null;
+ this.client = null;
+ this.activeThread = null;
+ },
+
+ /**
+ * Called for each location change in the debugged tab.
+ *
+ * @param string aType
+ * Packet type.
+ * @param object aPacket
+ * Packet received from the server.
+ */
+ _onTabNavigated: function(aType, aPacket) {
+ if (aType == "will-navigate") {
+ DebuggerView._handleTabNavigation();
+
+ // Discard all the old sources.
+ DebuggerController.Parser.clearCache();
+ SourceUtils.clearCache();
+ return;
+ }
+
+ this.ThreadState._handleTabNavigation();
+ this.StackFrames._handleTabNavigation();
+ this.SourceScripts._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);
+ }
+ },
+
+ /**
+ * Sets up a debugging session.
+ *
+ * @param DebuggerClient aClient
+ * The debugger client.
+ * @param string aThreadActor
+ * The remote protocol grip of the tab.
+ * @param function aCallback
+ * A function to invoke once the client attached to the active thread.
+ */
+ _startDebuggingTab: function(aClient, aThreadActor, aCallback) {
+ if (!aClient) {
+ Cu.reportError("No client found!");
+ return;
+ }
+ this.client = aClient;
+
+ aClient.attachThread(aThreadActor, (aResponse, aThreadClient) => {
+ if (!aThreadClient) {
+ Cu.reportError("Couldn't attach to thread: " + aResponse.error);
+ return;
+ }
+ this.activeThread = aThreadClient;
+
+ this.ThreadState.connect();
+ this.StackFrames.connect();
+ this.SourceScripts.connect();
+ aThreadClient.resume(this._ensureResumptionOrder);
+
+ if (aCallback) {
+ aCallback();
+ }
+ }, { useSourceMaps: Prefs.sourceMapsEnabled });
+ },
+
+ /**
+ * Sets up a chrome debugging session.
+ *
+ * @param DebuggerClient aClient
+ * The debugger client.
+ * @param object aChromeDebugger
+ * The remote protocol grip of the chrome debugger.
+ * @param function aCallback
+ * A function to invoke once the client attached to the active thread.
+ */
+ _startChromeDebugging: function(aClient, aChromeDebugger, aCallback) {
+ if (!aClient) {
+ Cu.reportError("No client found!");
+ return;
+ }
+ this.client = aClient;
+
+ aClient.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
+ if (!aThreadClient) {
+ Cu.reportError("Couldn't attach to thread: " + aResponse.error);
+ return;
+ }
+ this.activeThread = aThreadClient;
+
+ this.ThreadState.connect();
+ this.StackFrames.connect();
+ this.SourceScripts.connect();
+ aThreadClient.resume(this._ensureResumptionOrder);
+
+ if (aCallback) {
+ aCallback();
+ }
+ }, { useSourceMaps: Prefs.sourceMapsEnabled });
+ },
+
+ /**
+ * Detach and reattach to the thread actor with useSourceMaps true, blow
+ * away old scripts and get sources again.
+ */
+ reconfigureThread: function(aUseSourceMaps) {
+ this.client.reconfigureThread(aUseSourceMaps, (aResponse) => {
+ if (aResponse.error) {
+ let msg = "Couldn't reconfigure thread: " + aResponse.message;
+ Cu.reportError(msg);
+ dumpn(msg);
+ return;
+ }
+
+ DebuggerView._handleTabNavigation();
+ this.SourceScripts._handleTabNavigation();
+
+ // Update the stack frame list.
+ this.activeThread._clearFrames();
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ });
+ },
+
+ /**
+ * Attempts to quit the current process if allowed.
+ */
+ _quitApp: function() {
+ let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+
+ Services.obs.notifyObservers(canceled, "quit-application-requested", null);
+
+ // Somebody canceled our quit request.
+ if (canceled.data) {
+ return;
+ }
+ Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
+ },
+
+ _isInitialized: false,
+ _isDestroyed: false,
+ _startup: null,
+ _shutdown: null,
+ _connection: null,
+ client: null,
+ activeThread: null
+};
+
+/**
+ * ThreadState keeps the UI up to date with the state of the
+ * thread (paused/attached/etc.).
+ */
+function ThreadState() {
+ this._update = this._update.bind(this);
+}
+
+ThreadState.prototype = {
+ get activeThread() 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);
+ this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions);
+ this._handleTabNavigation();
+ },
+
+ /**
+ * 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) {
+ DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
+
+ if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
+ gTarget.emit("thread-" + aEvent);
+ }
+ }
+};
+
+/**
+ * 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._afterFramesCleared = this._afterFramesCleared.bind(this);
+ this.evaluate = this.evaluate.bind(this);
+}
+
+StackFrames.prototype = {
+ get activeThread() DebuggerController.activeThread,
+ autoScopeExpand: false,
+ currentFrame: null,
+ 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._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);
+ },
+
+ /**
+ * 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;
+ // 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;
+ }
+
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ DebuggerView.editor.focus();
+ },
+
+ /**
+ * Handler for the thread client's resumed notification.
+ */
+ _onResumed: function() {
+ DebuggerView.editor.setDebugLocation(-1);
+
+ // Prepare the watch expression evaluation string for the next pause.
+ if (!this._isWatchExpressionsEvaluation) {
+ this.currentWatchExpressions = this.syncedWatchExpressions;
+ }
+ },
+
+ /**
+ * Handler for the thread client's framesadded notification.
+ */
+ _onFrames: function() {
+ // Ignore useless notifications.
+ if (!this.activeThread.cachedFrames.length) {
+ return;
+ }
+
+ // Conditional breakpoints are { breakpoint, expression } tuples. The
+ // boolean evaluation of the expression decides if the active thread
+ // automatically resumes execution or not.
+ // TODO: handle all of this server-side: Bug 812172.
+ if (this.currentBreakpointLocation) {
+ let { url, line } = this.currentBreakpointLocation;
+ let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
+ if (breakpointClient) {
+ // Make sure a breakpoint actually exists at the specified url and line.
+ let conditionalExpression = breakpointClient.conditionalExpression;
+ if (conditionalExpression) {
+ // 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.
+ this.evaluate(conditionalExpression, 0);
+ this._isConditionalBreakpointEvaluation = true;
+ return;
+ }
+ }
+ }
+ // Got our evaluation of the current breakpoint's conditional expression.
+ if (this._isConditionalBreakpointEvaluation) {
+ this._isConditionalBreakpointEvaluation = false;
+ // If the breakpoint's conditional expression evaluation is falsy,
+ // automatically resume execution.
+ if (VariablesView.isFalsy({ value: this.currentEvaluation.return })) {
+ this.activeThread.resume(DebuggerController._ensureResumptionOrder);
+ return;
+ }
+ }
+
+
+ // Watch expressions are evaluated in the context of the topmost frame,
+ // and the results are displayed in the variables view.
+ // TODO: handle all of this server-side: Bug 832470, comment 14.
+ if (this.currentWatchExpressions) {
+ // Evaluation causes the stack frames to be cleared and active thread to
+ // pause, sending a 'clientEvaluated' packet and adding the frames again.
+ this.evaluate(this.currentWatchExpressions, 0);
+ this._isWatchExpressionsEvaluation = true;
+ return;
+ }
+ // Got our evaluation of the current watch expressions.
+ if (this._isWatchExpressionsEvaluation) {
+ this._isWatchExpressionsEvaluation = false;
+ // If an error was thrown during the evaluation of the watch expressions,
+ // then at least one expression evaluation could not be performed. So
+ // remove the most recent watch expression and try again.
+ if (this.currentEvaluation.throw) {
+ DebuggerView.WatchExpressions.removeAt(0);
+ DebuggerController.StackFrames.syncWatchExpressions();
+ return;
+ }
+ }
+
+
+ // Make sure the debugger view panes are visible.
+ DebuggerView.showInstrumentsPane();
+
+ // Make sure all the previous stackframes are removed before re-adding them.
+ DebuggerView.StackFrames.empty();
+
+ for (let frame of this.activeThread.cachedFrames) {
+ let { depth, where: { url, line } } = frame;
+ let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
+ let frameTitle = StackFrameUtils.getFrameTitle(frame);
+
+ DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
+ }
+ if (this.currentFrame == null) {
+ DebuggerView.StackFrames.selectedDepth = 0;
+ }
+ if (this.activeThread.moreFrames) {
+ DebuggerView.StackFrames.dirty = true;
+ }
+ },
+
+ /**
+ * Handler for the thread client's framescleared notification.
+ */
+ _onFramesCleared: function() {
+ this.currentFrame = null;
+ this.currentWatchExpressions = null;
+ this.currentBreakpointLocation = null;
+ this.currentEvaluation = null;
+ this.currentException = null;
+ this.currentReturnedValue = null;
+ // 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.
+ window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
+ },
+
+ /**
+ * Called soon after the thread client's framescleared notification.
+ */
+ _afterFramesCleared: function() {
+ // Ignore useless notifications.
+ if (this.activeThread.cachedFrames.length) {
+ return;
+ }
+ DebuggerView.StackFrames.empty();
+ DebuggerView.Sources.unhighlightBreakpoint();
+ DebuggerView.WatchExpressions.toggleContents(true);
+ DebuggerView.Variables.empty(0);
+ window.dispatchEvent(document, "Debugger:AfterFramesCleared");
+ },
+
+ /**
+ * 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.currentFrame = aDepth];
+ if (!frame) {
+ return;
+ }
+
+ // Check if the frame does not represent the evaluation of debuggee code.
+ let { environment, where: { url, line } } = frame;
+ if (!environment) {
+ return;
+ }
+
+ // Move the editor's caret to the proper url and line.
+ DebuggerView.updateEditor(url, line);
+ // Highlight the breakpoint at the specified url and line if it exists.
+ DebuggerView.Sources.highlightBreakpoint(url, line);
+ // 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.
+ DebuggerView.Variables.createHierarchy();
+ // Clear existing scopes and 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);
+
+ // Customize the scope for holding watch expressions evaluations.
+ scope.descriptorTooltip = false;
+ scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
+ scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel");
+ 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.
+ if (innermost || this.autoScopeExpand) {
+ scope.expand();
+ }
+ } while ((environment = environment.parent));
+
+ // Signal that variables have been fetched.
+ window.dispatchEvent(document, "Debugger:FetchedVariables");
+ DebuggerView.Variables.commitHierarchy();
+ },
+
+ /**
+ * 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. This is used
+ * for modifying the value of variables or properties in scope.
+ *
+ * @param string aExpression
+ * The expression to evaluate.
+ * @param number aFrame [optional]
+ * The frame depth used for evaluation.
+ */
+ evaluate: function(aExpression, aFrame = this.currentFrame || 0) {
+ let frame = this.activeThread.cachedFrames[aFrame];
+ this.activeThread.eval(frame.actor, aExpression);
+ },
+
+ /**
+ * 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 });
+ DebuggerView.Variables.controller.addExpander(excRef, this.currentException);
+ }
+ // Add any returned value.
+ if (this.currentReturnedValue) {
+ let retRef = aScope.addItem("<return>", { value: this.currentReturnedValue });
+ 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);
+ }
+ },
+
+ /**
+ * 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.dispatchEvent(document, "Debugger:FetchedWatchExpressions");
+ DebuggerView.Variables.commitHierarchy();
+ });
+ },
+
+ /**
+ * 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.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, "\\$&") + "\" + " + "'\\n'" + " + \"" +
+ "} catch (e) {" +
+ "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
+ "}" +
+ "\")"
+ ).join(",") +
+ "]";
+ } else {
+ this.syncedWatchExpressions =
+ this.currentWatchExpressions = null;
+ }
+ this.currentFrame = null;
+ this._onFrames();
+ }
+};
+
+/**
+ * Keeps the source script list up-to-date, using the thread client's
+ * source script cache.
+ */
+function SourceScripts() {
+ this._onNewGlobal = this._onNewGlobal.bind(this);
+ this._onNewSource = this._onNewSource.bind(this);
+ this._onSourcesAdded = this._onSourcesAdded.bind(this);
+}
+
+SourceScripts.prototype = {
+ get activeThread() DebuggerController.activeThread,
+ get debuggerClient() DebuggerController.client,
+ _newSourceTimeout: null,
+
+ /**
+ * Connect to the current thread client.
+ */
+ connect: function() {
+ dumpn("SourceScripts is connecting...");
+ this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
+ this.debuggerClient.addListener("newSource", this._onNewSource);
+ this._handleTabNavigation();
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function() {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("SourceScripts is disconnecting...");
+ window.clearTimeout(this._newSourceTimeout);
+ this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
+ this.debuggerClient.removeListener("newSource", this._onNewSource);
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ _handleTabNavigation: function() {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("Handling tab navigation in the SourceScripts");
+ window.clearTimeout(this._newSourceTimeout);
+
+ // Retrieve the list of script sources known to the server from before
+ // the client was ready to handle "newSource" notifications.
+ this.activeThread.getSources(this._onSourcesAdded);
+ },
+
+ /**
+ * Handler for the debugger client's unsolicited newGlobal notification.
+ */
+ _onNewGlobal: function(aNotification, aPacket) {
+ // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
+ // from bug 801084.
+ },
+
+ /**
+ * Handler for the debugger client's unsolicited newSource notification.
+ */
+ _onNewSource: function(aNotification, aPacket) {
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) {
+ return;
+ }
+
+ // Add the source in the debugger view sources container.
+ DebuggerView.Sources.addSource(aPacket.source, { staged: false });
+
+ let container = DebuggerView.Sources;
+ let preferredValue = container.preferredValue;
+
+ // Select this source if it's the preferred one.
+ if (aPacket.source.url == preferredValue) {
+ container.selectedValue = preferredValue;
+ }
+ // ..or the first entry if there's none selected yet after a while
+ else {
+ window.clearTimeout(this._newSourceTimeout);
+ this._newSourceTimeout = window.setTimeout(() => {
+ // If after a certain delay the preferred source still wasn't received,
+ // just give up on waiting and display the first entry.
+ if (!container.selectedValue) {
+ container.selectedIndex = 0;
+ }
+ }, NEW_SOURCE_DISPLAY_DELAY);
+ }
+
+ // If there are any stored breakpoints for this source, display them again,
+ // both in the editor and the breakpoints pane.
+ DebuggerController.Breakpoints.updateEditorBreakpoints();
+ DebuggerController.Breakpoints.updatePaneBreakpoints();
+
+ // Signal that a new script has been added.
+ window.dispatchEvent(document, "Debugger:AfterNewSource");
+ },
+
+ /**
+ * Callback for the debugger's active thread getSources() method.
+ */
+ _onSourcesAdded: function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("Error getting sources: " + aResponse.message);
+ return;
+ }
+
+ // Add all the sources in the debugger view sources container.
+ for (let source of aResponse.sources) {
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) != -1) {
+ continue;
+ }
+ DebuggerView.Sources.addSource(source, { staged: true });
+ }
+
+ let container = DebuggerView.Sources;
+ let preferredValue = container.preferredValue;
+
+ // Flushes all the prepared sources into the sources container.
+ container.commit({ sorted: true });
+
+ // Select the preferred source if it exists and was part of the response.
+ if (container.containsValue(preferredValue)) {
+ container.selectedValue = preferredValue;
+ }
+ // ..or the first entry if there's no one selected yet.
+ else if (!container.selectedValue) {
+ container.selectedIndex = 0;
+ }
+
+ // If there are any stored breakpoints for the sources, display them again,
+ // both in the editor and the breakpoints pane.
+ DebuggerController.Breakpoints.updateEditorBreakpoints();
+ DebuggerController.Breakpoints.updatePaneBreakpoints();
+
+ // Signal that scripts have been added.
+ window.dispatchEvent(document, "Debugger:AfterSourcesAdded");
+ },
+
+ /**
+ * Gets a specified source's text.
+ *
+ * @param object aSource
+ * The source object coming from the active thread.
+ * @param function aOnTimeout [optional]
+ * Function called when the source text takes a long time to fetch,
+ * but not necessarily failing. Long fetch times don't cause the
+ * rejection of the returned promise.
+ * @param number aDelay [optional]
+ * The amount of time it takes to consider a source slow to fetch.
+ * If unspecified, it defaults to a predefined value.
+ * @return object
+ * A promise that is resolved after the source text has been fetched.
+ */
+ getTextForSource: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) {
+ // Fetch the source text only once.
+ if (aSource._fetched) {
+ return aSource._fetched;
+ }
+
+ let deferred = Promise.defer();
+ aSource._fetched = deferred.promise;
+
+ // If the source text takes a long time to fetch, invoke a callback.
+ if (aOnTimeout) {
+ var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
+ }
+
+ // Get the source text from the active thread.
+ this.activeThread.source(aSource).source((aResponse) => {
+ if (aOnTimeout) {
+ window.clearTimeout(fetchTimeout);
+ }
+ if (aResponse.error) {
+ deferred.reject([aSource, aResponse.message]);
+ } else {
+ deferred.resolve([aSource, aResponse.source]);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * 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.
+ */
+ getTextForSources: function(aUrls) {
+ let deferred = Promise.defer();
+ let pending = new Set(aUrls);
+ 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 url of aUrls) {
+ let sourceItem = DebuggerView.Sources.getItemByValue(url);
+ let sourceClient = sourceItem.attachment.source;
+ this.getTextForSource(sourceClient, onTimeout).then(onFetch, onError);
+ }
+
+ /* Called if fetching a source takes too long. */
+ function onTimeout(aSource) {
+ onError([aSource]);
+ }
+
+ /* Called if fetching a source finishes successfully. */
+ function onFetch([aSource, aText]) {
+ // If fetching the source has previously timed out, discard it this time.
+ if (!pending.has(aSource.url)) {
+ return;
+ }
+ pending.delete(aSource.url);
+ fetched.push([aSource.url, aText]);
+ maybeFinish();
+ }
+
+ /* Called if fetching a source failed because of an error. */
+ function onError([aSource, aError]) {
+ pending.delete(aSource.url);
+ maybeFinish();
+ }
+
+ /* Called every time something interesting happens while fetching sources. */
+ function maybeFinish() {
+ if (pending.size == 0) {
+ deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
+ }
+ }
+
+ return deferred.promise;
+ }
+};
+
+/**
+ * Handles all the breakpoints in the current debugger.
+ */
+function Breakpoints() {
+ this._onEditorBreakpointChange = this._onEditorBreakpointChange.bind(this);
+ this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
+ this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
+ this.addBreakpoint = this.addBreakpoint.bind(this);
+ this.removeBreakpoint = this.removeBreakpoint.bind(this);
+ this.getBreakpoint = this.getBreakpoint.bind(this);
+}
+
+Breakpoints.prototype = {
+ get activeThread() DebuggerController.ThreadState.activeThread,
+ get editor() DebuggerView.editor,
+
+ /**
+ * The list of breakpoints in the debugger as tracked by the current
+ * debugger instance. This is an object where the values are BreakpointActor
+ * objects received from the client, while the keys are actor names, for
+ * example "conn0.breakpoint3".
+ */
+ store: {},
+
+ /**
+ * Skip editor breakpoint change events.
+ *
+ * This property tells the source editor event handler to skip handling of
+ * the BREAKPOINT_CHANGE events. This is used when the debugger adds/removes
+ * breakpoints from the editor. Typically, the BREAKPOINT_CHANGE event handler
+ * adds/removes events from the debugger, but when breakpoints are added from
+ * the public debugger API, we need to do things in reverse.
+ *
+ * This implementation relies on the fact that the source editor fires the
+ * BREAKPOINT_CHANGE events synchronously.
+ */
+ _skipEditorBreakpointCallbacks: false,
+
+ /**
+ * Adds the source editor breakpoint handlers.
+ */
+ initialize: function() {
+ this.editor.addEventListener(
+ SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
+ },
+
+ /**
+ * Removes the source editor breakpoint handlers & all the added breakpoints.
+ */
+ destroy: function() {
+ this.editor.removeEventListener(
+ SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
+
+ for each (let breakpointClient in this.store) {
+ this.removeBreakpoint(breakpointClient);
+ }
+ },
+
+ /**
+ * Event handler for breakpoint changes that happen in the editor. This
+ * function syncs the breakpoints in the editor to those in the debugger.
+ *
+ * @param object aEvent
+ * The SourceEditor.EVENTS.BREAKPOINT_CHANGE event object.
+ */
+ _onEditorBreakpointChange: function(aEvent) {
+ if (this._skipEditorBreakpointCallbacks) {
+ return;
+ }
+ this._skipEditorBreakpointCallbacks = true;
+ aEvent.added.forEach(this._onEditorBreakpointAdd, this);
+ aEvent.removed.forEach(this._onEditorBreakpointRemove, this);
+ this._skipEditorBreakpointCallbacks = false;
+ },
+
+ /**
+ * Event handler for new breakpoints that come from the editor.
+ *
+ * @param object aEditorBreakpoint
+ * The breakpoint object coming from the editor.
+ */
+ _onEditorBreakpointAdd: function(aEditorBreakpoint) {
+ let url = DebuggerView.Sources.selectedValue;
+ let line = aEditorBreakpoint.line + 1;
+
+ this.addBreakpoint({ url: url, line: line }, (aBreakpointClient) => {
+ // If the breakpoint client has an "actualLocation" attached, then
+ // the original requested placement for the breakpoint wasn't accepted.
+ // In this case, we need to update the editor with the new location.
+ if (aBreakpointClient.actualLocation) {
+ this.editor.removeBreakpoint(line - 1);
+ this.editor.addBreakpoint(aBreakpointClient.actualLocation.line - 1);
+ }
+ });
+ },
+
+ /**
+ * Event handler for breakpoints that are removed from the editor.
+ *
+ * @param object aEditorBreakpoint
+ * The breakpoint object that was removed from the editor.
+ */
+ _onEditorBreakpointRemove: function(aEditorBreakpoint) {
+ let url = DebuggerView.Sources.selectedValue;
+ let line = aEditorBreakpoint.line + 1;
+
+ this.removeBreakpoint(this.getBreakpoint(url, line));
+ },
+
+ /**
+ * Update the breakpoints in the editor view. This function takes the list of
+ * breakpoints in the debugger and adds them back into the editor view.
+ * This is invoked when the selected script is changed, or when new sources
+ * are received via the _onNewSource and _onSourcesAdded event listeners.
+ */
+ updateEditorBreakpoints: function() {
+ for each (let breakpointClient in this.store) {
+ if (DebuggerView.Sources.selectedValue == breakpointClient.location.url) {
+ this._showBreakpoint(breakpointClient, {
+ noPaneUpdate: true,
+ noPaneHighlight: true
+ });
+ }
+ }
+ },
+
+ /**
+ * Update the breakpoints in the pane view. This function takes the list of
+ * breakpoints in the debugger and adds them back into the breakpoints pane.
+ * This is invoked when new sources are received via the _onNewSource and
+ * _onSourcesAdded event listeners.
+ */
+ updatePaneBreakpoints: function() {
+ for each (let breakpointClient in this.store) {
+ if (DebuggerView.Sources.containsValue(breakpointClient.location.url)) {
+ this._showBreakpoint(breakpointClient, {
+ noEditorUpdate: true,
+ noPaneHighlight: true
+ });
+ }
+ }
+ },
+
+ /**
+ * Add a breakpoint.
+ *
+ * @param object aLocation
+ * The location where you want the breakpoint. This object must have
+ * two properties:
+ * - url: the url of the source.
+ * - line: the line number (starting from 1).
+ * @param function aCallback [optional]
+ * Optional function to invoke once the breakpoint is added. The
+ * callback is invoked with two arguments:
+ * - aBreakpointClient: the BreakpointActor client object
+ * - aResponseError: if there was any error
+ * @param object aFlags [optional]
+ * An object containing some of the following boolean properties:
+ * - conditionalExpression: tells this breakpoint's conditional expression
+ * - openPopup: tells if the expression popup should be shown
+ * - noEditorUpdate: tells if you want to skip editor updates
+ * - noPaneUpdate: tells if you want to skip breakpoint pane updates
+ * - noPaneHighlight: tells if you don't want to highlight the breakpoint
+ */
+ addBreakpoint: function(aLocation, aCallback, aFlags = {}) {
+ // Make sure a proper location is available.
+ if (!aLocation) {
+ aCallback && aCallback(null, new Error("Invalid breakpoint location."));
+ return;
+ }
+ let breakpointClient = this.getBreakpoint(aLocation.url, aLocation.line);
+
+ // If the breakpoint was already added, callback immediately.
+ if (breakpointClient) {
+ aCallback && aCallback(breakpointClient);
+ return;
+ }
+
+ this.activeThread.setBreakpoint(aLocation, (aResponse, aBreakpointClient) => {
+ let { url, line } = aResponse.actualLocation || aLocation;
+
+ // If the response contains a breakpoint that exists in the cache, prevent
+ // it from being shown in the source editor at an incorrect position.
+ if (this.getBreakpoint(url, line)) {
+ this._hideBreakpoint(aBreakpointClient);
+ return;
+ }
+
+ // If the breakpoint response has an "actualLocation" attached, then
+ // the original requested placement for the breakpoint wasn't accepted.
+ if (aResponse.actualLocation) {
+ // Store the originally requested location in case it's ever needed.
+ aBreakpointClient.requestedLocation = {
+ url: aBreakpointClient.location.url,
+ line: aBreakpointClient.location.line
+ };
+ // Store the response actual location to be used.
+ aBreakpointClient.actualLocation = aResponse.actualLocation;
+ // Update the breakpoint client with the actual location.
+ aBreakpointClient.location.url = aResponse.actualLocation.url;
+ aBreakpointClient.location.line = aResponse.actualLocation.line;
+ }
+
+ // Remember the breakpoint client in the store.
+ this.store[aBreakpointClient.actor] = aBreakpointClient;
+
+ // Attach any specified conditional expression to the breakpoint client.
+ aBreakpointClient.conditionalExpression = aFlags.conditionalExpression;
+
+ // Preserve information about the breakpoint's line text, to display it
+ // in the sources pane without requiring fetching the source (for example,
+ // after the target navigated).
+ aBreakpointClient.lineText = DebuggerView.getEditorLineText(line - 1).trim();
+
+ // Show the breakpoint in the editor and breakpoints pane.
+ this._showBreakpoint(aBreakpointClient, aFlags);
+
+ // We're done here.
+ aCallback && aCallback(aBreakpointClient, aResponse.error);
+ });
+ },
+
+ /**
+ * Remove a breakpoint.
+ *
+ * @param object aBreakpointClient
+ * The BreakpointActor client object to remove.
+ * @param function aCallback [optional]
+ * Optional function to invoke once the breakpoint is removed. The
+ * callback is invoked with one argument
+ * - aBreakpointClient: the breakpoint location (url and line)
+ * @param object aFlags [optional]
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ removeBreakpoint: function(aBreakpointClient, aCallback, aFlags = {}) {
+ // Make sure a proper breakpoint client is available.
+ if (!aBreakpointClient) {
+ aCallback && aCallback(null, new Error("Invalid breakpoint client."));
+ return;
+ }
+ let breakpointActor = aBreakpointClient.actor;
+
+ // If the breakpoint was already removed, callback immediately.
+ if (!this.store[breakpointActor]) {
+ aCallback && aCallback(aBreakpointClient.location);
+ return;
+ }
+
+ aBreakpointClient.remove(() => {
+ // Delete the breakpoint client from the store.
+ delete this.store[breakpointActor];
+
+ // Hide the breakpoint from the editor and breakpoints pane.
+ this._hideBreakpoint(aBreakpointClient, aFlags);
+
+ // We're done here.
+ aCallback && aCallback(aBreakpointClient.location);
+ });
+ },
+
+ /**
+ * Update the editor and breakpoints pane to show a specified breakpoint.
+ *
+ * @param object aBreakpointClient
+ * The BreakpointActor client object to show.
+ * @param object aFlags [optional]
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _showBreakpoint: function(aBreakpointClient, aFlags = {}) {
+ let currentSourceUrl = DebuggerView.Sources.selectedValue;
+ let { url, line } = aBreakpointClient.location;
+
+ // Update the editor if required.
+ if (!aFlags.noEditorUpdate) {
+ if (url == currentSourceUrl) {
+ this._skipEditorBreakpointCallbacks = true;
+ this.editor.addBreakpoint(line - 1);
+ this._skipEditorBreakpointCallbacks = false;
+ }
+ }
+ // Update the breakpoints pane if required.
+ if (!aFlags.noPaneUpdate) {
+ DebuggerView.Sources.addBreakpoint({
+ sourceLocation: url,
+ lineNumber: line,
+ lineText: aBreakpointClient.lineText,
+ actor: aBreakpointClient.actor,
+ openPopupFlag: aFlags.openPopup
+ });
+ }
+ // Highlight the breakpoint in the pane if required.
+ if (!aFlags.noPaneHighlight) {
+ DebuggerView.Sources.highlightBreakpoint(url, line, aFlags);
+ }
+
+ // Notify that we've shown a breakpoint.
+ window.dispatchEvent(document, "Debugger:BreakpointShown", aBreakpointClient);
+ },
+
+ /**
+ * Update the editor and breakpoints pane to hide a specified breakpoint.
+ *
+ * @param object aBreakpointClient
+ * The BreakpointActor client object to hide.
+ * @param object aFlags [optional]
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _hideBreakpoint: function(aBreakpointClient, aFlags = {}) {
+ let currentSourceUrl = DebuggerView.Sources.selectedValue;
+ let { url, line } = aBreakpointClient.location;
+
+ // Update the editor if required.
+ if (!aFlags.noEditorUpdate) {
+ if (url == currentSourceUrl) {
+ this._skipEditorBreakpointCallbacks = true;
+ this.editor.removeBreakpoint(line - 1);
+ this._skipEditorBreakpointCallbacks = false;
+ }
+ }
+ // Update the breakpoints pane if required.
+ if (!aFlags.noPaneUpdate) {
+ DebuggerView.Sources.removeBreakpoint(url, line);
+ }
+
+ // Notify that we've hidden a breakpoint.
+ window.dispatchEvent(document, "Debugger:BreakpointHidden", aBreakpointClient);
+ },
+
+ /**
+ * Get the BreakpointActor client object at the given location.
+ *
+ * @param string aUrl
+ * The URL of where the breakpoint is.
+ * @param number aLine
+ * The line number where the breakpoint is.
+ * @return object
+ * The BreakpointActor client object.
+ */
+ getBreakpoint: function(aUrl, aLine) {
+ for each (let breakpointClient in this.store) {
+ if (breakpointClient.location.url == aUrl &&
+ breakpointClient.location.line == aLine) {
+ return breakpointClient;
+ }
+ }
+ return null;
+ }
+};
+
+/**
+ * Localization convenience methods.
+ */
+let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+ chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+ chromeDebuggingPort: ["Int", "chrome-debugging-port"],
+ sourcesWidth: ["Int", "ui.panes-sources-width"],
+ instrumentsWidth: ["Int", "ui.panes-instruments-width"],
+ panesVisibleOnStartup: ["Bool", "ui.panes-visible-on-startup"],
+ variablesSortingEnabled: ["Bool", "ui.variables-sorting-enabled"],
+ variablesOnlyEnumVisible: ["Bool", "ui.variables-only-enum-visible"],
+ variablesSearchboxVisible: ["Bool", "ui.variables-searchbox-visible"],
+ pauseOnExceptions: ["Bool", "pause-on-exceptions"],
+ sourceMapsEnabled: ["Bool", "source-maps-enabled"]
+});
+
+/**
+ * Returns true if this is a chrome debugger instance.
+ * @return boolean
+ */
+XPCOMUtils.defineLazyGetter(window, "_isChromeDebugger", function() {
+ // We're inside a single top level XUL window in a different process.
+ return !(window.frameElement instanceof XULElement);
+});
+
+/**
+ * Preliminary setup for the DebuggerController object.
+ */
+DebuggerController.initialize();
+DebuggerController.Parser = new Parser();
+DebuggerController.ThreadState = new ThreadState();
+DebuggerController.StackFrames = new StackFrames();
+DebuggerController.SourceScripts = new SourceScripts();
+DebuggerController.Breakpoints = new Breakpoints();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+ "dispatchEvent": {
+ get: function() ViewHelpers.dispatchEvent,
+ },
+ "editor": {
+ get: function() DebuggerView.editor
+ },
+ "gTarget": {
+ get: function() DebuggerController._target
+ },
+ "gClient": {
+ get: function() DebuggerController.client
+ },
+ "gThreadClient": {
+ get: function() DebuggerController.activeThread
+ },
+ "gThreadState": {
+ get: function() DebuggerController.ThreadState
+ },
+ "gStackFrames": {
+ get: function() DebuggerController.StackFrames
+ },
+ "gSourceScripts": {
+ get: function() DebuggerController.SourceScripts
+ },
+ "gBreakpoints": {
+ get: function() DebuggerController.Breakpoints
+ },
+ "gCallStackPageSize": {
+ get: function() CALL_STACK_PAGE_SIZE,
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/browser/devtools/debugger/debugger-panes.js b/browser/devtools/debugger/debugger-panes.js
new file mode 100644
index 000000000..8f1c934f6
--- /dev/null
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -0,0 +1,2317 @@
+/* -*- 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";
+
+/**
+ * Functions handling the sources UI.
+ */
+function SourcesView() {
+ dumpn("SourcesView was instantiated");
+
+ this._onEditorLoad = this._onEditorLoad.bind(this);
+ this._onEditorUnload = this._onEditorUnload.bind(this);
+ this._onEditorSelection = this._onEditorSelection.bind(this);
+ this._onEditorContextMenu = this._onEditorContextMenu.bind(this);
+ this._onSourceSelect = this._onSourceSelect.bind(this);
+ this._onSourceClick = this._onSourceClick.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._onConditionalTextboxInput = this._onConditionalTextboxInput.bind(this);
+ this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
+}
+
+SourcesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the SourcesView");
+
+ this.widget = new SideMenuWidget(document.getElementById("sources"));
+ this.emptyText = L10N.getStr("noSourcesText");
+ this.unavailableText = L10N.getStr("noMatchingSourcesText");
+
+ 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");
+
+ window.addEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
+ window.addEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
+ this.widget.addEventListener("select", this._onSourceSelect, false);
+ this.widget.addEventListener("click", this._onSourceClick, false);
+ this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
+ this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbTextbox.addEventListener("input", this._onConditionalTextboxInput, false);
+ this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+
+ this.autoFocusOnSelection = false;
+
+ // Show an empty label by default.
+ this.empty();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the SourcesView");
+
+ window.removeEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
+ window.removeEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
+ this.widget.removeEventListener("select", this._onSourceSelect, false);
+ this.widget.removeEventListener("click", this._onSourceClick, false);
+ this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false);
+ this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbTextbox.removeEventListener("input", this._onConditionalTextboxInput, false);
+ this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+ },
+
+ /**
+ * Sets the preferred location to be selected in this sources container.
+ * @param string aSourceLocation
+ */
+ set preferredSource(aSourceLocation) {
+ this._preferredValue = aSourceLocation;
+
+ // Selects the element with the specified value in this sources container,
+ // if already inserted.
+ if (this.containsValue(aSourceLocation)) {
+ this.selectedValue = aSourceLocation;
+ }
+ },
+
+ /**
+ * 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:
+ * - forced: force the source to be immediately added
+ */
+ addSource: function(aSource, aOptions = {}) {
+ let url = aSource.url;
+ let label = SourceUtils.getSourceLabel(url.split(" -> ").pop());
+ let group = SourceUtils.getSourceGroup(url.split(" -> ").pop());
+
+ // Append a source item to this container.
+ this.push([label, url, group], {
+ staged: aOptions.staged, /* stage the item to be appended later? */
+ attachment: {
+ source: aSource
+ }
+ });
+ },
+
+ /**
+ * Adds a breakpoint to this sources container.
+ *
+ * @param object aOptions
+ * Several options or flags supported by this operation:
+ * - string sourceLocation
+ * The breakpoint's source location.
+ * - number lineNumber
+ * The breakpoint's line number to be displayed.
+ * - string lineText
+ * The breakpoint's line text to be displayed.
+ * - string actor
+ * A breakpoint identifier specified by the debugger controller.
+ * - boolean openPopupFlag [optional]
+ * A flag specifying if the expression popup should be shown.
+ */
+ addBreakpoint: function(aOptions) {
+ let { sourceLocation: url, lineNumber: line } = aOptions;
+
+ // Make sure we're not duplicating anything. If a breakpoint at the
+ // specified source location and line number already exists, just enable it.
+ if (this.getBreakpoint(url, line)) {
+ this.enableBreakpoint(url, line, { id: aOptions.actor });
+ return;
+ }
+
+ // Get the source item to which the breakpoint should be attached.
+ let sourceItem = this.getItemByValue(url);
+
+ // Create the element node and menu popup for the breakpoint item.
+ let breakpointView = this._createBreakpointView.call(this, aOptions);
+ let contextMenu = this._createContextMenu.call(this, aOptions);
+
+ // Append a breakpoint child item to the corresponding source item.
+ let breakpointItem = sourceItem.append(breakpointView.container, {
+ attachment: Heritage.extend(aOptions, {
+ 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 this is a conditional breakpoint, display a panel to input the
+ // corresponding conditional expression.
+ if (aOptions.openPopupFlag) {
+ this.highlightBreakpoint(url, line, { openPopup: true });
+ }
+ },
+
+ /**
+ * Removes a breakpoint from this sources container.
+ *
+ * @param string aSourceLocation
+ * The breakpoint source location.
+ * @param number aLineNumber
+ * The breakpoint line number.
+ */
+ removeBreakpoint: function(aSourceLocation, aLineNumber) {
+ // When a parent source item is removed, all the child breakpoint items are
+ // also automagically removed.
+ let sourceItem = this.getItemByValue(aSourceLocation);
+ if (!sourceItem) {
+ return;
+ }
+ let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
+ if (!breakpointItem) {
+ return;
+ }
+
+ sourceItem.remove(breakpointItem);
+ },
+
+ /**
+ * Returns the breakpoint at the specified source location and line number.
+ *
+ * @param string aSourceLocation
+ * The breakpoint source location.
+ * @param number aLineNumber
+ * The breakpoint line number.
+ * @return object
+ * The corresponding breakpoint item if found, null otherwise.
+ */
+ getBreakpoint: function(aSourceLocation, aLineNumber) {
+ return this.getItemForPredicate((aItem) =>
+ aItem.attachment.sourceLocation == aSourceLocation &&
+ aItem.attachment.lineNumber == aLineNumber);
+ },
+
+ /**
+ * Enables a breakpoint.
+ *
+ * @param string aSourceLocation
+ * The breakpoint source location.
+ * @param number aLineNumber
+ * The breakpoint line number.
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - silent: pass true to not update the checkbox checked state;
+ * this is usually necessary when the checked state will
+ * be updated automatically (e.g: on a checkbox click).
+ * - callback: function to invoke once the breakpoint is enabled
+ * - id: a new id to be applied to the corresponding element node
+ * @return boolean
+ * True if breakpoint existed and was enabled, false otherwise.
+ */
+ enableBreakpoint: function(aSourceLocation, aLineNumber, aOptions = {}) {
+ let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
+ if (!breakpointItem) {
+ return false;
+ }
+
+ // Set a new id to the corresponding breakpoint element if required.
+ if (aOptions.id) {
+ breakpointItem.attachment.view.container.id = "breakpoint-" + aOptions.id;
+ }
+ // Update the checkbox state if necessary.
+ if (!aOptions.silent) {
+ breakpointItem.attachment.view.checkbox.setAttribute("checked", "true");
+ }
+
+ let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+ let breakpointLocation = { url: url, line: line };
+
+ // Only create a new breakpoint if it doesn't exist yet.
+ if (!DebuggerController.Breakpoints.getBreakpoint(url, line)) {
+ DebuggerController.Breakpoints.addBreakpoint(breakpointLocation, aOptions.callback, {
+ noPaneUpdate: true,
+ noPaneHighlight: true,
+ conditionalExpression: breakpointItem.attachment.conditionalExpression
+ });
+ }
+
+ // Breakpoint is now enabled.
+ breakpointItem.attachment.disabled = false;
+ return true;
+ },
+
+ /**
+ * Disables a breakpoint.
+ *
+ * @param string aSourceLocation
+ * The breakpoint source location.
+ * @param number aLineNumber
+ * The breakpoint line number.
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - silent: pass true to not update the checkbox checked state;
+ * this is usually necessary when the checked state will
+ * be updated automatically (e.g: on a checkbox click).
+ * - callback: function to invoke once the breakpoint is disabled
+ * @return boolean
+ * True if breakpoint existed and was disabled, false otherwise.
+ */
+ disableBreakpoint: function(aSourceLocation, aLineNumber, aOptions = {}) {
+ let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
+ if (!breakpointItem) {
+ return false;
+ }
+
+ // Update the checkbox state if necessary.
+ if (!aOptions.silent) {
+ breakpointItem.attachment.view.checkbox.removeAttribute("checked");
+ }
+
+ let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+ let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
+
+ // Only remove the breakpoint if it exists.
+ if (breakpointClient) {
+ DebuggerController.Breakpoints.removeBreakpoint(breakpointClient, aOptions.callback, {
+ noPaneUpdate: true
+ });
+ // Remember the current conditional expression, to be reapplied when the
+ // breakpoint is re-enabled via enableBreakpoint().
+ breakpointItem.attachment.conditionalExpression = breakpointClient.conditionalExpression;
+ }
+
+ // Breakpoint is now disabled.
+ breakpointItem.attachment.disabled = true;
+ return true;
+ },
+
+ /**
+ * Highlights a breakpoint in this sources container.
+ *
+ * @param string aSourceLocation
+ * The breakpoint source location.
+ * @param number aLineNumber
+ * The breakpoint line number.
+ * @param object aFlags [optional]
+ * An object containing some of the following boolean properties:
+ * - updateEditor: true if editor updates should be allowed
+ * - openPopup: true if the expression popup should be shown
+ */
+ highlightBreakpoint: function(aSourceLocation, aLineNumber, aFlags = {}) {
+ let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
+ if (!breakpointItem) {
+ return;
+ }
+
+ // Breakpoint is now selected.
+ this._selectBreakpoint(breakpointItem);
+
+ // Update the editor source location and line number if necessary.
+ if (aFlags.updateEditor) {
+ DebuggerView.updateEditor(aSourceLocation, aLineNumber, { noDebug: true });
+ }
+
+ // If the breakpoint requires a new conditional expression, display
+ // the panel to input the corresponding expression.
+ if (aFlags.openPopup) {
+ this._openConditionalPopup();
+ } else {
+ this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Unhighlights the current breakpoint in this sources container.
+ */
+ unhighlightBreakpoint: function() {
+ this._unselectBreakpoint();
+ this._hideConditionalPopup();
+ },
+
+ /**
+ * Gets the currently selected breakpoint item.
+ * @return object
+ */
+ get selectedBreakpointItem() this._selectedBreakpoint,
+
+ /**
+ * Gets the currently selected breakpoint client.
+ * @return object
+ */
+ get selectedBreakpointClient() {
+ let breakpointItem = this._selectedBreakpoint;
+ if (breakpointItem) {
+ let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+ return DebuggerController.Breakpoints.getBreakpoint(url, line);
+ }
+ return null;
+ },
+
+ /**
+ * Marks a breakpoint as selected in this sources container.
+ *
+ * @param object aItem
+ * The breakpoint item to select.
+ */
+ _selectBreakpoint: function(aItem) {
+ if (this._selectedBreakpoint == aItem) {
+ return;
+ }
+ this._unselectBreakpoint();
+ this._selectedBreakpoint = aItem;
+ this._selectedBreakpoint.target.classList.add("selected");
+
+ // Ensure the currently selected breakpoint is visible.
+ this.widget.ensureElementIsVisible(aItem.target);
+ },
+
+ /**
+ * Marks the current breakpoint as unselected in this sources container.
+ */
+ _unselectBreakpoint: function() {
+ if (this._selectedBreakpoint) {
+ this._selectedBreakpoint.target.classList.remove("selected");
+ this._selectedBreakpoint = null;
+ }
+ },
+
+ /**
+ * Opens a conditional breakpoint's expression input popup.
+ */
+ _openConditionalPopup: function() {
+ let selectedBreakpointItem = this.selectedBreakpointItem;
+ let selectedBreakpointClient = this.selectedBreakpointClient;
+
+ if (selectedBreakpointClient.conditionalExpression === undefined) {
+ this._cbTextbox.value = selectedBreakpointClient.conditionalExpression = "";
+ } else {
+ this._cbTextbox.value = selectedBreakpointClient.conditionalExpression;
+ }
+
+ this._cbPanel.hidden = false;
+ this._cbPanel.openPopup(selectedBreakpointItem.attachment.view.lineNumber,
+ BREAKPOINT_CONDITIONAL_POPUP_POSITION,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
+ },
+
+ /**
+ * Hides a conditional breakpoint's expression input popup.
+ */
+ _hideConditionalPopup: function() {
+ this._cbPanel.hidden = true;
+ this._cbPanel.hidePopup();
+ },
+
+ /**
+ * Customization function for creating a breakpoint item's UI.
+ *
+ * @param object aOptions
+ * Additional options or flags supported by this operation:
+ * - number lineNumber
+ * The line number specified by the debugger controller.
+ * - string lineText
+ * The line text to be displayed.
+ * @return object
+ * An object containing the breakpoint container, checkbox,
+ * line number and line text nodes.
+ */
+ _createBreakpointView: function(aOptions) {
+ let { lineNumber, lineText } = aOptions;
+
+ let checkbox = document.createElement("checkbox");
+ checkbox.setAttribute("checked", "true");
+
+ let lineNumberNode = document.createElement("label");
+ lineNumberNode.className = "plain dbg-breakpoint-line";
+ lineNumberNode.setAttribute("value", lineNumber);
+
+ let lineTextNode = document.createElement("label");
+ lineTextNode.className = "plain dbg-breakpoint-text";
+ lineTextNode.setAttribute("value", lineText);
+ lineTextNode.setAttribute("crop", "end");
+ lineTextNode.setAttribute("flex", "1");
+ lineTextNode.setAttribute("tooltiptext",
+ lineText.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH));
+
+ let container = document.createElement("hbox");
+ container.id = "breakpoint-" + aOptions.actor;
+ container.className = "dbg-breakpoint side-menu-widget-item-other";
+ 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(lineNumberNode);
+ container.appendChild(lineTextNode);
+
+ return {
+ container: container,
+ checkbox: checkbox,
+ lineNumber: lineNumberNode,
+ lineText: lineTextNode
+ };
+ },
+
+ /**
+ * Creates a context menu for a breakpoint element.
+ *
+ * @param aOptions
+ * Additional options or flags supported by this operation:
+ * - string actor
+ * A breakpoint identifier specified by the debugger controller.
+ * @return object
+ * An object containing the breakpoint commandset and menu popup ids.
+ */
+ _createContextMenu: function(aOptions) {
+ let commandsetId = "bp-cSet-" + aOptions.actor;
+ let menupopupId = "bp-mPop-" + aOptions.actor;
+
+ let commandset = document.createElement("commandset");
+ let menupopup = document.createElement("menupopup");
+ commandset.id = commandsetId;
+ menupopup.id = menupopupId;
+
+ createMenuItem.call(this, "enableSelf", true);
+ createMenuItem.call(this, "disableSelf");
+ 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: commandsetId,
+ menupopupId: menupopupId
+ };
+
+ /**
+ * 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 + "-" + aOptions.actor + "-command";
+ let menuitemId = prefix + aName + "-" + aOptions.actor + "-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](aOptions.actor), 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);
+ }
+ },
+
+ /**
+ * Function called each time a breakpoint item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onBreakpointRemoved: function(aItem) {
+ dumpn("Finalizing breakpoint item: " + aItem);
+
+ // Destroy the context menu for the breakpoint.
+ let contextMenu = aItem.attachment.popup;
+ document.getElementById(contextMenu.commandsetId).remove();
+ document.getElementById(contextMenu.menupopupId).remove();
+
+ if (this._selectedBreakpoint == aItem) {
+ this._selectedBreakpoint = null;
+ }
+ },
+
+ /**
+ * The load listener for the source editor.
+ */
+ _onEditorLoad: function({ detail: editor }) {
+ editor.addEventListener("Selection", this._onEditorSelection, false);
+ editor.addEventListener("ContextMenu", this._onEditorContextMenu, false);
+ },
+
+ /**
+ * The unload listener for the source editor.
+ */
+ _onEditorUnload: function({ detail: editor }) {
+ editor.removeEventListener("Selection", this._onEditorSelection, false);
+ editor.removeEventListener("ContextMenu", this._onEditorContextMenu, false);
+ },
+
+ /**
+ * The selection listener for the source editor.
+ */
+ _onEditorSelection: function(e) {
+ let { start, end } = e.newValue;
+
+ let sourceLocation = this.selectedValue;
+ let lineStart = DebuggerView.editor.getLineAtOffset(start) + 1;
+ let lineEnd = DebuggerView.editor.getLineAtOffset(end) + 1;
+
+ if (this.getBreakpoint(sourceLocation, lineStart) && lineStart == lineEnd) {
+ this.highlightBreakpoint(sourceLocation, lineStart);
+ } else {
+ this.unhighlightBreakpoint();
+ }
+ },
+
+ /**
+ * The context menu listener for the source editor.
+ */
+ _onEditorContextMenu: function({ x, y }) {
+ let offset = DebuggerView.editor.getOffsetAtLocation(x, y);
+ let line = DebuggerView.editor.getLineAtOffset(offset);
+ this._editorContextMenuLineNumber = line;
+ },
+
+ /**
+ * The select listener for the sources container.
+ */
+ _onSourceSelect: function({ detail: sourceItem }) {
+ if (!sourceItem) {
+ return;
+ }
+ // The container is not empty and an actual item was selected.
+ let selectedSource = sourceItem.attachment.source;
+
+ if (DebuggerView.editorSource != selectedSource) {
+ DebuggerView.editorSource = selectedSource;
+ }
+ },
+
+ /**
+ * The click listener for the sources container.
+ */
+ _onSourceClick: function() {
+ // Use this container as a filtering target.
+ DebuggerView.Filtering.target = this;
+ },
+
+ /**
+ * 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 { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+
+ let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
+ let conditionalExpression = (breakpointClient || {}).conditionalExpression;
+
+ this.highlightBreakpoint(url, line, {
+ updateEditor: true,
+ openPopup: conditionalExpression !== undefined && e.button == 0
+ });
+ },
+
+ /**
+ * 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 { sourceLocation: url, lineNumber: line, disabled } = breakpointItem.attachment;
+
+ this[disabled ? "enableBreakpoint" : "disableBreakpoint"](url, line, {
+ silent: true
+ });
+
+ // 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;
+ },
+
+ /**
+ * The popup shown listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupShown: function() {
+ this._cbTextbox.focus();
+ this._cbTextbox.select();
+ },
+
+ /**
+ * The popup hiding listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupHiding: function() {
+ this._conditionalPopupVisible = false;
+ },
+
+ /**
+ * The input listener for the breakpoints conditional expression textbox.
+ */
+ _onConditionalTextboxInput: function() {
+ this.selectedBreakpointClient.conditionalExpression = this._cbTextbox.value;
+ },
+
+ /**
+ * The keypress listener for the breakpoints conditional expression textbox.
+ */
+ _onConditionalTextboxKeyPress: function(e) {
+ if (e.keyCode == e.DOM_VK_RETURN || e.keyCode == e.DOM_VK_ENTER) {
+ this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Called when the add breakpoint key sequence was pressed.
+ */
+ _onCmdAddBreakpoint: function() {
+ // If this command was executed via the context menu, add the breakpoint
+ // on the currently hovered line in the source editor.
+ if (this._editorContextMenuLineNumber >= 0) {
+ DebuggerView.editor.setCaretPosition(this._editorContextMenuLineNumber);
+ }
+ // Avoid placing breakpoints incorrectly when using key shortcuts.
+ this._editorContextMenuLineNumber = -1;
+
+ let url = DebuggerView.Sources.selectedValue;
+ let line = DebuggerView.editor.getCaretPosition().line + 1;
+ let breakpointItem = this.getBreakpoint(url, line);
+
+ // If a breakpoint already existed, remove it now.
+ if (breakpointItem) {
+ let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
+ DebuggerController.Breakpoints.removeBreakpoint(breakpointClient);
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ let breakpointLocation = { url: url, line: line };
+ DebuggerController.Breakpoints.addBreakpoint(breakpointLocation);
+ }
+ },
+
+ /**
+ * Called when the add conditional breakpoint key sequence was pressed.
+ */
+ _onCmdAddConditionalBreakpoint: function() {
+ // If this command was executed via the context menu, add the breakpoint
+ // on the currently hovered line in the source editor.
+ if (this._editorContextMenuLineNumber >= 0) {
+ DebuggerView.editor.setCaretPosition(this._editorContextMenuLineNumber);
+ }
+ // Avoid placing breakpoints incorrectly when using key shortcuts.
+ this._editorContextMenuLineNumber = -1;
+
+ let url = DebuggerView.Sources.selectedValue;
+ let line = DebuggerView.editor.getCaretPosition().line + 1;
+ let breakpointItem = this.getBreakpoint(url, line);
+
+ // If a breakpoint already existed or wasn't a conditional, morph it now.
+ if (breakpointItem) {
+ let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
+ this.highlightBreakpoint(url, line, { openPopup: true });
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ DebuggerController.Breakpoints.addBreakpoint({ url: url, line: line }, null, {
+ conditionalExpression: "",
+ openPopup: true
+ });
+ }
+ },
+
+ /**
+ * Function invoked on the "setConditional" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onSetConditional: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+ let { sourceLocation: url, lineNumber: line } = targetBreakpoint.attachment;
+
+ // Highlight the breakpoint and show a conditional expression popup.
+ this.highlightBreakpoint(url, line, { openPopup: true });
+
+ // Breakpoint is now highlighted.
+ aCallback();
+ },
+
+ /**
+ * Function invoked on the "enableSelf" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onEnableSelf: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+ let { sourceLocation: url, lineNumber: line, actor } = targetBreakpoint.attachment;
+
+ // Enable the breakpoint, in this container and the controller store.
+ if (this.enableBreakpoint(url, line)) {
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let enableSelfId = prefix + "enableSelf-" + actor + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + actor + "-menuitem";
+ document.getElementById(enableSelfId).setAttribute("hidden", "true");
+ document.getElementById(disableSelfId).removeAttribute("hidden");
+
+ // Breakpoint is now enabled.
+ // Breakpoints can only be set while the debuggee is paused, so if the
+ // active thread wasn't paused, wait for a resume before continuing.
+ if (gThreadClient.state != "paused") {
+ gThreadClient.addOneTimeListener("resumed", aCallback);
+ } else {
+ aCallback();
+ }
+ }
+ },
+
+ /**
+ * Function invoked on the "disableSelf" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDisableSelf: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+ let { sourceLocation: url, lineNumber: line, actor } = targetBreakpoint.attachment;
+
+ // Disable the breakpoint, in this container and the controller store.
+ if (this.disableBreakpoint(url, line)) {
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let enableSelfId = prefix + "enableSelf-" + actor + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + actor + "-menuitem";
+ document.getElementById(enableSelfId).removeAttribute("hidden");
+ document.getElementById(disableSelfId).setAttribute("hidden", "true");
+
+ // Breakpoint is now disabled.
+ aCallback();
+ }
+ },
+
+ /**
+ * Function invoked on the "deleteSelf" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDeleteSelf: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+ let { sourceLocation: url, lineNumber: line } = targetBreakpoint.attachment;
+
+ // Remove the breakpoint, from this container and the controller store.
+ this.removeBreakpoint(url, line);
+ gBreakpoints.removeBreakpoint(gBreakpoints.getBreakpoint(url, line), aCallback);
+ },
+
+ /**
+ * Function invoked on the "enableOthers" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onEnableOthers: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+
+ // Find a disabled breakpoint and re-enable it. Do this recursively until
+ // all required breakpoints are enabled, because each operation is async.
+ for (let source in this) {
+ for (let otherBreakpoint in source) {
+ if (otherBreakpoint != targetBreakpoint &&
+ otherBreakpoint.attachment.disabled) {
+ this._onEnableSelf(otherBreakpoint.attachment.actor, () =>
+ this._onEnableOthers(aId, aCallback));
+ return;
+ }
+ }
+ }
+ // All required breakpoints are now enabled.
+ aCallback();
+ },
+
+ /**
+ * Function invoked on the "disableOthers" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDisableOthers: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+
+ // Find an enabled breakpoint and disable it. Do this recursively until
+ // all required breakpoints are disabled, because each operation is async.
+ for (let source in this) {
+ for (let otherBreakpoint in source) {
+ if (otherBreakpoint != targetBreakpoint &&
+ !otherBreakpoint.attachment.disabled) {
+ this._onDisableSelf(otherBreakpoint.attachment.actor, () =>
+ this._onDisableOthers(aId, aCallback));
+ return;
+ }
+ }
+ }
+ // All required breakpoints are now disabled.
+ aCallback();
+ },
+
+ /**
+ * Function invoked on the "deleteOthers" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDeleteOthers: function(aId, aCallback = () => {}) {
+ let targetBreakpoint = this.getItemForPredicate(aItem => aItem.attachment.actor == aId);
+
+ // Find a breakpoint and delete it. Do this recursively until all required
+ // breakpoints are deleted, because each operation is async.
+ for (let source in this) {
+ for (let otherBreakpoint in source) {
+ if (otherBreakpoint != targetBreakpoint) {
+ this._onDeleteSelf(otherBreakpoint.attachment.actor, () =>
+ this._onDeleteOthers(aId, aCallback));
+ return;
+ }
+ }
+ }
+ // All required breakpoints are now deleted.
+ aCallback();
+ },
+
+ /**
+ * Function invoked on the "enableAll" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onEnableAll: function(aId) {
+ this._onEnableOthers(aId, () => this._onEnableSelf(aId));
+ },
+
+ /**
+ * Function invoked on the "disableAll" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDisableAll: function(aId) {
+ this._onDisableOthers(aId, () => this._onDisableSelf(aId));
+ },
+
+ /**
+ * Function invoked on the "deleteAll" menuitem command.
+ *
+ * @param string aId
+ * The original breakpoint client actor. If a breakpoint was disabled
+ * and then re-enabled, then this will not correspond to the entry in
+ * the controller's breakpoints store.
+ * @param function aCallback [optional]
+ * A function to invoke once this operation finishes.
+ */
+ _onDeleteAll: function(aId) {
+ this._onDeleteOthers(aId, () => this._onDeleteSelf(aId));
+ },
+
+ _commandset: null,
+ _popupset: null,
+ _cmPopup: null,
+ _cbPanel: null,
+ _cbTextbox: null,
+ _selectedBreakpoint: null,
+ _editorContextMenuLineNumber: -1,
+ _conditionalPopupVisible: false
+});
+
+/**
+ * Utility functions for handling sources.
+ */
+let SourceUtils = {
+ _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
+ _groupsCache: new Map(),
+
+ /**
+ * Clears the labels 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();
+ },
+
+ /**
+ * 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 = 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 { scheme, directory, fileName } = uri;
+ let hostPort;
+ // Add-on SDK jar: URLs will cause accessing hostPort to throw.
+ if (scheme != "jar") {
+ hostPort = uri.hostPort;
+ }
+ let lastDir = directory.split("/").reverse()[1];
+ let group = [];
+
+ // Only show interesting schemes, http is implicit.
+ if (scheme != "http") {
+ group.push(scheme);
+ }
+ // Hostnames don't always exist for files or some resource urls.
+ // e.g. file://foo/bar.js or resource:///foo/bar.js don't have a host.
+ if (hostPort) {
+ // If the hostname is a dot-separated identifier, show the first 2 parts.
+ group.push(hostPort.split(".").slice(0, 2).join("."));
+ }
+ // Append the last directory if the path leads to an actual file.
+ // e.g. http://foo.org/bar/ should only show "foo.org", not "foo.org bar"
+ if (fileName) {
+ group.push(lastDir);
+ }
+
+ let groupLabel = group.join(" ");
+ 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 L10N.ellipsis + aUrl.slice(-aLength);
+ break;
+ case "center":
+ return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1);
+ break;
+ case "end":
+ return aUrl.substr(0, aLength) + L10N.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.containsLabel(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 = aUrl.hostPort;
+ 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;
+ }
+};
+
+/**
+ * Functions handling the watch expressions UI.
+ */
+function WatchExpressionsView() {
+ dumpn("WatchExpressionsView was instantiated");
+
+ 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 ListWidget(document.getElementById("expressions"));
+ this.widget.permaText = L10N.getStr("addWatchExpressionText");
+ this.widget.itemFactory = this._createItemView;
+ this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
+ this.widget.addEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the WatchExpressionsView");
+
+ this.widget.removeEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Adds a watch expression in this container.
+ *
+ * @param string aExpression [optional]
+ * An optional initial watch expression text.
+ */
+ addExpression: function(aExpression = "") {
+ // Watch expressions are UI elements which benefit from visible panes.
+ DebuggerView.showInstrumentsPane();
+
+ // Append a watch expression item to this container.
+ let expressionItem = this.push([, aExpression], {
+ index: 0, /* specifies on which position should the item be appended */
+ relaxed: true, /* this container should allow dupes & degenerates */
+ attachment: {
+ initialExpression: aExpression,
+ currentExpression: ""
+ }
+ });
+
+ // Automatically focus the new watch expression input.
+ expressionItem.attachment.inputNode.select();
+ expressionItem.attachment.inputNode.focus();
+ DebuggerView.Variables.parentNode.scrollTop = 0;
+ },
+
+ /**
+ * 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 =
+ [i for (i in this) if (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.inputNode.value = aExpression;
+
+ // Synchronize with the controller's watch expressions store.
+ DebuggerController.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 =
+ [i for (i in this) if (i.attachment.currentExpression == aVar.name)][0];
+
+ // Remove the watch expression.
+ this.remove(expressionItem);
+
+ // Synchronize with the controller's watch expressions store.
+ DebuggerController.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.orderedItems.map((e) => e.attachment.currentExpression);
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param any aAttachment
+ * Some attached primitive/object.
+ */
+ _createItemView: function(aElementNode, aAttachment) {
+ let arrowNode = document.createElement("box");
+ arrowNode.className = "dbg-expression-arrow";
+
+ let inputNode = document.createElement("textbox");
+ inputNode.className = "plain dbg-expression-input";
+ inputNode.setAttribute("value", aAttachment.initialExpression);
+ 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);
+
+ aElementNode.className = "dbg-expression";
+ aElementNode.appendChild(arrowNode);
+ aElementNode.appendChild(inputNode);
+ aElementNode.appendChild(closeNode);
+
+ aAttachment.arrowNode = arrowNode;
+ aAttachment.inputNode = inputNode;
+ aAttachment.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 || DebuggerView.editor.getSelectedText());
+ }
+ },
+
+ /**
+ * 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.
+ DebuggerController.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.
+ DebuggerController.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.
+ DebuggerController.StackFrames.syncWatchExpressions();
+ },
+
+ /**
+ * The keypress listener for a watch expression's textbox.
+ */
+ _onKeyPress: function(e) {
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ case e.DOM_VK_ESCAPE:
+ DebuggerView.editor.focus();
+ return;
+ }
+ }
+});
+
+/**
+ * Functions handling the global search UI.
+ */
+function GlobalSearchView() {
+ dumpn("GlobalSearchView was instantiated");
+
+ this._startSearch = this._startSearch.bind(this);
+ this._performGlobalSearch = this._performGlobalSearch.bind(this);
+ this._createItemView = this._createItemView.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ 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 ListWidget(document.getElementById("globalsearch"));
+ this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
+
+ this.widget.emptyText = L10N.getStr("noMatchingStringsText");
+ this.widget.itemFactory = this._createItemView;
+ this.widget.addEventListener("scroll", this._onScroll, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the GlobalSearchView");
+
+ this.widget.removeEventListener("scroll", this._onScroll, false);
+ },
+
+ /**
+ * Gets the visibility state of the global search container.
+ * @return boolean
+ */
+ get hidden()
+ this.widget.getAttribute("hidden") == "true" ||
+ this._splitter.getAttribute("hidden") == "true",
+
+ /**
+ * 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);
+ },
+
+ /**
+ * Hides and removes all items from this search container.
+ */
+ clearView: function() {
+ this.hidden = true;
+ this.empty();
+ window.dispatchEvent(document, "Debugger:GlobalSearch:ViewCleared");
+ },
+
+ /**
+ * 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)
+ });
+ },
+
+ /**
+ * Allows searches to be scheduled and delayed to avoid redundant calls.
+ */
+ delayedSearch: true,
+
+ /**
+ * Schedules searching for a string in all of the sources.
+ *
+ * @param string aQuery
+ * The string to search for.
+ */
+ scheduleSearch: function(aQuery) {
+ if (!this.delayedSearch) {
+ this.performSearch(aQuery);
+ return;
+ }
+ let delay = Math.max(GLOBAL_SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
+
+ window.clearTimeout(this._searchTimeout);
+ this._searchFunction = this._startSearch.bind(this, aQuery);
+ this._searchTimeout = window.setTimeout(this._searchFunction, delay);
+ },
+
+ /**
+ * Immediately searches for a string in all of the sources.
+ *
+ * @param string aQuery
+ * The string to search for.
+ */
+ performSearch: function(aQuery) {
+ window.clearTimeout(this._searchTimeout);
+ this._searchFunction = null;
+ this._startSearch(aQuery);
+ },
+
+ /**
+ * Starts searching for a string in all of the sources.
+ *
+ * @param string aQuery
+ * The string to search for.
+ */
+ _startSearch: function(aQuery) {
+ this._searchedToken = aQuery;
+
+ // Start fetching as many sources as possible, then perform the search.
+ DebuggerController.SourceScripts
+ .getTextForSources(DebuggerView.Sources.values)
+ .then(this._performGlobalSearch);
+ },
+
+ /**
+ * Finds string matches in all the sources stored in the controller's cache,
+ * and groups them by location and line number.
+ */
+ _performGlobalSearch: function(aSources) {
+ // Get the currently searched token from the filtering input.
+ let token = this._searchedToken;
+
+ // Make sure we're actually searching for something.
+ if (!token) {
+ this.clearView();
+ window.dispatchEvent(document, "Debugger:GlobalSearch:TokenEmpty");
+ return;
+ }
+
+ // Search is not case sensitive, prepare the actual searched token.
+ let lowerCaseToken = token.toLowerCase();
+ let tokenLength = token.length;
+
+ // Prepare the results map, containing search details for each line.
+ let globalResults = new GlobalResults();
+
+ for (let [location, contents] of aSources) {
+ // Verify that the search token is found anywhere in the source.
+ if (!contents.toLowerCase().contains(lowerCaseToken)) {
+ continue;
+ }
+ let lines = contents.split("\n");
+ let sourceResults = new SourceResults();
+
+ for (let i = 0, len = lines.length; i < len; i++) {
+ let line = lines[i];
+ let lowerCaseLine = line.toLowerCase();
+
+ // Search is not case sensitive, and is tied to each line in the source.
+ if (!lowerCaseLine.contains(lowerCaseToken)) {
+ continue;
+ }
+
+ let lineNumber = i;
+ let lineResults = new LineResults();
+
+ lowerCaseLine.split(lowerCaseToken).reduce((prev, curr, index, { length }) => {
+ let prevLength = prev.length;
+ let currLength = curr.length;
+ let unmatched = line.substr(prevLength, currLength);
+ lineResults.add(unmatched);
+
+ if (index != length - 1) {
+ let matched = line.substr(prevLength + currLength, tokenLength);
+ let range = {
+ start: prevLength + currLength,
+ length: matched.length
+ };
+ lineResults.add(matched, range, true);
+ sourceResults.matchCount++;
+ }
+ return prev + token + curr;
+ }, "");
+
+ if (sourceResults.matchCount) {
+ sourceResults.add(lineNumber, lineResults);
+ }
+ }
+ if (sourceResults.matchCount) {
+ globalResults.add(location, sourceResults);
+ }
+ }
+
+ // Empty this container to rebuild the search results.
+ this.empty();
+
+ // Signal if there are any matches, and the rebuild the results.
+ if (globalResults.itemCount) {
+ this.hidden = false;
+ this._currentlyFocusedMatch = -1;
+ this._createGlobalResultsUI(globalResults);
+ window.dispatchEvent(document, "Debugger:GlobalSearch:MatchFound");
+ } else {
+ window.dispatchEvent(document, "Debugger:GlobalSearch:MatchNotFound");
+ }
+ },
+
+ /**
+ * 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 [location, sourceResults] in aGlobalResults) {
+ if (i++ == 0) {
+ this._createSourceResultsUI(location, sourceResults, true);
+ } 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, location, sourceResults) }, 0);
+ }
+ }
+ },
+
+ /**
+ * Creates source search results entries and adds them to this container.
+ *
+ * @param string aLocation
+ * The location of the source.
+ * @param SourceResults aSourceResults
+ * An object containing all the matched lines for a specific source.
+ * @param boolean aExpandFlag
+ * True to expand the source results.
+ */
+ _createSourceResultsUI: function(aLocation, aSourceResults, aExpandFlag) {
+ // Append a source results item to this container.
+ let sourceResultsItem = this.push([aLocation, aSourceResults.matchCount], {
+ index: -1, /* specifies on which position should the item be appended */
+ relaxed: true, /* this container should allow dupes & degenerates */
+ attachment: {
+ sourceResults: aSourceResults,
+ expandFlag: aExpandFlag
+ }
+ });
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param any aAttachment
+ * Some attached primitive/object.
+ * @param string aLocation
+ * The source result's location.
+ * @param string aMatchCount
+ * The source result's match count.
+ */
+ _createItemView: function(aElementNode, aAttachment, aLocation, aMatchCount) {
+ let { sourceResults, expandFlag } = aAttachment;
+
+ sourceResults.createView(aElementNode, aLocation, aMatchCount, expandFlag, {
+ onHeaderClick: this._onHeaderClick,
+ onLineClick: this._onLineClick,
+ onMatchClick: this._onMatchClick
+ });
+ },
+
+ /**
+ * 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 location = sourceResultsItem.location;
+ let lineNumber = lineResultsItem.lineNumber;
+ DebuggerView.updateEditor(location, lineNumber + 1, { noDebug: true });
+
+ let editor = DebuggerView.editor;
+ let offset = editor.getCaretOffset();
+ let { start, length } = lineResultsItem.lineData.range;
+ editor.setSelection(offset + start, offset + start + length);
+ },
+
+ /**
+ * The scroll listener for the global search container.
+ */
+ _onScroll: function(e) {
+ for (let item in this) {
+ this._expandResultsIfNeeded(item.target);
+ }
+ },
+
+ /**
+ * Expands the source results it they are currently visible.
+ *
+ * @param nsIDOMNode aTarget
+ * The element associated with the displayed item.
+ */
+ _expandResultsIfNeeded: function(aTarget) {
+ let sourceResultsItem = SourceResults.getItemForElement(aTarget);
+ if (sourceResultsItem.instance.toggled ||
+ sourceResultsItem.instance.expanded) {
+ return;
+ }
+ let { top, height } = aTarget.getBoundingClientRect();
+ let { clientHeight } = this.widget._parent;
+
+ if (top - height <= clientHeight || this._forceExpandResults) {
+ sourceResultsItem.instance.expand();
+ }
+ },
+
+ /**
+ * Scrolls a match into view if not already visible.
+ *
+ * @param nsIDOMNode aMatch
+ * The match to scroll into view.
+ */
+ _scrollMatchIntoViewIfNeeded: function(aMatch) {
+ // TODO: Accessing private widget properties. Figure out what's the best
+ // way to expose such things. Bug 876271.
+ let boxObject = this.widget._parent.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ boxObject.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: function() {
+ 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,
+ _searchTimeout: null,
+ _searchFunction: null,
+ _searchedToken: ""
+});
+
+/**
+ * An object containing all source results, grouped by source location.
+ * Iterable via "for (let [location, sourceResults] in globalResults) { }".
+ */
+function GlobalResults() {
+ this._store = new Map();
+ SourceResults._itemsByElement = new Map();
+ LineResults._itemsByElement = new Map();
+}
+
+GlobalResults.prototype = {
+ /**
+ * Adds source results to this store.
+ *
+ * @param string aLocation
+ * The location of the source.
+ * @param SourceResults aSourceResults
+ * An object containing all the matched lines for a specific source.
+ */
+ add: function(aLocation, aSourceResults) {
+ this._store.set(aLocation, aSourceResults);
+ },
+
+ /**
+ * Gets the number of source results in this store.
+ */
+ get itemCount() this._store.size,
+
+ _store: null
+};
+
+/**
+ * An object containing all the matched lines for a specific source.
+ * Iterable via "for (let [lineNumber, lineResults] in sourceResults) { }".
+ */
+function SourceResults() {
+ this._store = new Map();
+ this.matchCount = 0;
+}
+
+SourceResults.prototype = {
+ /**
+ * Adds line results to this store.
+ *
+ * @param number aLineNumber
+ * The line location in the source.
+ * @param LineResults aLineResults
+ * An object containing all the matches for a specific line.
+ */
+ add: function(aLineNumber, aLineResults) {
+ this._store.set(aLineNumber, aLineResults);
+ },
+
+ /**
+ * The number of matches in this store. One line may have multiple matches.
+ */
+ matchCount: -1,
+
+ /**
+ * Expands the element, showing all the added details.
+ */
+ expand: function() {
+ this._target.resultsContainer.removeAttribute("hidden")
+ this._target.arrow.setAttribute("open", "");
+ },
+
+ /**
+ * Collapses the element, hiding all the added details.
+ */
+ collapse: function() {
+ this._target.resultsContainer.setAttribute("hidden", "true");
+ this._target.arrow.removeAttribute("open");
+ },
+
+ /**
+ * Toggles between the element collapse/expand state.
+ */
+ toggle: function(e) {
+ if (e instanceof Event) {
+ this._userToggled = true;
+ }
+ this.expanded ^= 1;
+ },
+
+ /**
+ * Relaxes the auto-expand rules to always show as many results as possible.
+ */
+ alwaysExpand: true,
+
+ /**
+ * Gets this element's expanded state.
+ * @return boolean
+ */
+ get expanded()
+ this._target.resultsContainer.getAttribute("hidden") != "true" &&
+ this._target.arrow.hasAttribute("open"),
+
+ /**
+ * Sets this element's expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),
+
+ /**
+ * Returns if this element was ever toggled via user interaction.
+ * @return boolean
+ */
+ get toggled() this._userToggled,
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() this._target,
+
+ /**
+ * Customization function for creating this item's UI.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param string aLocation
+ * The source result's location.
+ * @param string aMatchCount
+ * The source result's match count.
+ * @param boolean aExpandFlag
+ * True to expand the source results.
+ * @param object aCallbacks
+ * An object containing all the necessary callback functions:
+ * - onHeaderClick
+ * - onMatchClick
+ */
+ createView: function(aElementNode, aLocation, aMatchCount, aExpandFlag, aCallbacks) {
+ this._target = aElementNode;
+
+ let arrow = document.createElement("box");
+ arrow.className = "arrow";
+
+ let locationNode = document.createElement("label");
+ locationNode.className = "plain dbg-results-header-location";
+ locationNode.setAttribute("value", SourceUtils.trimUrlLength(aLocation));
+
+ let matchCountNode = document.createElement("label");
+ matchCountNode.className = "plain dbg-results-header-match-count";
+ matchCountNode.setAttribute("value", "(" + aMatchCount + ")");
+
+ let 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 = document.createElement("vbox");
+ resultsContainer.className = "dbg-results-container";
+ resultsContainer.setAttribute("hidden", "true");
+
+ for (let [lineNumber, lineResults] of this._store) {
+ lineResults.createView(resultsContainer, lineNumber, aCallbacks)
+ }
+
+ aElementNode.arrow = arrow;
+ aElementNode.resultsHeader = resultsHeader;
+ aElementNode.resultsContainer = resultsContainer;
+
+ if ((aExpandFlag || this.alwaysExpand) &&
+ aMatchCount < 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-" + aLocation;
+ aElementNode.className = "dbg-source-results";
+ aElementNode.appendChild(resultsBox);
+
+ SourceResults._itemsByElement.set(aElementNode, {
+ location: aLocation,
+ matchCount: aMatchCount,
+ autoExpand: aExpandFlag,
+ instance: this
+ });
+ },
+
+ _store: null,
+ _target: null,
+ _userToggled: false
+};
+
+/**
+ * An object containing all the matches for a specific line.
+ * Iterable via "for (let chunk in lineResults) { }".
+ */
+function LineResults() {
+ this._store = [];
+}
+
+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
+ });
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() this._target,
+
+ /**
+ * Customization function for creating this item's UI.
+ *
+ * @param nsIDOMNode aContainer
+ * The element associated with the displayed item.
+ * @param number aLineNumber
+ * The line location in the source.
+ * @param object aCallbacks
+ * An object containing all the necessary callback functions:
+ * - onMatchClick
+ * - onLineClick
+ */
+ createView: function(aContainer, aLineNumber, aCallbacks) {
+ this._target = aContainer;
+
+ let lineNumberNode = document.createElement("label");
+ let lineContentsNode = document.createElement("hbox");
+ let lineString = "";
+ let lineLength = 0;
+ let firstMatch = null;
+
+ lineNumberNode.className = "plain dbg-results-line-number";
+ lineNumberNode.setAttribute("value", aLineNumber + 1);
+ lineContentsNode.className = "light list-widget-item dbg-results-line-contents";
+ lineContentsNode.setAttribute("flex", "1");
+
+ for (let chunk of this._store) {
+ let { string, range, match } = chunk;
+ lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
+ lineLength += string.length;
+
+ let label = document.createElement("label");
+ label.className = "plain dbg-results-line-contents-string";
+ label.setAttribute("value", lineString);
+ label.setAttribute("match", match);
+ lineContentsNode.appendChild(label);
+
+ if (match) {
+ this._entangleMatch(aLineNumber, label, chunk);
+ label.addEventListener("click", aCallbacks.onMatchClick, false);
+ firstMatch = firstMatch || label;
+ }
+ if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
+ lineContentsNode.appendChild(this._ellipsis.cloneNode());
+ 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);
+ aContainer.appendChild(searchResult);
+ },
+
+ /**
+ * Handles a match while creating the view.
+ * @param number aLineNumber
+ * @param nsIDOMNode aNode
+ * @param object aMatchChunk
+ */
+ _entangleMatch: function(aLineNumber, aNode, aMatchChunk) {
+ LineResults._itemsByElement.set(aNode, {
+ lineNumber: aLineNumber,
+ lineData: aMatchChunk
+ });
+ },
+
+ /**
+ * Handles a line while creating the view.
+ * @param nsIDOMNode aNode
+ * @param nsIDOMNode aFirstMatch
+ */
+ _entangleLine: function(aNode, aFirstMatch) {
+ LineResults._itemsByElement.set(aNode, {
+ firstMatch: aFirstMatch,
+ nonenumerable: 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", L10N.ellipsis);
+ return label;
+ })(),
+
+ _store: null,
+ _target: null
+};
+
+/**
+ * A generator-iterator over the global, source or line results.
+ */
+GlobalResults.prototype.__iterator__ =
+SourceResults.prototype.__iterator__ =
+LineResults.prototype.__iterator__ = function() {
+ for (let item of this._store) {
+ yield item;
+ }
+};
+
+/**
+ * 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);
+};
+
+/**
+ * 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.nonenumerable && !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.nonenumerable) {
+ 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.nonenumerable) {
+ count++;
+ }
+ }
+ return count;
+};
+
+/**
+ * Preliminary setup for the DebuggerView object.
+ */
+DebuggerView.Sources = new SourcesView();
+DebuggerView.WatchExpressions = new WatchExpressionsView();
+DebuggerView.GlobalSearch = new GlobalSearchView();
diff --git a/browser/devtools/debugger/debugger-toolbar.js b/browser/devtools/debugger/debugger-toolbar.js
new file mode 100644
index 000000000..b18e13516
--- /dev/null
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -0,0 +1,1611 @@
+/* -*- 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";
+
+// A time interval sufficient for the options popup panel to finish hiding
+// itself.
+const POPUP_HIDDEN_DELAY = 100; // ms
+
+/**
+ * Functions handling the toolbar view: close button, expand/collapse button,
+ * pause/resume and stepping buttons etc.
+ */
+function ToolbarView() {
+ dumpn("ToolbarView was instantiated");
+
+ 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 = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the ToolbarView");
+
+ this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
+ this._resumeOrderPanel = document.getElementById("resumption-order-panel");
+ this._resumeButton = document.getElementById("resume");
+ this._stepOverButton = document.getElementById("step-over");
+ this._stepInButton = document.getElementById("step-in");
+ this._stepOutButton = document.getElementById("step-out");
+ this._chromeGlobals = document.getElementById("chrome-globals");
+
+ let resumeKey = LayoutHelpers.prettyKey(document.getElementById("resumeKey"), true);
+ let stepOverKey = LayoutHelpers.prettyKey(document.getElementById("stepOverKey"), true);
+ let stepInKey = LayoutHelpers.prettyKey(document.getElementById("stepInKey"), true);
+ let stepOutKey = LayoutHelpers.prettyKey(document.getElementById("stepOutKey"), true);
+ this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey);
+ this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey);
+ this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey);
+ this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey);
+ this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey);
+
+ this._instrumentsPaneToggleButton.addEventListener("mousedown", 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);
+
+ // TODO: bug 806775 - group scripts by globals using hostAnnotations.
+ // this.toggleChromeGlobalsContainer(window._isChromeDebugger);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the ToolbarView");
+
+ this._instrumentsPaneToggleButton.removeEventListener("mousedown", 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);
+ },
+
+ /**
+ * 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 descriptionNode = document.getElementById("resumption-panel-desc");
+ descriptionNode.setAttribute("value", label);
+
+ this._resumeOrderPanel.openPopup(this._resumeButton);
+ },
+
+ /**
+ * Sets the resume button state based on the debugger active thread.
+ *
+ * @param string aState
+ * Either "paused" or "attached".
+ */
+ toggleResumeButtonState: function(aState) {
+ // 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);
+ }
+ // If we're attached, do the opposite.
+ else if (aState == "attached") {
+ this._resumeButton.removeAttribute("checked");
+ this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip);
+ }
+ },
+
+ /**
+ * Sets the chrome globals container hidden or visible. It's hidden by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ toggleChromeGlobalsContainer: function(aVisibleFlag) {
+ this._chromeGlobals.setAttribute("hidden", !aVisibleFlag);
+ },
+
+ /**
+ * Listener handling the toggle button click event.
+ */
+ _onTogglePanesPressed: function() {
+ DebuggerView.toggleInstrumentsPane({
+ visible: DebuggerView.instrumentsPaneHidden,
+ animated: true,
+ delayed: true
+ });
+ },
+
+ /**
+ * Listener handling the pause/resume button click event.
+ */
+ _onResumePressed: function() {
+ if (DebuggerController.activeThread.paused) {
+ let warn = DebuggerController._ensureResumptionOrder;
+ DebuggerController.activeThread.resume(warn);
+ } else {
+ DebuggerController.activeThread.interrupt();
+ }
+ },
+
+ /**
+ * Listener handling the step over button click event.
+ */
+ _onStepOverPressed: function() {
+ if (DebuggerController.activeThread.paused) {
+ DebuggerController.activeThread.stepOver();
+ }
+ },
+
+ /**
+ * Listener handling the step in button click event.
+ */
+ _onStepInPressed: function() {
+ if (DebuggerController.activeThread.paused) {
+ DebuggerController.activeThread.stepIn();
+ }
+ },
+
+ /**
+ * Listener handling the step out button click event.
+ */
+ _onStepOutPressed: function() {
+ if (DebuggerController.activeThread.paused) {
+ DebuggerController.activeThread.stepOut();
+ }
+ },
+
+ _instrumentsPaneToggleButton: null,
+ _resumeOrderPanel: null,
+ _resumeButton: null,
+ _stepOverButton: null,
+ _stepInButton: null,
+ _stepOutButton: null,
+ _chromeGlobals: null,
+ _resumeTooltip: "",
+ _pauseTooltip: "",
+ _stepOverTooltip: "",
+ _stepInTooltip: "",
+ _stepOutTooltip: ""
+};
+
+/**
+ * Functions handling the options UI.
+ */
+function OptionsView() {
+ dumpn("OptionsView was instantiated");
+
+ this._togglePauseOnExceptions = this._togglePauseOnExceptions.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);
+}
+
+OptionsView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the OptionsView");
+
+ this._button = document.getElementById("debugger-options");
+ this._pauseOnExceptionsItem = document.getElementById("pause-on-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._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions);
+ this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup);
+ this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible);
+ this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible);
+ this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the OptionsView");
+ // Nothing to do here yet.
+ },
+
+ /**
+ * Listener handling the 'gear menu' popup showing event.
+ */
+ _onPopupShowing: function() {
+ this._button.setAttribute("open", "true");
+ },
+
+ /**
+ * 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.dispatchEvent(document, "Debugger:OptionsPopupHidden");
+ },
+
+ /**
+ * Listener handling the 'pause on exceptions' menuitem command.
+ */
+ _togglePauseOnExceptions: function() {
+ let pref = Prefs.pauseOnExceptions =
+ this._pauseOnExceptionsItem.getAttribute("checked") == "true";
+
+ DebuggerController.activeThread.pauseOnExceptions(pref);
+ },
+
+ /**
+ * 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";
+
+ DebuggerView.Variables.onlyEnumVisible = pref;
+ },
+
+ /**
+ * Listener handling the 'show variables searchbox' menuitem command.
+ */
+ _toggleShowVariablesFilterBox: function() {
+ let pref = Prefs.variablesSearchboxVisible =
+ this._showVariablesFilterBoxItem.getAttribute("checked") == "true";
+
+ DebuggerView.Variables.searchEnabled = pref;
+ },
+
+ /**
+ * Listener handling the 'show original source' menuitem command.
+ */
+ _toggleShowOriginalSource: function() {
+ function reconfigure() {
+ window.removeEventListener("Debugger:OptionsPopupHidden", reconfigure, false);
+
+ // The popup panel needs more time to hide after triggering onpopuphidden.
+ window.setTimeout(() => {
+ DebuggerController.reconfigureThread(pref);
+ }, POPUP_HIDDEN_DELAY);
+ }
+
+ let pref = Prefs.sourceMapsEnabled =
+ this._showOriginalSourceItem.getAttribute("checked") == "true";
+
+ // Don't block the UI while reconfiguring the server.
+ window.addEventListener("Debugger:OptionsPopupHidden", reconfigure, false);
+ },
+
+ _button: null,
+ _pauseOnExceptionsItem: null,
+ _showPanesOnStartupItem: null,
+ _showVariablesOnlyEnumItem: null,
+ _showVariablesFilterBoxItem: null,
+ _showOriginalSourceItem: null
+};
+
+/**
+ * Functions handling the chrome globals UI.
+ */
+function ChromeGlobalsView() {
+ dumpn("ChromeGlobalsView was instantiated");
+
+ this._onSelect = this._onSelect.bind(this);
+ this._onClick = this._onClick.bind(this);
+}
+
+ChromeGlobalsView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the ChromeGlobalsView");
+
+ this.widget = document.getElementById("chrome-globals");
+ this.emptyText = L10N.getStr("noGlobalsText");
+ this.unavailableText = L10N.getStr("noMatchingGlobalsText");
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("click", this._onClick, false);
+
+ // Show an empty label by default.
+ this.empty();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the ChromeGlobalsView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * The select listener for the chrome globals container.
+ */
+ _onSelect: function() {
+ // TODO: bug 806775, do something useful for chrome debugging.
+ },
+
+ /**
+ * The click listener for the chrome globals container.
+ */
+ _onClick: function() {
+ // Use this container as a filtering target.
+ DebuggerView.Filtering.target = this;
+ }
+});
+
+/**
+ * Functions handling the stackframes UI.
+ */
+function StackFramesView() {
+ dumpn("StackFramesView was instantiated");
+
+ this._onStackframeRemoved = this._onStackframeRemoved.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._afterScroll = this._afterScroll.bind(this);
+}
+
+StackFramesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the StackFramesView");
+
+ let commandset = this._commandset = document.createElement("commandset");
+ let menupopup = this._menupopup = document.createElement("menupopup");
+ commandset.id = "stackframesCommandset";
+ menupopup.id = "stackframesMenupopup";
+
+ document.getElementById("debuggerPopupset").appendChild(menupopup);
+ document.getElementById("debuggerCommands").appendChild(commandset);
+
+ this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("scroll", this._onScroll, true);
+ window.addEventListener("resize", this._onScroll, true);
+
+ this.autoFocusOnFirstItem = false;
+ this.autoFocusOnSelection = false;
+ },
+
+ /**
+ * 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 aFrameTitle
+ * The frame title to be displayed in the list.
+ * @param string aSourceLocation
+ * The source location to be displayed in the list.
+ * @param string aLineNumber
+ * The line number to be displayed in the list.
+ * @param number aDepth
+ * The frame depth specified by the debugger.
+ */
+ addFrame: function(aFrameTitle, aSourceLocation, aLineNumber, aDepth) {
+ // Create the element node and menu entry for the stack frame item.
+ let frameView = this._createFrameView.apply(this, arguments);
+ let menuEntry = this._createMenuEntry.apply(this, arguments);
+
+ // Append a stack frame item to this container.
+ this.push([frameView], {
+ index: 0, /* specifies on which position should the item be appended */
+ attachment: {
+ popup: menuEntry,
+ depth: aDepth
+ },
+ attributes: [
+ ["contextmenu", "stackframesMenupopup"],
+ ["tooltiptext", aSourceLocation]
+ ],
+ // Make sure that when the stack frame item is removed, the corresponding
+ // menuitem and command are also destroyed.
+ finalize: this._onStackframeRemoved
+ });
+ },
+
+ /**
+ * Selects the frame at the specified depth in this container.
+ * @param number aDepth
+ */
+ set selectedDepth(aDepth) {
+ this.selectedItem = (aItem) => aItem.attachment.depth == aDepth;
+ },
+
+ /**
+ * 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 aFrameTitle
+ * The frame title to be displayed in the list.
+ * @param string aSourceLocation
+ * The source location to be displayed in the list.
+ * @param string aLineNumber
+ * The line number to be displayed in the list.
+ * @param number aDepth
+ * The frame depth specified by the debugger.
+ * @return nsIDOMNode
+ * The stack frame view.
+ */
+ _createFrameView: function(aFrameTitle, aSourceLocation, aLineNumber, aDepth) {
+ let frameDetails =
+ SourceUtils.trimUrlLength(
+ SourceUtils.getSourceLabel(aSourceLocation),
+ STACK_FRAMES_SOURCE_URL_MAX_LENGTH,
+ STACK_FRAMES_SOURCE_URL_TRIM_SECTION) + SEARCH_LINE_FLAG + aLineNumber;
+
+ let frameTitleNode = document.createElement("label");
+ frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag";
+ frameTitleNode.setAttribute("value", aFrameTitle);
+
+ let frameDetailsNode = document.createElement("label");
+ frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id";
+ frameDetailsNode.setAttribute("value", frameDetails);
+
+ let container = document.createElement("hbox");
+ container.id = "stackframe-" + aDepth;
+ container.className = "dbg-stackframe";
+
+ container.appendChild(frameTitleNode);
+ container.appendChild(frameDetailsNode);
+
+ return container;
+ },
+
+ /**
+ * Customization function for populating an item's context menu.
+ *
+ * @param string aFrameTitle
+ * The frame title to be displayed in the list.
+ * @param string aSourceLocation
+ * The source location to be displayed in the list.
+ * @param string aLineNumber
+ * The line number to be displayed in the list.
+ * @param number aDepth
+ * The frame depth specified by the debugger.
+ * @return object
+ * An object containing the stack frame command and menu item.
+ */
+ _createMenuEntry: function(aFrameTitle, aSourceLocation, aLineNumber, aDepth) {
+ let frameDescription =
+ SourceUtils.trimUrlLength(
+ SourceUtils.getSourceLabel(aSourceLocation),
+ STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH,
+ STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION) + SEARCH_LINE_FLAG + aLineNumber;
+
+ let prefix = "sf-cMenu-"; // "stackframes context menu"
+ let commandId = prefix + aDepth + "-" + "-command";
+ let menuitemId = prefix + aDepth + "-" + "-menuitem";
+
+ let command = document.createElement("command");
+ command.id = commandId;
+ command.addEventListener("command", () => this.selectedDepth = aDepth, false);
+
+ let menuitem = document.createElement("menuitem");
+ menuitem.id = menuitemId;
+ menuitem.className = "dbg-stackframe-menuitem";
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("command", commandId);
+ menuitem.setAttribute("tooltiptext", aSourceLocation);
+
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain dbg-stackframe-menuitem-title";
+ labelNode.setAttribute("value", aFrameTitle);
+ labelNode.setAttribute("flex", "1");
+
+ let descriptionNode = document.createElement("label");
+ descriptionNode.className = "plain dbg-stackframe-menuitem-details";
+ descriptionNode.setAttribute("value", frameDescription);
+
+ menuitem.appendChild(labelNode);
+ menuitem.appendChild(descriptionNode);
+
+ this._commandset.appendChild(command);
+ this._menupopup.appendChild(menuitem);
+
+ return {
+ command: command,
+ menuitem: menuitem
+ };
+ },
+
+ /**
+ * Function called each time a stack frame item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onStackframeRemoved: function(aItem) {
+ dumpn("Finalizing stackframe item: " + aItem);
+
+ // Destroy the context menu item for the stack frame.
+ let contextItem = aItem.attachment.popup;
+ contextItem.command.remove();
+ contextItem.menuitem.remove();
+ },
+
+ /**
+ * 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.
+ gStackFrames.selectFrame(stackframeItem.attachment.depth);
+
+ // Update the context menu to show the currently selected stackframe item
+ // as a checked entry.
+ for (let otherItem in this) {
+ if (otherItem != stackframeItem) {
+ otherItem.attachment.popup.menuitem.removeAttribute("checked");
+ } else {
+ otherItem.attachment.popup.menuitem.setAttribute("checked", "");
+ }
+ }
+ }
+ },
+
+ /**
+ * The scroll listener for the stackframes container.
+ */
+ _onScroll: function() {
+ // Update the stackframes container only if we have to.
+ if (!this.dirty) {
+ return;
+ }
+ window.clearTimeout(this._scrollTimeout);
+ this._scrollTimeout = window.setTimeout(this._afterScroll, STACK_FRAMES_SCROLL_DELAY);
+ },
+
+ /**
+ * Requests the addition of more frames from the controller.
+ */
+ _afterScroll: function() {
+ // TODO: Accessing private widget properties. Figure out what's the best
+ // way to expose such things. Bug 876271.
+ let list = this.widget._list;
+ let scrollPosition = list.scrollPosition;
+ let scrollWidth = list.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) {
+ list.ensureElementIsVisible(this.getItemAtIndex(CALL_STACK_PAGE_SIZE - 1).target);
+ this.dirty = false;
+
+ // Loads more stack frames from the debugger server cache.
+ DebuggerController.StackFrames.addMoreFrames();
+ }
+ },
+
+ _commandset: null,
+ _menupopup: null,
+ _scrollTimeout: null
+});
+
+/**
+ * Utility functions for handling stackframes.
+ */
+let 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;
+ }
+};
+
+/**
+ * Functions handling the filtering UI.
+ */
+function FilterView() {
+ dumpn("FilterView was instantiated");
+
+ this._onClick = this._onClick.bind(this);
+ this._onSearch = this._onSearch.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._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 = LayoutHelpers.prettyKey(document.getElementById("fileSearchKey"), true);
+ this._globalSearchKey = LayoutHelpers.prettyKey(document.getElementById("globalSearchKey"), true);
+ this._filteredFunctionsKey = LayoutHelpers.prettyKey(document.getElementById("functionSearchKey"), true);
+ this._tokenSearchKey = LayoutHelpers.prettyKey(document.getElementById("tokenSearchKey"), true);
+ this._lineSearchKey = LayoutHelpers.prettyKey(document.getElementById("lineSearchKey"), true);
+ this._variableSearchKey = LayoutHelpers.prettyKey(document.getElementById("variableSearchKey"), true);
+
+ this._searchbox.addEventListener("click", this._onClick, false);
+ this._searchbox.addEventListener("select", this._onSearch, false);
+ this._searchbox.addEventListener("input", this._onSearch, false);
+ this._searchbox.addEventListener("keypress", this._onKeyPress, false);
+ this._searchbox.addEventListener("blur", this._onBlur, false);
+
+ 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._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("searchPanelLine", this._lineSearchKey));
+ this._variableOperatorLabel.setAttribute("value",
+ L10N.getFormatStr("searchPanelVariable", this._variableSearchKey));
+
+ // TODO: bug 806775 - group scripts by globals using hostAnnotations.
+ // if (window._isChromeDebugger) {
+ // this.target = DebuggerView.ChromeGlobals;
+ // } else {
+ this.target = DebuggerView.Sources;
+ // }
+ },
+
+ /**
+ * 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._onSearch, false);
+ this._searchbox.removeEventListener("input", this._onSearch, false);
+ this._searchbox.removeEventListener("keypress", this._onKeyPress, false);
+ this._searchbox.removeEventListener("blur", this._onBlur, false);
+ },
+
+ /**
+ * Sets the target container to be currently filtered.
+ * @param object aView
+ */
+ set target(aView) {
+ let placeholder = "";
+ switch (aView) {
+ case DebuggerView.ChromeGlobals:
+ placeholder = L10N.getFormatStr("emptyChromeGlobalsFilterText", this._fileSearchKey);
+ break;
+ case DebuggerView.Sources:
+ placeholder = L10N.getFormatStr("emptyFilterText", this._fileSearchKey);
+ break;
+ }
+ this._searchbox.setAttribute("placeholder", placeholder);
+ this._target = aView;
+ },
+
+ /**
+ * Gets the target container to be currently filtered.
+ * @return object
+ */
+ get target() this._target,
+
+ /**
+ * Gets the entered file, line and token entered in the searchbox.
+ * @return array
+ */
+ get searchboxInfo() {
+ let operator, file, line, token;
+
+ 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 lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG);
+ let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG);
+
+ // This is not a global, function or variable search, allow file/line flags.
+ if (globalFlagIndex != 0 && functionFlagIndex != 0 && variableFlagIndex != 0) {
+ let fileEnd = lineFlagIndex != -1
+ ? lineFlagIndex
+ : tokenFlagIndex != -1
+ ? tokenFlagIndex
+ : rawLength;
+
+ let lineEnd = tokenFlagIndex != -1
+ ? tokenFlagIndex
+ : rawLength;
+
+ operator = "";
+ file = rawValue.slice(0, fileEnd);
+ line = ~~(rawValue.slice(fileEnd + 1, lineEnd)) || 0;
+ token = rawValue.slice(lineEnd + 1);
+ }
+ // Global searches dissalow the use of file or line flags.
+ else if (globalFlagIndex == 0) {
+ operator = SEARCH_GLOBAL_FLAG;
+ file = "";
+ line = 0;
+ token = rawValue.slice(1);
+ }
+ // Function searches dissalow the use of file or line flags.
+ else if (functionFlagIndex == 0) {
+ operator = SEARCH_FUNCTION_FLAG;
+ file = "";
+ line = 0;
+ token = rawValue.slice(1);
+ }
+ // Variable searches dissalow the use of file or line flags.
+ else if (variableFlagIndex == 0) {
+ operator = SEARCH_VARIABLE_FLAG;
+ file = "";
+ line = 0;
+ token = rawValue.slice(1);
+ }
+
+ return [operator, file, line, token];
+ },
+
+ /**
+ * Returns the current searched operator.
+ * @return string
+ */
+ get currentOperator() this.searchboxInfo[0],
+
+ /**
+ * Returns the currently searched file.
+ * @return string
+ */
+ get searchedFile() this.searchboxInfo[1],
+
+ /**
+ * Returns the currently searched line.
+ * @return number
+ */
+ get searchedLine() this.searchboxInfo[2],
+
+ /**
+ * Returns the currently searched token.
+ * @return string
+ */
+ get searchedToken() this.searchboxInfo[3],
+
+ /**
+ * Clears the text from the searchbox and resets any changed view.
+ */
+ clearSearch: function() {
+ this._searchbox.value = "";
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Performs a file search if necessary.
+ *
+ * @param string aFile
+ * The source location to search for.
+ */
+ _performFileSearch: function(aFile) {
+ // Don't search for files if the input hasn't changed.
+ if (this._prevSearchedFile == aFile) {
+ return;
+ }
+
+ // This is the target container to be currently filtered. Clicking on a
+ // container generally means it should become a target.
+ let view = this._target;
+
+ // If we're not searching for a file anymore, unhide all the items.
+ if (!aFile) {
+ for (let item in view) {
+ item.target.hidden = false;
+ }
+ view.refresh();
+ }
+ // If the searched file string is valid, hide non-matched items.
+ else {
+ let found = false;
+ let lowerCaseFile = aFile.toLowerCase();
+
+ for (let item in view) {
+ let element = item.target;
+ let lowerCaseLabel = item.label.toLowerCase();
+
+ // Search is not case sensitive, and is tied to the label not the value.
+ if (lowerCaseLabel.match(lowerCaseFile)) {
+ element.hidden = false;
+
+ // Automatically select the first match.
+ if (!found) {
+ found = true;
+ view.selectedItem = item;
+ view.refresh();
+ }
+ }
+ // Item not matched, hide the corresponding node.
+ else {
+ element.hidden = true;
+ }
+ }
+ // If no matches were found, display the appropriate info.
+ if (!found) {
+ view.setUnavailable();
+ }
+ }
+ // Synchronize with the view's filtered sources container.
+ DebuggerView.FilteredSources.syncFileSearch();
+
+ // Hide all the groups with no visible children.
+ view.widget.hideEmptyGroups();
+
+ // Ensure the currently selected item is visible.
+ view.widget.ensureSelectionIsVisible({ withGroup: true });
+
+ // Remember the previously searched file to avoid redundant filtering.
+ this._prevSearchedFile = aFile;
+ },
+
+ /**
+ * 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) {
+ // Don't search for lines if the input hasn't changed.
+ if (this._prevSearchedLine != aLine && aLine) {
+ DebuggerView.editor.setCaretPosition(aLine - 1);
+ }
+ // Can't search for lines and tokens at the same time.
+ if (this._prevSearchedToken && !aLine) {
+ this._target.refresh();
+ }
+
+ // Remember the previously searched line to avoid redundant filtering.
+ this._prevSearchedLine = aLine;
+ },
+
+ /**
+ * 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) {
+ // Don't search for tokens if the input hasn't changed.
+ if (this._prevSearchedToken != aToken && aToken) {
+ let editor = DebuggerView.editor;
+ let offset = editor.find(aToken, { ignoreCase: true });
+ if (offset > -1) {
+ editor.setSelection(offset, offset + aToken.length)
+ }
+ }
+ // Can't search for tokens and lines at the same time.
+ if (this._prevSearchedLine && !aToken) {
+ this._target.refresh();
+ }
+
+ // Remember the previously searched token to avoid redundant filtering.
+ this._prevSearchedToken = aToken;
+ },
+
+ /**
+ * The click listener for the search container.
+ */
+ _onClick: function() {
+ this._searchboxHelpPanel.openPopup(this._searchbox);
+ },
+
+ /**
+ * The search listener for the search container.
+ */
+ _onSearch: function() {
+ this._searchboxHelpPanel.hidePopup();
+ let [operator, file, line, token] = this.searchboxInfo;
+
+ // If this is a global search, schedule it for when the user stops typing,
+ // or hide the corresponding pane otherwise.
+ if (operator == SEARCH_GLOBAL_FLAG) {
+ DebuggerView.GlobalSearch.scheduleSearch(token);
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ // If this is a function search, schedule it for when the user stops typing,
+ // or hide the corresponding panel otherwise.
+ if (operator == SEARCH_FUNCTION_FLAG) {
+ DebuggerView.FilteredFunctions.scheduleSearch(token);
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ // If this is a variable search, defer the action to the corresponding
+ // variables view instance.
+ if (operator == SEARCH_VARIABLE_FLAG) {
+ DebuggerView.Variables.scheduleSearch(token);
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ DebuggerView.GlobalSearch.clearView();
+ DebuggerView.FilteredFunctions.clearView();
+
+ this._performFileSearch(file);
+ this._performLineSearch(line);
+ this._performTokenSearch(token);
+ },
+
+ /**
+ * 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);
+
+ let [operator, file, line, token] = this.searchboxInfo;
+ let isGlobal = operator == SEARCH_GLOBAL_FLAG;
+ let isFunction = operator == SEARCH_FUNCTION_FLAG;
+ let isVariable = operator == SEARCH_VARIABLE_FLAG;
+ let action = -1;
+
+ if (file && !line && !token) {
+ var isFileSearch = true;
+ }
+ if (line && !token) {
+ var isLineSearch = true;
+ }
+ if (this._prevSearchedToken != token) {
+ var isDifferentToken = true;
+ }
+
+ // Meta+G and Ctrl+N focus next matches.
+ if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) {
+ action = 0;
+ }
+ // Meta+Shift+G and Ctrl+P focus previous matches.
+ else if ((e.char == "G" && e.metaKey) || e.char == "p" && e.ctrlKey) {
+ action = 1;
+ }
+ // 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 e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ var isReturnKey = true;
+ // fall through
+ case e.DOM_VK_DOWN:
+ action = 0;
+ break;
+ case e.DOM_VK_UP:
+ action = 1;
+ break;
+ case e.DOM_VK_ESCAPE:
+ action = 2;
+ break;
+ }
+
+ if (action == 2) {
+ DebuggerView.editor.focus();
+ return;
+ }
+ if (action == -1 || (!operator && !file && !line && !token)) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Select the next or previous file search entry.
+ if (isFileSearch) {
+ if (isReturnKey) {
+ DebuggerView.FilteredSources.clearView();
+ DebuggerView.editor.focus();
+ this.clearSearch();
+ } else {
+ DebuggerView.FilteredSources[["selectNext", "selectPrev"][action]]();
+ }
+ this._prevSearchedFile = file;
+ return;
+ }
+
+ // Perform a global search based on the specified operator.
+ if (isGlobal) {
+ if (isReturnKey && (isDifferentToken || DebuggerView.GlobalSearch.hidden)) {
+ DebuggerView.GlobalSearch.performSearch(token);
+ } else {
+ DebuggerView.GlobalSearch[["selectNext", "selectPrev"][action]]();
+ }
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ // Perform a function search based on the specified operator.
+ if (isFunction) {
+ if (isReturnKey && (isDifferentToken || DebuggerView.FilteredFunctions.hidden)) {
+ DebuggerView.FilteredFunctions.performSearch(token);
+ } else if (!isReturnKey) {
+ DebuggerView.FilteredFunctions[["selectNext", "selectPrev"][action]]();
+ } else {
+ DebuggerView.FilteredFunctions.clearView();
+ DebuggerView.editor.focus();
+ this.clearSearch();
+ }
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ // Perform a variable search based on the specified operator.
+ if (isVariable) {
+ if (isReturnKey && isDifferentToken) {
+ DebuggerView.Variables.performSearch(token);
+ } else {
+ DebuggerView.Variables.expandFirstSearchResults();
+ }
+ this._prevSearchedToken = token;
+ return;
+ }
+
+ // Increment or decrement the specified line.
+ if (isLineSearch && !isReturnKey) {
+ line += action == 0 ? 1 : -1;
+ let lineCount = DebuggerView.editor.getLineCount();
+ let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line;
+
+ DebuggerView.editor.setCaretPosition(lineTarget - 1);
+ this._searchbox.value = file + SEARCH_LINE_FLAG + lineTarget;
+ this._prevSearchedLine = lineTarget;
+ return;
+ }
+
+ let editor = DebuggerView.editor;
+ let offset = editor[["findNext", "findPrevious"][action]](true);
+ if (offset > -1) {
+ editor.setSelection(offset, offset + token.length)
+ }
+ },
+
+ /**
+ * The blur listener for the search container.
+ */
+ _onBlur: function() {
+ DebuggerView.GlobalSearch.clearView();
+ DebuggerView.FilteredSources.clearView();
+ DebuggerView.FilteredFunctions.clearView();
+ DebuggerView.Variables.performSearch(null);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when a filtering key sequence was pressed.
+ *
+ * @param string aOperator
+ * The operator to use for filtering.
+ */
+ _doSearch: function(aOperator = "") {
+ this._searchbox.focus();
+ this._searchbox.value = ""; // Need to clear value beforehand. Bug 779738.
+ 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() {
+ DebuggerView.Variables.performSearch("");
+ this._doSearch(SEARCH_VARIABLE_FLAG);
+ this._searchboxHelpPanel.hidePopup();
+ },
+
+ /**
+ * Called when the variables focus key sequence was pressed.
+ */
+ _doVariablesFocus: function() {
+ DebuggerView.showInstrumentsPane();
+ 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: "",
+ _target: null,
+ _prevSearchedFile: "",
+ _prevSearchedLine: 0,
+ _prevSearchedToken: ""
+};
+
+/**
+ * Functions handling the filtered sources UI.
+ */
+function FilteredSourcesView() {
+ dumpn("FilteredSourcesView was instantiated");
+
+ 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;
+ },
+
+ /**
+ * Updates the list of sources displayed in this container.
+ */
+ syncFileSearch: function() {
+ this.empty();
+
+ // If there's no currently searched file, or there are no matches found,
+ // hide the popup and avoid creating the view again.
+ if (!DebuggerView.Filtering.searchedFile ||
+ !DebuggerView.Sources.visibleItems.length) {
+ this.hidden = true;
+ return;
+ }
+
+ // Get the currently visible items in the sources container.
+ let visibleItems = DebuggerView.Sources.visibleItems;
+ let displayedItems = visibleItems.slice(0, RESULTS_PANEL_MAX_RESULTS);
+
+ // Append a location item item to this container.
+ for (let item of displayedItems) {
+ let trimmedLabel = SourceUtils.trimUrlLength(item.label);
+ let trimmedValue = SourceUtils.trimUrlLength(item.value, 0, "start");
+ let locationItem = this.push([trimmedLabel, trimmedValue], {
+ relaxed: true, /* this container should allow dupes & degenerates */
+ attachment: {
+ fullLabel: item.label,
+ fullValue: item.value
+ }
+ });
+ }
+
+ // Select the first entry in this container.
+ this.selectedIndex = 0;
+
+ // Only display the results panel if there's at least one entry available.
+ this.hidden = this.itemCount == 0;
+ },
+
+ /**
+ * The click listener for this container.
+ */
+ _onClick: function(e) {
+ let locationItem = this.getItemForElement(e.target);
+ if (locationItem) {
+ this.selectedItem = locationItem;
+ 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) {
+ DebuggerView.updateEditor(locationItem.attachment.fullValue, 0);
+ }
+ }
+});
+
+/**
+ * Functions handling the function search UI.
+ */
+function FilteredFunctionsView() {
+ dumpn("FilteredFunctionsView was instantiated");
+
+ this._performFunctionSearch = this._performFunctionSearch.bind(this);
+ 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;
+ },
+
+ /**
+ * Allows searches to be scheduled and delayed to avoid redundant calls.
+ */
+ delayedSearch: true,
+
+ /**
+ * Schedules searching for a function in all of the sources.
+ *
+ * @param string aQuery
+ * The function to search for.
+ */
+ scheduleSearch: function(aQuery) {
+ if (!this.delayedSearch) {
+ this.performSearch(aQuery);
+ return;
+ }
+ let delay = Math.max(FUNCTION_SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
+
+ window.clearTimeout(this._searchTimeout);
+ this._searchFunction = this._startSearch.bind(this, aQuery);
+ this._searchTimeout = window.setTimeout(this._searchFunction, delay);
+ },
+
+ /**
+ * Immediately searches for a function in all of the sources.
+ *
+ * @param string aQuery
+ * The function to search for.
+ */
+ performSearch: function(aQuery) {
+ window.clearTimeout(this._searchTimeout);
+ this._searchFunction = null;
+ this._startSearch(aQuery);
+ },
+
+ /**
+ * Starts searching for a function in all of the sources.
+ *
+ * @param string aQuery
+ * The function to search for.
+ */
+ _startSearch: function(aQuery) {
+ this._searchedToken = aQuery;
+
+ // Start fetching as many sources as possible, then perform the search.
+ DebuggerController.SourceScripts
+ .getTextForSources(DebuggerView.Sources.values)
+ .then(this._performFunctionSearch);
+ },
+
+ /**
+ * Finds function matches in all the sources stored in the cache, and groups
+ * them by location and line number.
+ */
+ _performFunctionSearch: function(aSources) {
+ // Get the currently searched token from the filtering input.
+ // Continue parsing even if the searched token is an empty string, to
+ // cache the syntax tree nodes generated by the reflection API.
+ let token = this._searchedToken;
+
+ // Make sure the currently displayed source is parsed first. Once the
+ // maximum allowed number of resutls are found, parsing will be halted.
+ let currentUrl = DebuggerView.Sources.selectedValue;
+ aSources.sort(([sourceUrl]) => sourceUrl == currentUrl ? -1 : 1);
+
+ // If not searching for a specific function, only parse the displayed source.
+ if (!token) {
+ aSources.splice(1);
+ }
+
+ // Prepare the results array, containing search details for each source.
+ let searchResults = [];
+
+ for (let [location, contents] of aSources) {
+ let parserMethods = DebuggerController.Parser.get(location, contents);
+ let sourceResults = parserMethods.getNamedFunctionDefinitions(token);
+
+ for (let scriptResult of sourceResults) {
+ for (let parseResult of scriptResult.parseResults) {
+ searchResults.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 (searchResults.length >= RESULTS_PANEL_MAX_RESULTS) {
+ this._syncFunctionSearch(searchResults);
+ return;
+ }
+ }
+ }
+ }
+ // Couldn't reach the maximum allowed number of results, but that's ok,
+ // continue building the UI.
+ this._syncFunctionSearch(searchResults);
+ },
+
+ /**
+ * Updates the list of functions displayed in this container.
+ *
+ * @param array aSearchResults
+ * The results array, containing search details for each source.
+ */
+ _syncFunctionSearch: function(aSearchResults) {
+ this.empty();
+
+ // Show the popup even if the search token is an empty string. If there are
+ // no matches found, hide the popup and avoid creating the view again.
+ if (!aSearchResults.length) {
+ this.hidden = true;
+ 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;
+ }
+
+ // Append a function item to this container.
+ let trimmedLabel = SourceUtils.trimUrlLength(item.displayedName + "()");
+ let trimmedValue = SourceUtils.trimUrlLength(item.sourceUrl, 0, "start");
+ let description = (item.inferredChain || []).join(".");
+
+ let functionItem = this.push([trimmedLabel, trimmedValue, description], {
+ index: -1, /* specifies on which position should the item be appended */
+ relaxed: true, /* this container should allow dupes & degenerates */
+ attachment: item
+ });
+ }
+
+ // Select the first entry in this container.
+ this.selectedIndex = 0;
+
+ // Only display the results panel if there's at least one entry available.
+ this.hidden = this.itemCount == 0;
+ },
+
+ /**
+ * The click listener for this container.
+ */
+ _onClick: function(e) {
+ let functionItem = this.getItemForElement(e.target);
+ if (functionItem) {
+ this.selectedItem = functionItem;
+ DebuggerView.Filtering.clearSearch();
+ }
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: function({ detail: functionItem }) {
+ if (functionItem) {
+ let sourceUrl = functionItem.attachment.sourceUrl;
+ let scriptOffset = functionItem.attachment.scriptOffset;
+ let actualLocation = functionItem.attachment.actualLocation;
+
+ DebuggerView.updateEditor(sourceUrl, actualLocation.start.line, {
+ charOffset: scriptOffset,
+ columnOffset: actualLocation.start.column,
+ noDebug: true
+ });
+ }
+ },
+
+ _searchTimeout: null,
+ _searchFunction: null,
+ _searchedToken: ""
+});
+
+/**
+ * Preliminary setup for the DebuggerView object.
+ */
+DebuggerView.Toolbar = new ToolbarView();
+DebuggerView.Options = new OptionsView();
+DebuggerView.Filtering = new FilterView();
+DebuggerView.FilteredSources = new FilteredSourcesView();
+DebuggerView.FilteredFunctions = new FilteredFunctionsView();
+DebuggerView.ChromeGlobals = new ChromeGlobalsView();
+DebuggerView.StackFrames = new StackFramesView();
diff --git a/browser/devtools/debugger/debugger-view.js b/browser/devtools/debugger/debugger-view.js
new file mode 100644
index 000000000..0ef24446b
--- /dev/null
+++ b/browser/devtools/debugger/debugger-view.js
@@ -0,0 +1,850 @@
+/* -*- 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 SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
+const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
+const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
+const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
+const STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH = 32; // chars
+const STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION = "center";
+const STACK_FRAMES_SCROLL_DELAY = 100; // ms
+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
+const RESULTS_PANEL_POPUP_POSITION = "before_end";
+const RESULTS_PANEL_MAX_RESULTS = 10;
+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 = "*";
+
+/**
+ * Object defining the debugger view components.
+ */
+let DebuggerView = {
+ /**
+ * Initializes the debugger view.
+ *
+ * @param function aCallback
+ * Called after the view finishes initializing.
+ */
+ initialize: function(aCallback) {
+ dumpn("Initializing the DebuggerView");
+
+ this._initializePanes();
+
+ this.Toolbar.initialize();
+ this.Options.initialize();
+ this.Filtering.initialize();
+ this.FilteredSources.initialize();
+ this.FilteredFunctions.initialize();
+ this.ChromeGlobals.initialize();
+ this.StackFrames.initialize();
+ this.Sources.initialize();
+ this.WatchExpressions.initialize();
+ this.GlobalSearch.initialize();
+
+ this._initializeVariablesView();
+ this._initializeEditor(aCallback);
+ },
+
+ /**
+ * Destroys the debugger view.
+ *
+ * @param function aCallback
+ * Called after the view finishes destroying.
+ */
+ destroy: function(aCallback) {
+ dumpn("Destroying the DebuggerView");
+
+ this.Toolbar.destroy();
+ this.Options.destroy();
+ this.Filtering.destroy();
+ this.FilteredSources.destroy();
+ this.FilteredFunctions.destroy();
+ this.ChromeGlobals.destroy();
+ this.StackFrames.destroy();
+ this.Sources.destroy();
+ this.WatchExpressions.destroy();
+ this.GlobalSearch.destroy();
+
+ this._destroyPanes();
+ this._destroyEditor();
+
+ aCallback();
+ },
+
+ /**
+ * Initializes the UI for all the displayed panes.
+ */
+ _initializePanes: function() {
+ dumpn("Initializing the DebuggerView panes");
+
+ this._sourcesPane = document.getElementById("sources-pane");
+ this._instrumentsPane = document.getElementById("instruments-pane");
+ this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
+
+ this._collapsePaneString = L10N.getStr("collapsePanes");
+ this._expandPaneString = L10N.getStr("expandPanes");
+
+ this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
+ this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
+ this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup });
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: function() {
+ dumpn("Destroying the DebuggerView panes");
+
+ Prefs.sourcesWidth = this._sourcesPane.getAttribute("width");
+ Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width");
+
+ this._sourcesPane = 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: DebuggerController.StackFrames.evaluate,
+ lazyEmpty: true
+ });
+
+ // Attach a controller that handles interfacing with the debugger protocol.
+ VariablesViewController.attach(this.Variables, {
+ getGripClient: aObject => gThreadClient.pauseGrip(aObject)
+ });
+
+ // Relay events from the VariablesView.
+ this.Variables.on("fetched", (aEvent, aType) => {
+ switch (aType) {
+ case "variables":
+ window.dispatchEvent(document, "Debugger:FetchedVariables");
+ break;
+ case "properties":
+ window.dispatchEvent(document, "Debugger:FetchedProperties");
+ break;
+ }
+ });
+ },
+
+ /**
+ * Initializes the SourceEditor instance.
+ *
+ * @param function aCallback
+ * Called after the editor finishes initializing.
+ */
+ _initializeEditor: function(aCallback) {
+ dumpn("Initializing the DebuggerView editor");
+
+ let placeholder = document.getElementById("editor");
+ let config = {
+ mode: SourceEditor.MODES.JAVASCRIPT,
+ readOnly: true,
+ showLineNumbers: true,
+ showAnnotationRuler: true,
+ showOverviewRuler: true
+ };
+
+ this.editor = new SourceEditor();
+ this.editor.init(placeholder, config, () => {
+ this._loadingText = L10N.getStr("loadingText");
+ this._onEditorLoad();
+ aCallback();
+ });
+ },
+
+ /**
+ * The load event handler for the source editor, also executing any necessary
+ * post-load operations.
+ */
+ _onEditorLoad: function() {
+ dumpn("Finished loading the DebuggerView editor");
+
+ DebuggerController.Breakpoints.initialize();
+ window.dispatchEvent(document, "Debugger:EditorLoaded", this.editor);
+ this.editor.focus();
+ },
+
+ /**
+ * Destroys the SourceEditor instance and also executes any necessary
+ * post-unload operations.
+ */
+ _destroyEditor: function() {
+ dumpn("Destroying the DebuggerView editor");
+
+ DebuggerController.Breakpoints.destroy();
+ window.dispatchEvent(document, "Debugger:EditorUnloaded", this.editor);
+ },
+
+ /**
+ * 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 = "") {
+ // Avoid setting the editor mode for very large files.
+ if (aTextContent.length >= SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
+ this.editor.setMode(SourceEditor.MODES.TEXT);
+ return;
+ }
+
+ if (aContentType) {
+ if (/javascript/.test(aContentType)) {
+ this.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ } else {
+ this.editor.setMode(SourceEditor.MODES.HTML);
+ }
+ } else if (aTextContent.match(/^\s*</)) {
+ // Use HTML mode for files in which the first non whitespace character is
+ // &lt;, regardless of extension.
+ this.editor.setMode(SourceEditor.MODES.HTML);
+ } else {
+ // Use JS mode for files with .js and .jsm extensions.
+ if (/\.jsm?$/.test(SourceUtils.trimUrlQuery(aUrl))) {
+ this.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ } else {
+ this.editor.setMode(SourceEditor.MODES.TEXT);
+ }
+ }
+ },
+
+ /**
+ * Sets the currently displayed source text in the editor.
+ *
+ * To update the source editor's current caret and debug location based on
+ * a requested url and line, use the DebuggerView.updateEditor method.
+ *
+ * @param object aSource
+ * The source object coming from the active thread.
+ */
+ set editorSource(aSource) {
+ if (!this._isInitialized || this._isDestroyed || this._editorSource == aSource) {
+ return;
+ }
+
+ dumpn("Setting the DebuggerView editor source: " + aSource.url +
+ ", fetched: " + !!aSource._fetched);
+
+ this.editor.setMode(SourceEditor.MODES.TEXT);
+ this.editor.setText(L10N.getStr("loadingText"));
+ this.editor.resetUndo();
+ this._editorSource = aSource;
+
+ DebuggerController.SourceScripts.getTextForSource(aSource).then(([, aText]) => {
+ // Avoid setting an unexpected source. This may happen when fast switching
+ // between sources that haven't been fetched yet.
+ if (this._editorSource != aSource) {
+ return;
+ }
+
+ this.editor.setText(aText);
+ this.editor.resetUndo();
+ this.setEditorMode(aSource.url, aSource.contentType, aText);
+
+ // Update the editor's current caret and debug locations given by the
+ // currently active frame in the stack, if there's one available.
+ this.updateEditor();
+
+ // Synchronize any other components with the currently displayed source.
+ DebuggerView.Sources.selectedValue = aSource.url;
+ DebuggerController.Breakpoints.updateEditorBreakpoints();
+
+ // Notify that we've shown a source file.
+ window.dispatchEvent(document, "Debugger:SourceShown", aSource);
+ },
+ ([, aError]) => {
+ // Rejected. TODO: Bug 884484.
+ let msg = "Error loading: " + aSource.url + "\n" + aError;
+ dumpn(msg);
+ Cu.reportError(msg);
+ });
+ },
+
+ /**
+ * Gets the currently displayed source text in the editor.
+ *
+ * @return object
+ * The source object coming from the active thread.
+ */
+ get editorSource() this._editorSource,
+
+ /**
+ * Update the source editor's current caret and debug location based on
+ * a requested url and line. If unspecified, they default to the location
+ * given by the currently active frame in the stack.
+ *
+ * @param string aUrl [optional]
+ * The target source url.
+ * @param number aLine [optional]
+ * The target line number 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
+ * - noSwitch: don't switch to the source if not currently selected
+ * - noCaret: don't set the caret location at the specified line
+ * - noDebug: don't set the debug location at the specified line
+ */
+ updateEditor: function(aUrl, aLine, aFlags = {}) {
+ if (!this._isInitialized || this._isDestroyed) {
+ return;
+ }
+ // If the location is not specified, default to the location given by
+ // the currently active frame in the stack.
+ if (!aUrl && !aLine) {
+ let cachedFrames = DebuggerController.activeThread.cachedFrames;
+ let currentFrame = DebuggerController.StackFrames.currentFrame;
+ let frame = cachedFrames[currentFrame];
+ if (frame) {
+ let { url, line } = frame.where;
+ this.updateEditor(url, line, { noSwitch: true });
+ }
+ return;
+ }
+
+ dumpn("Updating the DebuggerView editor: " + aUrl + " @ " + aLine +
+ ", flags: " + aFlags.toSource());
+
+ // If the currently displayed source is the requested one, update.
+ if (this.Sources.selectedValue == aUrl) {
+ set(aLine);
+ }
+ // If the requested source exists, display it and update.
+ else if (this.Sources.containsValue(aUrl) && !aFlags.noSwitch) {
+ this.Sources.selectedValue = aUrl;
+ set(aLine);
+ }
+ // Dumb request, invalidate the caret position and debug location.
+ else {
+ set(0);
+ }
+
+ // Updates the source editor's caret position and debug location.
+ // @param number a Line
+ function set(aLine) {
+ let editor = DebuggerView.editor;
+
+ // Handle any additional options for showing the source.
+ if (aFlags.charOffset) {
+ aLine += editor.getLineAtOffset(aFlags.charOffset);
+ }
+ if (aFlags.lineOffset) {
+ aLine += aFlags.lineOffset;
+ }
+ if (!aFlags.noCaret) {
+ editor.setCaretPosition(aLine - 1, aFlags.columnOffset);
+ }
+ if (!aFlags.noDebug) {
+ editor.setDebugLocation(aLine - 1, aFlags.columnOffset);
+ }
+ }
+ },
+
+ /**
+ * Gets the text in the source editor's specified line.
+ *
+ * @param number aLine [optional]
+ * The line to get the text from.
+ * If unspecified, it defaults to the current caret position line.
+ * @return string
+ * The specified line's text.
+ */
+ getEditorLineText: function(aLine) {
+ let line = aLine || this.editor.getCaretPosition().line;
+ let start = this.editor.getLineStart(line);
+ let end = this.editor.getLineEnd(line);
+ return this.editor.getText(start, end);
+ },
+
+ /**
+ * Gets the text in the source editor's selection bounds.
+ *
+ * @return string
+ * The selected text.
+ */
+ getEditorSelectionText: function() {
+ let selection = this.editor.getSelection();
+ return this.editor.getText(selection.start, selection.end);
+ },
+
+ /**
+ * Gets the visibility state of the instruments pane.
+ * @return boolean
+ */
+ get instrumentsPaneHidden()
+ this._instrumentsPane.hasAttribute("pane-collapsed"),
+
+ /**
+ * 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
+ */
+ toggleInstrumentsPane: function(aFlags) {
+ let pane = this._instrumentsPane;
+ let button = this._instrumentsPaneToggleButton;
+
+ ViewHelpers.togglePane(aFlags, pane);
+
+ if (aFlags.visible) {
+ button.removeAttribute("pane-collapsed");
+ button.setAttribute("tooltiptext", this._collapsePaneString);
+ } else {
+ button.setAttribute("pane-collapsed", "");
+ button.setAttribute("tooltiptext", this._expandPaneString);
+ }
+ },
+
+ /**
+ * 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
+ });
+ },
+
+ /**
+ * 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.FilteredSources.clearView();
+ this.FilteredFunctions.clearView();
+ this.GlobalSearch.clearView();
+ this.ChromeGlobals.empty();
+ this.StackFrames.empty();
+ this.Sources.empty();
+ this.Variables.empty();
+
+ if (this.editor) {
+ this.editor.setText("");
+ this.editor.focus();
+ this._editorSource = null;
+ }
+ },
+
+ Toolbar: null,
+ Options: null,
+ Filtering: null,
+ FilteredSources: null,
+ FilteredFunctions: null,
+ GlobalSearch: null,
+ ChromeGlobals: null,
+ StackFrames: null,
+ Sources: null,
+ Variables: null,
+ WatchExpressions: null,
+ _editor: null,
+ _editorSource: null,
+ _loadingText: "",
+ _sourcesPane: null,
+ _instrumentsPane: null,
+ _instrumentsPaneToggleButton: null,
+ _collapsePaneString: "",
+ _expandPaneString: "",
+ _isInitialized: false,
+ _isDestroyed: false
+};
+
+/**
+ * A stacked list of items, compatible with WidgetMethods instances, used for
+ * displaying views like the watch expressions, filtering or search results etc.
+ *
+ * You should never need to access these methods directly, use the wrapped
+ * WidgetMethods instead.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+function ListWidget(aNode) {
+ this._parent = aNode;
+
+ // Create an internal list container.
+ this._list = document.createElement("vbox");
+ 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);
+}
+
+ListWidget.prototype = {
+ /**
+ * Overrides an item's element type (e.g. "vbox" or "hbox") in this container.
+ * @param string aType
+ */
+ itemType: "hbox",
+
+ /**
+ * Customization function for creating an item's UI in this container.
+ *
+ * @param nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param string aLabel
+ * The item's label.
+ * @param string aValue
+ * The item's value.
+ */
+ itemFactory: null,
+
+ /**
+ * Immediately inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param string aLabel
+ * The label displayed in the container.
+ * @param string aValue
+ * The actual internal value of the item.
+ * @param string aDescription [optional]
+ * An optional description of the item.
+ * @param any aAttachment [optional]
+ * Some attached primitive/object.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function(aIndex, aLabel, aValue, aDescription, aAttachment) {
+ let list = this._list;
+ let childNodes = list.childNodes;
+
+ let element = document.createElement(this.itemType);
+ this.itemFactory(element, aAttachment, aLabel, aValue, aDescription);
+ this._removeEmptyNotice();
+
+ element.classList.add("list-widget-item");
+ return list.insertBefore(element, 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;
+ }
+ if (!this._list.hasChildNodes()) {
+ this._appendEmptyNotice();
+ }
+ },
+
+ /**
+ * Immediately 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();
+ }
+ parent.scrollTop = 0;
+ parent.scrollLeft = 0;
+
+ this._selectedItem = null;
+ this._appendEmptyNotice();
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() 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");
+ }
+ }
+ },
+
+ /**
+ * Sets the text displayed permanently in this container's header.
+ * @param string aValue
+ */
+ set permaText(aValue) {
+ if (this._permaTextNode) {
+ this._permaTextNode.setAttribute("value", aValue);
+ }
+ this._permaTextValue = aValue;
+ this._appendPermaNotice();
+ },
+
+ /**
+ * 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 displayed permanently in this container's header.
+ */
+ _appendPermaNotice: function() {
+ if (this._permaTextNode || !this._permaTextValue) {
+ return;
+ }
+
+ let label = document.createElement("label");
+ label.className = "empty list-widget-item";
+ label.setAttribute("value", this._permaTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._permaTextNode = label;
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _appendEmptyNotice: function() {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+
+ let label = document.createElement("label");
+ label.className = "empty list-widget-item";
+ 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;
+ },
+
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _permaTextNode: null,
+ _permaTextValue: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+/**
+ * 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.className = "results-panel";
+ this._panel.setAttribute("level", "top");
+ this._panel.setAttribute("noautofocus", "true");
+ document.documentElement.appendChild(this._panel);
+ }
+ if (!this.widget) {
+ this.widget = new ListWidget(this._panel);
+ this.widget.itemType = "vbox";
+ this.widget.itemFactory = this._createItemView;
+ }
+ }
+ // 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() this._anchor,
+
+ /**
+ * Sets the container panel hidden or visible. It's hidden by default.
+ * @param boolean aFlag
+ */
+ set hidden(aFlag) {
+ if (aFlag) {
+ this._panel.hidePopup();
+ } else {
+ this._panel.openPopup(this._anchor, this.position, this.left, this.top);
+ this.anchor.focus();
+ }
+ },
+
+ /**
+ * Gets this container's visibility state.
+ * @return boolean
+ */
+ get hidden()
+ 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 nsIDOMNode aElementNode
+ * The element associated with the displayed item.
+ * @param any aAttachment
+ * Some attached primitive/object.
+ * @param string aLabel
+ * The item's label.
+ * @param string aValue
+ * The item's value.
+ * @param string aDescription
+ * An optional description of the item.
+ */
+ _createItemView: function(aElementNode, aAttachment, aLabel, aValue, aDescription) {
+ let labelsGroup = document.createElement("hbox");
+
+ if (aDescription) {
+ let preLabelNode = document.createElement("label");
+ preLabelNode.className = "plain results-panel-item-pre";
+ preLabelNode.setAttribute("value", aDescription);
+ labelsGroup.appendChild(preLabelNode);
+ }
+ if (aLabel) {
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain results-panel-item-name";
+ labelNode.setAttribute("value", aLabel);
+ labelsGroup.appendChild(labelNode);
+ }
+
+ let valueNode = document.createElement("label");
+ valueNode.className = "plain results-panel-item-details";
+ valueNode.setAttribute("value", aValue);
+
+ aElementNode.className = "light results-panel-item";
+ aElementNode.appendChild(labelsGroup);
+ aElementNode.appendChild(valueNode);
+ },
+
+ _anchor: null,
+ _panel: null,
+ position: RESULTS_PANEL_POPUP_POSITION,
+ left: 0,
+ top: 0
+});
diff --git a/browser/devtools/debugger/debugger.css b/browser/devtools/debugger/debugger.css
new file mode 100644
index 000000000..7e9967781
--- /dev/null
+++ b/browser/devtools/debugger/debugger.css
@@ -0,0 +1,24 @@
+/* -*- 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/. */
+
+/* Sources search view */
+
+#globalsearch {
+ overflow: auto;
+}
+
+/* Watch expressions view */
+
+#expressions {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* Toolbar */
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
diff --git a/browser/devtools/debugger/debugger.xul b/browser/devtools/debugger/debugger.xul
new file mode 100644
index 000000000..3ea6bf908
--- /dev/null
+++ b/browser/devtools/debugger/debugger.xul
@@ -0,0 +1,375 @@
+<?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://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/debugger.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/debugger.dtd">
+ %debuggerDTD;
+]>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/devtools/source-editor-overlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="960" height="480"
+ persist="screenX screenY width height sizemode">
+
+ <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="debugger-toolbar.js"/>
+ <script type="text/javascript" src="debugger-panes.js"/>
+
+ <commandset id="editMenuCommands"/>
+ <commandset id="sourceEditorCommands"/>
+
+ <commandset id="debuggerCommands">
+ <command id="nextSourceCommand"
+ oncommand="DebuggerView.Sources.selectNextItem()"/>
+ <command id="prevSourceCommand"
+ oncommand="DebuggerView.Sources.selectPrevItem()"/>
+ <command id="resumeCommand"
+ oncommand="DebuggerView.Toolbar._onResumePressed()"/>
+ <command id="stepOverCommand"
+ oncommand="DebuggerView.Toolbar._onStepOverPressed()"/>
+ <command id="stepInCommand"
+ oncommand="DebuggerView.Toolbar._onStepInPressed()"/>
+ <command id="stepOutCommand"
+ oncommand="DebuggerView.Toolbar._onStepOutPressed()"/>
+ <command id="fileSearchCommand"
+ oncommand="DebuggerView.Filtering._doFileSearch()"/>
+ <command id="globalSearchCommand"
+ oncommand="DebuggerView.Filtering._doGlobalSearch()"/>
+ <command id="functionSearchCommand"
+ oncommand="DebuggerView.Filtering._doFunctionSearch()"/>
+ <command id="tokenSearchCommand"
+ oncommand="DebuggerView.Filtering._doTokenSearch()"/>
+ <command id="lineSearchCommand"
+ oncommand="DebuggerView.Filtering._doLineSearch()"/>
+ <command id="variableSearchCommand"
+ oncommand="DebuggerView.Filtering._doVariableSearch()"/>
+ <command id="variablesFocusCommand"
+ oncommand="DebuggerView.Filtering._doVariablesFocus()"/>
+ <command id="addBreakpointCommand"
+ oncommand="DebuggerView.Sources._onCmdAddBreakpoint()"/>
+ <command id="addConditionalBreakpointCommand"
+ oncommand="DebuggerView.Sources._onCmdAddConditionalBreakpoint()"/>
+ <command id="addWatchExpressionCommand"
+ oncommand="DebuggerView.WatchExpressions._onCmdAddExpression()"/>
+ <command id="removeAllWatchExpressionsCommand"
+ oncommand="DebuggerView.WatchExpressions._onCmdRemoveAllExpressions()"/>
+ <command id="togglePauseOnExceptions"
+ oncommand="DebuggerView.Options._togglePauseOnExceptions()"/>
+ <command id="toggleShowPanesOnStartup"
+ oncommand="DebuggerView.Options._toggleShowPanesOnStartup()"/>
+ <command id="toggleShowOnlyEnum"
+ oncommand="DebuggerView.Options._toggleShowVariablesOnlyEnum()"/>
+ <command id="toggleShowVariablesFilterBox"
+ oncommand="DebuggerView.Options._toggleShowVariablesFilterBox()"/>
+ <command id="toggleShowOriginalSource"
+ oncommand="DebuggerView.Options._toggleShowOriginalSource()"/>
+ </commandset>
+
+ <popupset id="debuggerPopupset">
+ <menupopup id="sourceEditorContextMenu"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <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-addAsWatch"
+ label="&debuggerUI.seMenuAddWatch;"
+ key="addWatchExpressionKey"
+ command="addWatchExpressionCommand"/>
+ <menuseparator/>
+ <menuitem id="se-cMenu-copy"/>
+ <menuseparator/>
+ <menuitem id="se-cMenu-selectAll"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findFile"
+ label="&debuggerUI.searchFile;"
+ accesskey="&debuggerUI.searchFile.key;"
+ key="fileSearchKey"
+ command="fileSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findGlobal"
+ label="&debuggerUI.searchGlobal;"
+ accesskey="&debuggerUI.searchGlobal.key;"
+ key="globalSearchKey"
+ command="globalSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findFunction"
+ label="&debuggerUI.searchFunction;"
+ accesskey="&debuggerUI.searchFunction.key;"
+ key="functionSearchKey"
+ command="functionSearchCommand"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findToken"
+ label="&debuggerUI.searchToken;"
+ accesskey="&debuggerUI.searchToken.key;"
+ key="tokenSearchKey"
+ command="tokenSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-findLine"
+ label="&debuggerUI.searchLine;"
+ accesskey="&debuggerUI.searchLine.key;"
+ key="lineSearchKey"
+ command="lineSearchCommand"/>
+ <menuseparator/>
+ <menuitem id="se-dbg-cMenu-findVariable"
+ label="&debuggerUI.searchVariable;"
+ accesskey="&debuggerUI.searchVariable.key;"
+ key="variableSearchKey"
+ command="variableSearchCommand"/>
+ <menuitem id="se-dbg-cMenu-focusVariables"
+ label="&debuggerUI.focusVariables;"
+ accesskey="&debuggerUI.focusVariables.key;"
+ key="variablesFocusKey"
+ command="variablesFocusCommand"/>
+ </menupopup>
+ <menupopup id="debuggerWatchExpressionsContextMenu">
+ <menuitem id="add-watch-expression"
+ label="&debuggerUI.addWatch;"
+ accesskey="&debuggerUI.addWatch.key;"
+ key="addWatchExpressionKey"
+ command="addWatchExpressionCommand"/>
+ <menuitem id="removeAll-watch-expression"
+ label="&debuggerUI.removeAllWatch;"
+ accesskey="&debuggerUI.removeAllWatch.key;"
+ 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="pause-on-exceptions"
+ type="checkbox"
+ label="&debuggerUI.pauseExceptions;"
+ accesskey="&debuggerUI.pauseExceptions.key;"
+ command="togglePauseOnExceptions"/>
+ <menuitem id="show-panes-on-startup"
+ type="checkbox"
+ label="&debuggerUI.showPanesOnInit;"
+ accesskey="&debuggerUI.showPanesOnInit.key;"
+ command="toggleShowPanesOnStartup"/>
+ <menuitem id="show-vars-only-enum"
+ type="checkbox"
+ label="&debuggerUI.showOnlyEnum;"
+ accesskey="&debuggerUI.showOnlyEnum.key;"
+ command="toggleShowOnlyEnum"/>
+ <menuitem id="show-vars-filter-box"
+ type="checkbox"
+ label="&debuggerUI.showVarsFilter;"
+ accesskey="&debuggerUI.showVarsFilter.key;"
+ command="toggleShowVariablesFilterBox"/>
+ <menuitem id="show-original-source"
+ type="checkbox"
+ label="&debuggerUI.showOriginalSource;"
+ accesskey="&debuggerUI.showOriginalSource.key;"
+ command="toggleShowOriginalSource"/>
+ </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.resume;"
+ command="resumeCommand"/>
+ <key id="stepOverKey"
+ keycode="&debuggerUI.stepping.stepOver;"
+ command="stepOverCommand"/>
+ <key id="stepInKey"
+ keycode="&debuggerUI.stepping.stepIn;"
+ command="stepInCommand"/>
+ <key id="stepOutKey"
+ keycode="&debuggerUI.stepping.stepOut;"
+ 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.searchLine.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"/>
+ </keyset>
+
+ <vbox id="body" flex="1">
+ <toolbar class="devtools-toolbar">
+ <hbox id="debugger-controls">
+ <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>
+ <menulist id="chrome-globals"
+ class="devtools-menulist"
+ sizetopopup="none" hidden="true"/>
+ <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-option-toolbarbutton"
+ tooltiptext="&debuggerUI.optsButton.tooltip;"
+ popup="debuggerPrefsContextMenu"
+ tabindex="0"/>
+ </toolbar>
+ <vbox flex="1">
+ <scrollbox id="globalsearch" orient="vertical" hidden="true"/>
+ <splitter class="devtools-horizontal-splitter" hidden="true"/>
+ <hbox flex="1">
+ <vbox id="sources-pane">
+ <vbox id="sources" flex="1"/>
+ </vbox>
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="editor" flex="1"/>
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="instruments-pane" hidden="true">
+ <vbox id="expressions"/>
+ <splitter class="devtools-horizontal-splitter"/>
+ <vbox id="variables" flex="1"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <panel id="searchbox-help-panel"
+ level="top"
+ type="arrow"
+ noautofocus="true"
+ position="before_start">
+ <vbox>
+ <label id="searchbox-panel-description"
+ value="&debuggerUI.searchPanelTitle;"/>
+ <hbox align="center">
+ <button id="global-operator-button"
+ class="searchbox-panel-operator-button"
+ 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"
+ 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"
+ 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"
+ 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"
+ command="variableSearchCommand"/>
+ <label id="variable-operator-label"
+ class="plain searchbox-panel-operator-label"/>
+ </hbox>
+ </vbox>
+ </panel>
+
+ <panel id="conditional-breakpoint-panel"
+ hidden="true"
+ level="top"
+ type="arrow"
+ noautofocus="true">
+ <vbox>
+ <label id="conditional-breakpoint-panel-description"
+ value="&debuggerUI.condBreakPanelTitle;"/>
+ <textbox id="conditional-breakpoint-panel-textbox"/>
+ </vbox>
+ </panel>
+
+ <panel id="resumption-order-panel"
+ type="arrow"
+ noautofocus="true"
+ position="before_start">
+ <hbox align="start">
+ <image class="alert-icon"/>
+ <label id="resumption-panel-desc" class="description"/>
+ </hbox>
+ </panel>
+
+</window>
diff --git a/browser/devtools/debugger/moz.build b/browser/devtools/debugger/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/debugger/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/debugger/test/Makefile.in b/browser/devtools/debugger/test/Makefile.in
new file mode 100644
index 000000000..a912a546c
--- /dev/null
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_TESTS = \
+ browser_dbg_aaa_run_first_leaktest.js \
+ browser_dbg_clean-exit.js \
+ browser_dbg_cmd.js \
+ browser_dbg_cmd_break.js \
+ browser_dbg_debuggerstatement.js \
+ browser_dbg_listtabs-01.js \
+ browser_dbg_listtabs-02.js \
+ browser_dbg_tabactor-01.js \
+ browser_dbg_tabactor-02.js \
+ browser_dbg_globalactor-01.js \
+ browser_dbg_nav-01.js \
+ browser_dbg_propertyview-01.js \
+ browser_dbg_propertyview-02.js \
+ browser_dbg_propertyview-03.js \
+ browser_dbg_propertyview-04.js \
+ browser_dbg_propertyview-05.js \
+ browser_dbg_propertyview-06.js \
+ browser_dbg_propertyview-07.js \
+ browser_dbg_propertyview-08.js \
+ browser_dbg_propertyview-09.js \
+ browser_dbg_propertyview-10.js \
+ browser_dbg_propertyview-11.js \
+ browser_dbg_propertyview-12.js \
+ browser_dbg_propertyview-edit-value.js \
+ browser_dbg_propertyview-edit-watch.js \
+ browser_dbg_propertyview-data-big.js \
+ browser_dbg_propertyview-data-getset-01.js \
+ browser_dbg_propertyview-data-getset-02.js \
+ browser_dbg_propertyview-data.js \
+ browser_dbg_propertyview-filter-04.js \
+ browser_dbg_propertyview-filter-05.js \
+ browser_dbg_propertyview-filter-06.js \
+ browser_dbg_propertyview-filter-07.js \
+ browser_dbg_propertyview-filter-08.js \
+ $(filter disabled-temporarily--bug-782179, browser_dbg_reload-same-script.js) \
+ browser_dbg_reload-preferred-script.js \
+ browser_dbg_pane-collapse.js \
+ browser_dbg_panesize-inner.js \
+ browser_dbg_breadcrumbs-access.js \
+ browser_dbg_stack-01.js \
+ browser_dbg_stack-02.js \
+ browser_dbg_stack-03.js \
+ browser_dbg_stack-04.js \
+ browser_dbg_stack-05.js \
+ browser_dbg_location-changes.js \
+ browser_dbg_location-changes-new.js \
+ browser_dbg_location-changes-blank.js \
+ browser_dbg_location-changes-bp.js \
+ browser_dbg_sources-cache.js \
+ browser_dbg_scripts-switching.js \
+ browser_dbg_scripts-switching-02.js \
+ browser_dbg_scripts-sorting.js \
+ browser_dbg_scripts-searching-01.js \
+ browser_dbg_scripts-searching-02.js \
+ browser_dbg_scripts-searching-03.js \
+ browser_dbg_scripts-searching-04.js \
+ browser_dbg_scripts-searching-05.js \
+ browser_dbg_scripts-searching-06.js \
+ browser_dbg_scripts-searching-07.js \
+ browser_dbg_scripts-searching-08.js \
+ browser_dbg_scripts-searching-files_ui.js \
+ browser_dbg_scripts-searching-popup.js \
+ browser_dbg_function-search.js \
+ browser_dbg_pause-resume.js \
+ browser_dbg_pause-warning.js \
+ browser_dbg_update-editor-mode.js \
+ browser_dbg_select-line.js \
+ browser_dbg_breakpoint-new-script.js \
+ browser_dbg_bug723069_editor-breakpoints.js \
+ browser_dbg_bug723071_editor-breakpoints-pane.js \
+ browser_dbg_bug723071_editor-breakpoints-highlight.js \
+ browser_dbg_bug723071_editor-breakpoints-contextmenu.js \
+ browser_dbg_bug740825_conditional-breakpoints-01.js \
+ browser_dbg_bug740825_conditional-breakpoints-02.js \
+ browser_dbg_bug727429_watch-expressions-01.js \
+ browser_dbg_bug727429_watch-expressions-02.js \
+ browser_dbg_bug731394_editor-contextmenu.js \
+ browser_dbg_bug737803_editor_actual_location.js \
+ browser_dbg_bug786070_hide_nonenums.js \
+ browser_dbg_bug868163_highight_on_pause.js \
+ browser_dbg_displayName.js \
+ browser_dbg_pause-exceptions.js \
+ browser_dbg_multiple-windows.js \
+ browser_dbg_iframes.js \
+ browser_dbg_bfcache.js \
+ browser_dbg_progress-listener-bug.js \
+ browser_dbg_chrome-debugging.js \
+ browser_dbg_source_maps-01.js \
+ browser_dbg_source_maps-02.js \
+ browser_dbg_step-out.js \
+ head.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_PAGES = \
+ browser_dbg_cmd_break.html \
+ browser_dbg_cmd.html \
+ testactors.js \
+ browser_dbg_tab1.html \
+ browser_dbg_tab2.html \
+ browser_dbg_debuggerstatement.html \
+ browser_dbg_stack.html \
+ browser_dbg_script-switching.html \
+ browser_dbg_script-switching-02.html \
+ test-script-switching-01.js \
+ test-script-switching-02.js \
+ browser_dbg_big-data.html \
+ browser_dbg_frame-parameters.html \
+ browser_dbg_update-editor-mode.html \
+ test-editor-mode \
+ browser_dbg_displayName.html \
+ browser_dbg_iframes.html \
+ browser_dbg_with-frame.html \
+ browser_dbg_pause-exceptions.html \
+ browser_dbg_breakpoint-new-script.html \
+ browser_dbg_conditional-breakpoints.html \
+ browser_dbg_watch-expressions.html \
+ browser_dbg_function-search-01.html \
+ browser_dbg_function-search-02.html \
+ test-function-search-01.js \
+ test-function-search-02.js \
+ test-function-search-03.js \
+ binary_search.html \
+ binary_search.coffee \
+ binary_search.js \
+ binary_search.map \
+ test-location-changes-bp.js \
+ test-location-changes-bp.html \
+ test-step-out.html \
+ $(NULL)
+
+# Bug 903750 - Disabled on OSX for intermittent failures
+ifneq ($(OS_ARCH), Darwin)
+MOCHITEST_BROWSER_TESTS += \
+ browser_dbg_propertyview-filter-01.js \
+ $(NULL)
+endif
+
+# Bug 847588, bug 860349, bug 871713
+# Disabled on OSX and Linux for intermittent failures
+ifeq (,$(filter Darwin Linux,$(OS_ARCH)))
+MOCHITEST_BROWSER_TESTS += \
+ browser_dbg_createChrome.js \
+ browser_dbg_propertyview-filter-02.js \
+ browser_dbg_propertyview-filter-03.js \
+ browser_dbg_propertyview-reexpand.js \
+ $(NULL)
+endif
+
+MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/debugger/test/binary_search.coffee b/browser/devtools/debugger/test/binary_search.coffee
new file mode 100644
index 000000000..e3dacdaaa
--- /dev/null
+++ b/browser/devtools/debugger/test/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/browser/devtools/debugger/test/binary_search.html b/browser/devtools/debugger/test/binary_search.html
new file mode 100644
index 000000000..f9615da7c
--- /dev/null
+++ b/browser/devtools/debugger/test/binary_search.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Source Map Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="binary_search.js"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/binary_search.js b/browser/devtools/debugger/test/binary_search.js
new file mode 100644
index 000000000..ab5a77df1
--- /dev/null
+++ b/browser/devtools/debugger/test/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=binary_search.map
+*/
diff --git a/browser/devtools/debugger/test/binary_search.map b/browser/devtools/debugger/test/binary_search.map
new file mode 100644
index 000000000..c5aaeab2f
--- /dev/null
+++ b/browser/devtools/debugger/test/binary_search.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "binary_search.js",
+ "sourceRoot": "",
+ "sources": [
+ "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"
+} \ No newline at end of file
diff --git a/browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js b/browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js
new file mode 100644
index 000000000..a179a2d0a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests if the debugger leaks.
+ * If leaks happen here, there's something very, very fishy going on.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ // 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);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ closeDebuggerAndFinish();
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bfcache.js b/browser/devtools/debugger/test/browser_dbg_bfcache.js
new file mode 100644
index 000000000..0b2b4c1ea
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bfcache.js
@@ -0,0 +1,129 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 scripts when moving
+ * back and forward in the tab.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSources = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testInitialLoad();
+ });
+}
+
+function testInitialLoad() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ executeSoon(function() {
+ validateFirstPage();
+ testLocationChange();
+ });
+ });
+
+ gDebuggee.firstCall();
+}
+
+function testLocationChange()
+{
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+
+ gDebugger.addEventListener("Debugger:AfterSourcesAdded", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ validateSecondPage();
+ testBack();
+ });
+ });
+ });
+ content.location = STACK_URL;
+ });
+}
+
+function testBack()
+{
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired after going back.");
+ info("Still attached to the tab.");
+
+ gDebugger.addEventListener("Debugger:AfterSourcesAdded", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ validateFirstPage();
+ testForward();
+ });
+ });
+ });
+
+ info("Going back.");
+ content.history.back();
+}
+
+function testForward()
+{
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired after going forward.");
+ info("Still attached to the tab.");
+
+ gDebugger.addEventListener("Debugger:AfterSourcesAdded", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ validateSecondPage();
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+
+ info("Going forward.");
+ content.history.forward();
+}
+
+function validateFirstPage() {
+ gSources = gDebugger.DebuggerView.Sources;
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ ok(gDebugger.DebuggerView.Sources.containsLabel("test-script-switching-01.js"),
+ "Found the first script label.");
+ ok(gDebugger.DebuggerView.Sources.containsLabel("test-script-switching-02.js"),
+ "Found the second script label.");
+}
+
+function validateSecondPage() {
+ gSources = gDebugger.DebuggerView.Sources;
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of scripts.");
+
+ ok(gDebugger.DebuggerView.Sources.containsLabel("browser_dbg_stack.html"),
+ "Found the single script label.");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_big-data.html b/browser/devtools/debugger/test/browser_dbg_big-data.html
new file mode 100644
index 000000000..bbdacd6a6
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_big-data.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Big Data Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ function test(aNumber) {
+ var buffer = new ArrayBuffer(aNumber);
+ var z = new Int8Array(buffer);
+ debugger;
+ };
+ function load() {
+ test(10000);
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", load, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js b/browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js
new file mode 100644
index 000000000..a16b8264f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js
@@ -0,0 +1,154 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ let editor = gDebugger.DebuggerView.editor;
+ let sources = gDebugger.DebuggerView.Sources;
+ let stackframes = gDebugger.DebuggerView.StackFrames;
+
+ is(editor.getCaretPosition().line, 5,
+ "The source editor caret position was incorrect (1).");
+ is(sources.selectedLabel, "test-script-switching-02.js",
+ "The currently selected source is incorrect (1).");
+ is(stackframes.selectedIndex, 3,
+ "The currently selected stackframe is incorrect (1).");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(editor.getCaretPosition().line, 6,
+ "The source editor caret position was incorrect (2).");
+ is(sources.selectedLabel, "test-script-switching-02.js",
+ "The currently selected source is incorrect (2).");
+ is(stackframes.selectedIndex, 3,
+ "The currently selected stackframe is incorrect (2).");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(editor.getCaretPosition().line, 5,
+ "The source editor caret position was incorrect (3).");
+ is(sources.selectedLabel, "test-script-switching-02.js",
+ "The currently selected source is incorrect (3).");
+ is(stackframes.selectedIndex, 3,
+ "The currently selected stackframe is incorrect (3).");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ stackframes.selectedItem.target,
+ gDebugger);
+
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(editor.getCaretPosition().line, 5,
+ "The source editor caret position was incorrect (4).");
+ is(sources.selectedLabel, "test-script-switching-02.js",
+ "The currently selected source is incorrect (4).");
+ is(stackframes.selectedIndex, 2,
+ "The currently selected stackframe is incorrect (4).");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ is(editor.getCaretPosition().line, 4,
+ "The source editor caret position was incorrect (5).");
+ is(sources.selectedLabel, "test-script-switching-01.js",
+ "The currently selected source is incorrect (5).");
+ is(stackframes.selectedIndex, 1,
+ "The currently selected stackframe is incorrect (5).");
+
+ EventUtils.sendKey("UP", gDebugger);
+
+ is(editor.getCaretPosition().line, 4,
+ "The source editor caret position was incorrect (6).");
+ is(sources.selectedLabel, "test-script-switching-01.js",
+ "The currently selected source is incorrect (6).");
+ is(stackframes.selectedIndex, 0,
+ "The currently selected stackframe is incorrect (6).");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ is(editor.getCaretPosition().line, 5,
+ "The source editor caret position was incorrect (7).");
+ is(sources.selectedLabel, "test-script-switching-02.js",
+ "The currently selected source is incorrect (7).");
+ is(stackframes.selectedIndex, 3,
+ "The currently selected stackframe is incorrect (7).");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ is(editor.getCaretPosition().line, 4,
+ "The source editor caret position was incorrect (8).");
+ is(sources.selectedLabel, "test-script-switching-01.js",
+ "The currently selected source is incorrect (8).");
+ is(stackframes.selectedIndex, 0,
+ "The currently selected stackframe is incorrect (8).");
+
+ closeDebuggerAndFinish();
+ });
+
+ EventUtils.sendKey("HOME", gDebugger);
+ });
+
+ EventUtils.sendKey("END", gDebugger);
+ });
+
+ EventUtils.sendKey("UP", gDebugger);
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.html b/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.html
new file mode 100644
index 000000000..547afd9d8
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset='utf-8'/>
+<script type="text/javascript">
+function runDebuggerStatement() {
+ debugger;
+}
+function myFunction() {
+ var a = 1;
+ debugger;
+}
+</script>
+</head>
+<body>
+
+<button type="button" onclick="myFunction()">Run</button>
+
+</body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.js b/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.js
new file mode 100644
index 000000000..59ebc49ee
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_breakpoint-new-script.js
@@ -0,0 +1,92 @@
+/* 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 771452: make sure that setting a breakpoint in an inline script doesn't
+// add it twice.
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_breakpoint-new-script.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ testAddBreakpoint();
+ });
+}
+
+function testAddBreakpoint()
+{
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The debugger statement was reached.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have one frame.");
+
+ gPane.addBreakpoint({ url: TAB_URL, line: 9 }, function (aResponse, bpClient) {
+ testResume();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.runDebuggerStatement();
+}
+
+function testResume()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint wasn't hit yet.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("resumed", function() {
+ thread.addOneTimeListener("paused", function() {
+ executeSoon(testBreakpointHit);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"));
+ });
+
+ thread.resume();
+}
+
+function testBreakpointHit()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint was hit.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("paused", function test(aEvent, aPacket) {
+ thread.addOneTimeListener("resumed", function() {
+ executeSoon(closeDebuggerAndFinish);
+ });
+
+ is(aPacket.why.type, "debuggerStatement", "Execution has advanced to the next line.");
+ isnot(aPacket.why.type, "breakpoint", "No ghost breakpoint was hit.");
+ thread.resume();
+ });
+
+ thread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
new file mode 100644
index 000000000..642ffd50c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
@@ -0,0 +1,308 @@
+/* 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 + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gBreakpoints = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onSourceShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onSourceShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onSourceShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ gSources = gDebugger.DebuggerView.Sources;
+ gEditor = gDebugger.editor;
+ gBreakpoints = gPane.getAllBreakpoints();
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ isnot(gEditor.getText().indexOf("debugger"), -1,
+ "The correct script was loaded initially.");
+
+ isnot(gSources.selectedValue, gSources.values[0],
+ "The correct script is selected");
+
+ is(Object.keys(gBreakpoints), 0, "no breakpoints");
+ ok(!gPane.getBreakpoint("foo", 3), "getBreakpoint('foo', 3) returns falsey");
+ is(gEditor.getBreakpoints().length, 0, "no breakpoints in the editor");
+
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddFirst);
+ executeSoon(function() {
+ gPane.addBreakpoint({url: gSources.selectedValue, line: 6}, onBreakpointAddFirst);
+ });
+ }
+
+ let breakpointsAdded = 0;
+ let breakpointsRemoved = 0;
+ let editorBreakpointChanges = 0;
+
+ function onEditorBreakpointAddFirst(aEvent)
+ {
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddFirst);
+ editorBreakpointChanges++;
+
+ ok(aEvent, "breakpoint1 added to the editor");
+ is(aEvent.added.length, 1, "one breakpoint added to the editor");
+ is(aEvent.removed.length, 0, "no breakpoint was removed from the editor");
+ is(aEvent.added[0].line, 5, "editor breakpoint line is correct");
+
+ is(gEditor.getBreakpoints().length, 1,
+ "editor.getBreakpoints().length is correct");
+ }
+
+ function onBreakpointAddFirst(aBreakpointClient, aResponseError)
+ {
+ breakpointsAdded++;
+
+ ok(aBreakpointClient, "breakpoint1 added, client received");
+ ok(!aResponseError, "breakpoint1 added without errors");
+ is(aBreakpointClient.location.url, gSources.selectedValue,
+ "breakpoint1 client url is correct");
+ is(aBreakpointClient.location.line, 6,
+ "breakpoint1 client line is correct");
+
+ executeSoon(function() {
+ ok(aBreakpointClient.actor in gBreakpoints,
+ "breakpoint1 client found in the list of debugger breakpoints");
+ is(Object.keys(gBreakpoints).length, 1,
+ "the list of debugger breakpoints holds only one breakpoint");
+ is(gPane.getBreakpoint(gSources.selectedValue, 6), aBreakpointClient,
+ "getBreakpoint returns the correct breakpoint");
+
+ info("remove the first breakpoint");
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointRemoveFirst);
+ gPane.removeBreakpoint(aBreakpointClient, onBreakpointRemoveFirst);
+ });
+ }
+
+ function onBreakpointRemoveFirst(aLocation)
+ {
+ breakpointsRemoved++;
+
+ ok(aLocation, "breakpoint1 removed");
+ is(aLocation.url, gSources.selectedValue, "breakpoint1 remove: url is correct");
+ is(aLocation.line, 6, "breakpoint1 remove: line is correct");
+
+ executeSoon(testBreakpointAddBackground);
+ }
+
+ function onEditorBreakpointRemoveFirst(aEvent)
+ {
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointRemoveFirst);
+ editorBreakpointChanges++;
+
+ ok(aEvent, "breakpoint1 removed from the editor");
+ is(aEvent.added.length, 0, "no breakpoint was added to the editor");
+ is(aEvent.removed.length, 1, "one breakpoint was removed from the editor");
+ is(aEvent.removed[0].line, 5, "editor breakpoint line is correct");
+
+ is(gEditor.getBreakpoints().length, 0,
+ "editor.getBreakpoints().length is correct");
+ }
+
+ function testBreakpointAddBackground()
+ {
+ info("add a breakpoint to the second script which is not selected");
+
+ is(Object.keys(gBreakpoints).length, 0,
+ "no breakpoints in the debugger");
+ ok(!gPane.getBreakpoint(gSources.selectedValue, 6),
+ "getBreakpoint(selectedScript, 6) returns no breakpoint");
+ isnot(gSources.values[0], gSources.selectedValue,
+ "first script location is not the currently selected script");
+
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddBackgroundTrap);
+ gPane.addBreakpoint({url: gSources.values[0], line: 5}, onBreakpointAddBackground);
+ }
+
+ function onEditorBreakpointAddBackgroundTrap(aEvent)
+ {
+ // Trap listener: no breakpoint must be added to the editor when a
+ // breakpoint is added to a script that is not currently selected.
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddBackgroundTrap);
+ editorBreakpointChanges++;
+ ok(false, "breakpoint2 must not be added to the editor");
+ }
+
+ function onBreakpointAddBackground(aBreakpointClient, aResponseError)
+ {
+ breakpointsAdded++;
+
+ ok(aBreakpointClient, "breakpoint2 added, client received");
+ ok(!aResponseError, "breakpoint2 added without errors");
+ is(aBreakpointClient.location.url, gSources.values[0],
+ "breakpoint2 client url is correct");
+ is(aBreakpointClient.location.line, 5,
+ "breakpoint2 client line is correct");
+
+ executeSoon(function() {
+ ok(aBreakpointClient.actor in gBreakpoints,
+ "breakpoint2 client found in the list of debugger breakpoints");
+
+ is(Object.keys(gBreakpoints).length, 1,
+ "one breakpoint in the debugger");
+ is(gPane.getBreakpoint(gSources.values[0], 5), aBreakpointClient,
+ "getBreakpoint(locations[0], 5) returns the correct breakpoint");
+
+ // remove the trap listener
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddBackgroundTrap);
+
+ info("switch to the second script");
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddSwitch);
+ gEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onEditorTextChanged);
+ gSources.selectedIndex = 0;
+ });
+ }
+
+ function onEditorBreakpointAddSwitch(aEvent)
+ {
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointAddSwitch);
+ editorBreakpointChanges++;
+
+ ok(aEvent, "breakpoint2 added to the editor");
+ is(aEvent.added.length, 1, "one breakpoint added to the editor");
+ is(aEvent.removed.length, 0, "no breakpoint was removed from the editor");
+ is(aEvent.added[0].line, 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 (gDebugger.editor.getText() == gDebugger.L10N.getStr("loadingText")) {
+ return;
+ }
+ // The requested source text has been shown, remove the event listener.
+ gEditor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onEditorTextChanged);
+
+ is(gEditor.getText().indexOf("debugger"), -1,
+ "The second script is no longer displayed.");
+
+ isnot(gEditor.getText().indexOf("firstCall"), -1,
+ "The first script is displayed.");
+
+ let window = gEditor.editorElement.contentWindow;
+ executeSoon(() => window.mozRequestAnimationFrame(onReadyForClick));
+ }
+
+ function onReadyForClick()
+ {
+ info("remove the second breakpoint using the mouse");
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointRemoveSecond);
+
+ let iframe = gEditor.editorElement;
+ 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 lineOffset = gEditor.getLineStart(4);
+ let coords = gEditor.getLocationAtOffset(lineOffset);
+
+ let rect = iframe.getBoundingClientRect();
+ let left = rect.left + 10;
+ let top = rect.top + coords.y + 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)
+ {
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, onEditorBreakpointRemoveSecond);
+ editorBreakpointChanges++;
+
+ ok(aEvent, "breakpoint2 removed from the editor");
+ is(aEvent.added.length, 0, "no breakpoint was added to the editor");
+ is(aEvent.removed.length, 1, "one breakpoint was removed from the editor");
+ is(aEvent.removed[0].line, 4, "editor breakpoint line is correct");
+
+ is(gEditor.getBreakpoints().length, 0,
+ "editor.getBreakpoints().length is correct");
+
+ executeSoon(function() {
+ gDebugger.gClient.addOneTimeListener("resumed", function() {
+ finalCheck();
+ closeDebuggerAndFinish();
+ });
+ gDebugger.DebuggerController.activeThread.resume();
+ });
+ }
+
+ function finalCheck() {
+ is(Object.keys(gBreakpoints).length, 0, "no breakpoint in the debugger");
+ ok(!gPane.getBreakpoint(gSources.values[0], 5),
+ "getBreakpoint(locations[0], 5) returns no breakpoint");
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ 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");
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gBreakpoints = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-contextmenu.js b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-contextmenu.js
new file mode 100644
index 000000000..20bfb39a1
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-contextmenu.js
@@ -0,0 +1,466 @@
+/* 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 + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSources.preferredSource = EXAMPLE_URL + "test-script-switching-02.js";
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let { url, loaded, text } = aEvent.detail;
+ info("Shown source: " + url + ", loaded: " + loaded + ", text:\n" + text);
+ info("Shown label: " + gSources.selectedLabel);
+ info("All labels:" + gSources.labels);
+ if (url.indexOf("-02") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ performTestWhileNotPaused();
+ }
+ });
+ });
+
+ function addBreakpoints(callback) {
+ gPane.addBreakpoint({url: gSources.orderedItems[0].value, line: 5}, function(cl, err) {
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 6}, function(cl, err) {
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 7}, function(cl, err) {
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 8}, function(cl, err) {
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 9}, function(cl, err) {
+ callback();
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function performTestWhileNotPaused()
+ {
+ info("Performing test while not paused...");
+
+ addBreakpoints(function() {
+ initialChecks();
+
+ checkBreakpointToggleSelf(0, function() {
+ checkBreakpointToggleOthers(0, function() {
+ checkBreakpointToggleSelf(1, function() {
+ checkBreakpointToggleOthers(1, function() {
+ checkBreakpointToggleSelf(2, function() {
+ checkBreakpointToggleOthers(2, function() {
+ checkBreakpointToggleSelf(3, function() {
+ checkBreakpointToggleOthers(3, function() {
+ checkBreakpointToggleSelf(4, function() {
+ checkBreakpointToggleOthers(4, function() {
+ testDeleteAll(function() {
+ performTestWhilePaused();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function performTestWhilePaused()
+ {
+ info("Performing test while paused...");
+
+ addBreakpoints(function() {
+ initialChecks();
+
+ pauseAndCheck(function() {
+ checkBreakpointToggleSelf(0, function() {
+ checkBreakpointToggleOthers(0, function() {
+ checkBreakpointToggleSelf(1, function() {
+ checkBreakpointToggleOthers(1, function() {
+ checkBreakpointToggleSelf(2, function() {
+ checkBreakpointToggleOthers(2, function() {
+ checkBreakpointToggleSelf(3, function() {
+ checkBreakpointToggleOthers(3, function() {
+ checkBreakpointToggleSelf(4, function() {
+ checkBreakpointToggleOthers(4, function() {
+ testDeleteAll(function() {
+ closeDebuggerAndFinish();
+ });
+ }, true);
+ });
+ }, true);
+ });
+ }, true);
+ });
+ }, true);
+ });
+ }, true);
+ });
+ });
+ });
+ }
+
+ function pauseAndCheck(callback) {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", function() {
+ pauseAndCallback(function() {
+ is(gSources.selectedLabel, "test-script-switching-01.js",
+ "The currently selected source is incorrect (1).");
+ is(gSources.selectedIndex, 1,
+ "The currently selected source is incorrect (2).");
+
+ waitForCaretPos(4, function() {
+ ok(true, "The editor location is correct after pausing.");
+ callback();
+ });
+ });
+ });
+ }
+
+ function pauseAndCallback(callback) {
+ let scriptShown = false;
+ let framesadded = false;
+ let testContinued = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ scriptShown = true;
+ executeSoon(continueTest);
+ }
+ });
+
+ gDebugger.gThreadClient.addOneTimeListener("framesadded", function() {
+ framesadded = true;
+ executeSoon(continueTest);
+ });
+
+ function continueTest() {
+ if (scriptShown && framesadded && !testContinued) {
+ testContinued = true;
+ callback();
+ }
+ }
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+ }
+
+ function initialChecks() {
+ for (let source in gSources) {
+ for (let breakpoint in source) {
+ let { sourceLocation: url, lineNumber: line, actor } = breakpoint.attachment;
+
+ ok(gPane.getBreakpoint(url, line),
+ "All breakpoint items should have corresponding clients (1).");
+ ok(breakpoint.attachment.actor,
+ "All breakpoint items should have corresponding clients (2).");
+ is(!!breakpoint.attachment.disabled, false,
+ "All breakpoints should initially be enabled.");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let enableSelfId = prefix + "enableSelf-" + actor + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + actor + "-menuitem";
+
+ 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(breakpoint.attachment.view.checkbox.getAttribute("checked"), "true",
+ "All breakpoints should initially have a checked checkbox.");
+ }
+ }
+ }
+
+ function checkBreakpointToggleSelf(index, callback) {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[index],
+ gDebugger);
+
+ let selectedBreakpoint = gSources.selectedBreakpointItem;
+ let { sourceLocation: url, lineNumber: line, actor } = selectedBreakpoint.attachment;
+
+ ok(gPane.getBreakpoint(url, line),
+ "There should be a breakpoint client available (1).");
+ ok(gSources.selectedBreakpointClient,
+ "There should be a breakpoint client available (2).");
+ is(!!selectedBreakpoint.attachment.disabled, false,
+ "The breakpoint should not be disabled yet.");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let enableSelfId = prefix + "enableSelf-" + actor + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + actor + "-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'.");
+
+ waitForCaretPos(selectedBreakpoint.attachment.lineNumber - 1, function() {
+ ok(true, "The editor location is correct (" + index + ").");
+
+ gDebugger.addEventListener("Debugger:BreakpointHidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ ok(!gPane.getBreakpoint(url, line),
+ "There should be no breakpoint client available (2).");
+ ok(!gSources.selectedBreakpointClient,
+ "There should be no breakpoint client available (3).");
+
+ gDebugger.addEventListener("Debugger:BreakpointShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ ok(gPane.getBreakpoint(url, line),
+ "There should be a breakpoint client available (4).");
+ ok(gSources.selectedBreakpointClient,
+ "There should be a breakpoint client available (5).");
+
+ callback();
+ });
+
+ // Test re-disabling this breakpoint.
+ executeSoon(function() {
+ gSources._onEnableSelf(selectedBreakpoint.attachment.actor);
+ is(selectedBreakpoint.attachment.disabled, false,
+ "The current breakpoint should now 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(selectedBreakpoint.attachment.view.checkbox.hasAttribute("checked"),
+ "The breakpoint should now be checked.");
+ });
+ });
+
+ // Test disabling this breakpoint.
+ executeSoon(function() {
+ gSources._onDisableSelf(selectedBreakpoint.attachment.actor);
+ is(selectedBreakpoint.attachment.disabled, true,
+ "The current breakpoint should now 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(!selectedBreakpoint.attachment.view.checkbox.hasAttribute("checked"),
+ "The breakpoint should now be unchecked.");
+ });
+ });
+ }
+
+ function checkBreakpointToggleOthers(index, callback, whilePaused) {
+ let count = 4
+ gDebugger.addEventListener("Debugger:BreakpointHidden", function _onEvent(aEvent) {
+ info(count + " breakpoints remain to be hidden...");
+ if (!(--count)) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ ok(true, "All breakpoints except one were hidden.");
+
+ let selectedBreakpoint = gSources.selectedBreakpointItem;
+ let { sourceLocation: url, lineNumber: line, actor } = selectedBreakpoint.attachment;
+
+ ok(gPane.getBreakpoint(url, line),
+ "There should be a breakpoint client available (6).");
+ ok(gSources.selectedBreakpointClient,
+ "There should be a breakpoint client available (7).");
+ is(!!selectedBreakpoint.attachment.disabled, false,
+ "The targetted breakpoint should not have been disabled.");
+
+ for (let source in gSources) {
+ for (let otherBreakpoint in source) {
+ if (otherBreakpoint != selectedBreakpoint) {
+ ok(!gPane.getBreakpoint(
+ otherBreakpoint.attachment.sourceLocation,
+ otherBreakpoint.attachment.lineNumber),
+ "There should be no breakpoint client for a disabled breakpoint (8).");
+ is(otherBreakpoint.attachment.disabled, true,
+ "Non-targetted breakpoints should have been disabled (9).");
+ }
+ }
+ }
+
+ count = 4;
+ gDebugger.addEventListener("Debugger:BreakpointShown", function _onEvent(aEvent) {
+ info(count + " breakpoints remain to be reshown...");
+ if (!(--count)) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ ok(true, "All breakpoints are now reshown.");
+
+ for (let source in gSources) {
+ for (let someBreakpoint in source) {
+ ok(gPane.getBreakpoint(
+ someBreakpoint.attachment.sourceLocation,
+ someBreakpoint.attachment.lineNumber),
+ "There should be a breakpoint client for all enabled breakpoints (10).");
+ is(someBreakpoint.attachment.disabled, false,
+ "All breakpoints should now have been enabled (11).");
+ }
+ }
+
+ count = 5;
+ gDebugger.addEventListener("Debugger:BreakpointHidden", function _onEvent(aEvent) {
+ info(count + " breakpoints remain to be rehidden...");
+ if (!(--count)) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ ok(true, "All breakpoints are now rehidden.");
+
+ for (let source in gSources) {
+ for (let someBreakpoint in source) {
+ ok(!gPane.getBreakpoint(
+ someBreakpoint.attachment.sourceLocation,
+ someBreakpoint.attachment.lineNumber),
+ "There should be no breakpoint client for a disabled breakpoint (12).");
+ is(someBreakpoint.attachment.disabled, true,
+ "All breakpoints should now have been disabled (13).");
+ }
+ }
+
+ count = 5;
+ gDebugger.addEventListener("Debugger:BreakpointShown", function _onEvent(aEvent) {
+ info(count + " breakpoints remain to be reshown...");
+ if (!(--count)) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ ok(true, "All breakpoints are now rehidden.");
+
+ for (let source in gSources) {
+ for (let someBreakpoint in source) {
+ ok(gPane.getBreakpoint(
+ someBreakpoint.attachment.sourceLocation,
+ someBreakpoint.attachment.lineNumber),
+ "There should be a breakpoint client for all enabled breakpoints (14).");
+ is(someBreakpoint.attachment.disabled, false,
+ "All breakpoints should now have been enabled (15).");
+ }
+ }
+
+ // Done.
+ if (!whilePaused) {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", callback);
+ } else {
+ callback();
+ }
+ }
+ });
+
+ // Test re-enabling all breakpoints.
+ enableAll();
+ }
+ });
+
+ // Test disabling all breakpoints.
+ if (!whilePaused) {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", disableAll);
+ } else {
+ disableAll();
+ }
+ }
+ });
+
+ // Test re-enabling other breakpoints.
+ enableOthers();
+ }
+ });
+
+ // Test disabling other breakpoints.
+ if (!whilePaused) {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", disableOthers);
+ } else {
+ disableOthers();
+ }
+ }
+
+ function testDeleteAll(callback) {
+ let count = 5
+ gDebugger.addEventListener("Debugger:BreakpointHidden", function _onEvent(aEvent) {
+ info(count + " breakpoints remain to be hidden...");
+ if (!(--count)) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ ok(true, "All breakpoints were hidden.");
+
+ ok(!gSources.selectedBreakpointItem,
+ "There should be no breakpoint item available (16).");
+ ok(!gSources.selectedBreakpointClient,
+ "There should be no breakpoint client available (17).");
+
+ for (let source in gSources) {
+ for (let otherBreakpoint in source) {
+ ok(false, "It's a trap!");
+ }
+ }
+
+ // Done.
+ callback();
+ }
+ });
+
+ // Test deleting all breakpoints.
+ deleteAll();
+ }
+
+ function disableOthers() {
+ gSources._onDisableOthers(gSources.selectedBreakpointItem.attachment.actor);
+ }
+ function enableOthers() {
+ gSources._onEnableOthers(gSources.selectedBreakpointItem.attachment.actor);
+ }
+ function disableAll() {
+ gSources._onDisableAll(gSources.selectedBreakpointItem.attachment.actor);
+ }
+ function enableAll() {
+ gSources._onEnableAll(gSources.selectedBreakpointItem.attachment.actor);
+ }
+ function deleteAll() {
+ gSources._onDeleteAll(gSources.selectedBreakpointItem.attachment.actor);
+ }
+
+ function waitForCaretPos(number, callback)
+ {
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gEditor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-highlight.js b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-highlight.js
new file mode 100644
index 000000000..1440cf268
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-highlight.js
@@ -0,0 +1,224 @@
+/* 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 + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSources.preferredSource = EXAMPLE_URL + "test-script-switching-02.js";
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let { url, loaded, text } = aEvent.detail;
+ info("Shown source: " + url + ", loaded: " + loaded + ", text:\n" + text);
+ info("Shown label: " + gSources.selectedLabel);
+ info("All labels:" + gSources.labels);
+ if (url.indexOf("-02") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ performTest();
+ }
+ });
+ });
+
+ function performTest()
+ {
+ initialChecks();
+ gPane.addBreakpoint({url: gSources.orderedItems[0].value, line: 5}, function(cl, err) {
+ initialChecks();
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 6}, function(cl, err) {
+ initialChecks();
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 7}, function(cl, err) {
+ initialChecks();
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 8}, function(cl, err) {
+ initialChecks();
+ gPane.addBreakpoint({url: gSources.orderedItems[1].value, line: 9}, function(cl, err) {
+ initialChecks();
+ testHighlight1();
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function initialChecks() {
+ is(gSources.selectedValue, gSources.orderedItems[1].value,
+ "The currently selected source is incorrect (0).");
+ is(gEditor.getCaretPosition().line, 0,
+ "The editor caret line was incorrect (0).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (0).");
+ }
+
+ function testHighlight1() {
+ gSources.highlightBreakpoint(gSources.orderedItems[0].value, 5);
+ checkHighlight(gSources.orderedItems[0].value, 5);
+
+ is(gSources.selectedValue, gSources.orderedItems[1].value,
+ "The currently selected source is incorrect (1).");
+
+ is(gEditor.getCaretPosition().line, 0,
+ "The editor caret line was incorrect (1).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (1).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[0],
+ gDebugger);
+
+ waitForCaretPos(4, function() {
+ ok(true, "The editor location is correct (1).");
+ testHighlight2();
+ });
+ }
+
+ function testHighlight2() {
+ gSources.highlightBreakpoint(gSources.orderedItems[1].value, 6);
+ checkHighlight(gSources.orderedItems[1].value, 6);
+
+ is(gSources.selectedValue, gSources.orderedItems[0].value,
+ "The currently selected source is incorrect (2).");
+
+ is(gEditor.getCaretPosition().line, 4,
+ "The editor caret line was incorrect (2).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (2).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[1],
+ gDebugger);
+
+ waitForCaretPos(5, function() {
+ ok(true, "The editor location is correct (2).");
+ testHighlight3();
+ });
+ }
+
+ function testHighlight3() {
+ gSources.highlightBreakpoint(gSources.orderedItems[1].value, 7);
+ checkHighlight(gSources.orderedItems[1].value, 7);
+
+ is(gSources.selectedValue, gSources.orderedItems[1].value,
+ "The currently selected source is incorrect (3).");
+
+ is(gEditor.getCaretPosition().line, 5,
+ "The editor caret line was incorrect (3).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (3).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[2],
+ gDebugger);
+
+ waitForCaretPos(6, function() {
+ ok(true, "The editor location is correct (3).");
+ testHighlight4();
+ });
+ }
+
+ function testHighlight4() {
+ gSources.highlightBreakpoint(gSources.orderedItems[1].value, 8);
+ checkHighlight(gSources.orderedItems[1].value, 8);
+
+ is(gSources.selectedValue, gSources.orderedItems[1].value,
+ "The currently selected source is incorrect (4).");
+
+ is(gEditor.getCaretPosition().line, 6,
+ "The editor caret line was incorrect (4).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (4).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[3],
+ gDebugger);
+
+ waitForCaretPos(7, function() {
+ ok(true, "The editor location is correct (4).");
+ testHighlight5();
+ });
+ }
+
+ function testHighlight5() {
+ gSources.highlightBreakpoint(gSources.orderedItems[1].value, 9);
+ checkHighlight(gSources.orderedItems[1].value, 9);
+
+ is(gSources.selectedValue, gSources.orderedItems[1].value,
+ "The currently selected source is incorrect (5).");
+
+ is(gEditor.getCaretPosition().line, 7,
+ "The editor caret line was incorrect (5).");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret column was incorrect (5).");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebugger.document.querySelectorAll(".dbg-breakpoint")[4],
+ gDebugger);
+
+ waitForCaretPos(8, function() {
+ ok(true, "The editor location is correct (5).");
+ closeDebuggerAndFinish();
+ });
+ }
+
+ function checkHighlight(aUrl, aLine) {
+ is(gSources.selectedBreakpointItem, gSources.getBreakpoint(aUrl, aLine),
+ "The currently selected breakpoint item is incorrect.");
+ is(gSources.selectedBreakpointClient, gPane.getBreakpoint(aUrl, aLine),
+ "The currently selected breakpoint client is incorrect.");
+
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, aUrl,
+ "The selected breakpoint item's source location attachment is incorrect.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, aLine,
+ "The selected breakpoint item's source line number is incorrect.");
+
+ ok(gSources.selectedBreakpointItem.target.classList.contains("selected"),
+ "The selected breakpoint item's target should have a selected class.");
+ }
+
+ function waitForCaretPos(number, callback)
+ {
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gEditor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js
new file mode 100644
index 000000000..edce10c27
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js
@@ -0,0 +1,301 @@
+/* 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 scripts in the debuggee.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gBreakpoints = null;
+let gBreakpointsParent = null;
+let gBreakpointsList = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ let breakpointsAdded = 0;
+ let breakpointsDisabled = 0;
+ let breakpointsRemoved = 0;
+
+ function performTest()
+ {
+ gEditor = gDebugger.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gPane.getAllBreakpoints();
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ isnot(gEditor.getText().indexOf("debugger"), -1,
+ "The correct script was loaded initially.");
+
+ isnot(gSources.selectedValue, gSources.values[0],
+ "The correct script is selected");
+
+ is(Object.keys(gBreakpoints).length, 0, "no breakpoints");
+ ok(!gPane.getBreakpoint("chocolate", 3), "getBreakpoint('chocolate', 3) returns falsey");
+ is(gEditor.getBreakpoints().length, 0, "no breakpoints in the editor");
+
+ gBreakpointsParent = gSources.widget._parent;
+ gBreakpointsList = gSources.widget._list;
+
+ is(gBreakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, 0,
+ "No breakpoints should be visible at this point.");
+
+ addBreakpoints(function() {
+ 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(gBreakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, 3,
+ "3 breakpoints should be visible at this point.");
+
+ disableBreakpoints(function() {
+ 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(gBreakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded,
+ "Should have the same number of breakpoints in the pane.");
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsDisabled,
+ "Should have the same number of disabled breakpoints.");
+
+ addBreakpoints(function() {
+ 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(gBreakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.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.");
+
+ removeBreakpoints(function() {
+ is(breakpointsRemoved, 3,
+ "Should have 3 removed breakpoints.");
+
+ is(gBreakpointsParent.childNodes.length, 1, // one sources list
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.childNodes.length, 1, // one sources group
+ "Found junk in the breakpoints container.");
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, 0,
+ "No breakpoints should be visible at this point.");
+
+ executeSoon(function() {
+ gDebugger.gClient.addOneTimeListener("resumed", function() {
+ finalCheck();
+ closeDebuggerAndFinish();
+ });
+ gDebugger.DebuggerController.activeThread.resume();
+ });
+ });
+ });
+ });
+ }, true);
+
+ function addBreakpoints(callback, increment)
+ {
+ let line;
+
+ executeSoon(function()
+ {
+ line = 6;
+ gPane.addBreakpoint({url: gSources.selectedValue, line: line},
+ function(cl, err) {
+ onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+ line = 7;
+ gPane.addBreakpoint({url: gSources.selectedValue, line: line},
+ function(cl, err) {
+ onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+ line = 9;
+ gPane.addBreakpoint({url: gSources.selectedValue, line: line},
+ function(cl, err) {
+ onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+ executeSoon(function() {
+ callback();
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function disableBreakpoints(callback)
+ {
+ let nodes = Array.slice(gBreakpointsList.querySelectorAll(".dbg-breakpoint"));
+ info("Nodes to disable: " + breakpointsAdded);
+
+ is(nodes.length, breakpointsAdded,
+ "The number of nodes to disable is incorrect.");
+
+ Array.forEach(nodes, function(bkp) {
+ info("Disabling breakpoint: " + bkp.id);
+
+ let sourceItem = gSources.getItemForElement(bkp);
+ let breakpointItem = gSources.getItemForElement.call(sourceItem, bkp);
+ let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+ info("Found data: " + breakpointItem.attachment.toSource());
+
+ gSources.disableBreakpoint(url, line, { callback: function() {
+ if (++breakpointsDisabled !== breakpointsAdded) {
+ return;
+ }
+ executeSoon(function() {
+ callback();
+ });
+ }});
+ });
+ }
+
+ function removeBreakpoints(callback)
+ {
+ let nodes = Array.slice(gBreakpointsList.querySelectorAll(".dbg-breakpoint"));
+ info("Nodes to remove: " + breakpointsAdded);
+
+ is(nodes.length, breakpointsAdded,
+ "The number of nodes to remove is incorrect.");
+
+ Array.forEach(nodes, function(bkp) {
+ info("Removing breakpoint: " + bkp.id);
+
+ let sourceItem = gSources.getItemForElement(bkp);
+ let breakpointItem = gSources.getItemForElement.call(sourceItem, bkp);
+ let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
+ info("Found data: " + breakpointItem.attachment.toSource());
+
+ gPane.removeBreakpoint(gPane.getBreakpoint(url, line), function() {
+ if (++breakpointsRemoved !== breakpointsAdded) {
+ return;
+ }
+ executeSoon(function() {
+ callback();
+ });
+ });
+ });
+ }
+
+ function onBreakpointAdd(aBreakpointClient, aResponseError)
+ {
+ if (this.increment) {
+ breakpointsAdded++;
+ }
+
+ is(gBreakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded,
+ this.increment ? "Should have added a breakpoint in the pane."
+ : "Should have the same number of breakpoints in the pane.");
+
+ let id = "breakpoint-" + aBreakpointClient.actor;
+ let bkp = gDebugger.document.getElementById(id);
+ let line = bkp.getElementsByClassName("dbg-breakpoint-line")[0];
+ let text = bkp.getElementsByClassName("dbg-breakpoint-text")[0];
+ let check = bkp.querySelector("checkbox");
+
+ is(bkp.id, id,
+ "Breakpoint element " + id + " found successfully.");
+ is(line.getAttribute("value"), this.line,
+ "The expected information wasn't found in the breakpoint element.");
+ is(text.getAttribute("value"), gDebugger.DebuggerView.getEditorLineText(this.line - 1).trim(),
+ "The expected line text wasn't found in the breakpoint element.");
+ is(check.getAttribute("checked"), "true",
+ "The breakpoint enable checkbox is checked as expected.");
+ }
+ }
+
+ function finalCheck() {
+ is(Object.keys(gBreakpoints).length, 0, "no breakpoint in the debugger");
+ ok(!gPane.getBreakpoint(gSources.values[0], 5),
+ "getBreakpoint(locations[0], 5) returns no breakpoint");
+ }
+
+ registerCleanupFunction(function() {
+ is(breakpointsAdded, 3, "correct number of breakpoints have been added");
+ is(breakpointsDisabled, 3, "correct number of breakpoints have been disabled");
+ is(breakpointsRemoved, 3, "correct number of breakpoints have been removed");
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gBreakpoints = null;
+ gBreakpointsParent = null;
+ gBreakpointsList = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js
new file mode 100644
index 000000000..c4d7fd46f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js
@@ -0,0 +1,241 @@
+/* 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 + "browser_dbg_watch-expressions.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gWatch = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+ performTest();
+ });
+
+ 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);
+ gDebugger.editor.focus();
+ is(gWatch.getAllStrings().length, 1,
+ "Empty watch expressions are automatically removed");
+
+
+ addAndCheckExpressions(2, 0, "a", true);
+ gDebugger.editor.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);
+ gDebugger.editor.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);
+ gDebugger.editor.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.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.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.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");
+
+
+ closeDebuggerAndFinish();
+ }
+
+ function addAndCheckCustomExpression(total, index, string, noBlur) {
+ addAndCheckExpressions(total, index, "", true);
+
+ for (let i = 0; i < string.length; i++) {
+ EventUtils.sendChar(string[i], gDebugger);
+ }
+
+ gDebugger.editor.focus();
+
+ let element = gWatch.getItemAtIndex(index).target;
+
+ is(gWatch.getItemAtIndex(index).attachment.initialExpression, "",
+ "The initial expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, "",
+ "The initial expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getItemAtIndex(index).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getString(index), string,
+ "The expression at index " + index + " should be correct (3)");
+ is(gWatch.getAllStrings()[index], string,
+ "The expression at index " + index + " should be correct (4)");
+ }
+
+ function addAndCheckExpressions(total, index, string, noBlur) {
+ gWatch.addExpression(string);
+
+ is(gWatch.getAllStrings().length, total,
+ "There should be " + total + " watch expressions available (1)");
+ is(gWatch.itemCount, total,
+ "There should be " + total + " watch expressions available (2)");
+
+ ok(gWatch.getItemAtIndex(index),
+ "The expression at index " + index + " should be available");
+ is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+ "The expression at index " + index + " should have an initial expression");
+
+ let element = gWatch.getItemAtIndex(index).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(index),
+ "The correct watch expression item was accessed");
+
+ ok(gWatch.widget.getItemAtIndex(index) instanceof XULElement,
+ "The correct watch expression element was accessed (1)");
+ is(element, gWatch.widget.getItemAtIndex(index),
+ "The correct watch expression element was accessed (2)");
+
+ is(gWatch.getItemForElement(element).attachment.arrowNode.hidden, false,
+ "The arrow node should be visible");
+ is(gWatch.getItemForElement(element).attachment.closeNode.hidden, false,
+ "The close button should be visible");
+ is(gWatch.getItemForElement(element).attachment.inputNode.getAttribute("focused"), "true",
+ "The textbox input should be focused");
+
+ is(gDebugger.DebuggerView.Variables.parentNode.scrollTop, 0,
+ "The variables view should be scrolled to top");
+
+ is(gWatch.orderedItems[0], gWatch.getItemAtIndex(index),
+ "The correct watch expression was added to the cache (1)");
+ is(gWatch.orderedItems[0], gWatch.getItemForElement(element),
+ "The correct watch expression was added to the cache (2)");
+
+ if (!noBlur) {
+ gDebugger.editor.focus();
+
+ is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+ "The initial expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, string,
+ "The initial expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getItemAtIndex(index).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getString(index), string,
+ "The expression at index " + index + " should be correct (3)");
+ is(gWatch.getAllStrings()[index], string,
+ "The expression at index " + index + " should be correct (4)");
+ }
+ }
+
+ function removeAndCheckExpression(total, index, string) {
+ gWatch.removeAt(index);
+
+ is(gWatch.getAllStrings().length, total,
+ "There should be " + total + " watch expressions available (1)");
+ is(gWatch.itemCount, total,
+ "There should be " + total + " watch expressions available (2)");
+
+ ok(gWatch.getItemAtIndex(index),
+ "The expression at index " + index + " should still be available");
+ is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+ "The expression at index " + index + " should still have an initial expression");
+
+ let element = gWatch.getItemAtIndex(index).target;
+
+ is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+ "The initial expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.initialExpression, string,
+ "The initial expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getItemAtIndex(index).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (1)");
+ is(gWatch.getItemForElement(element).attachment.currentExpression, string,
+ "The expression at index " + index + " should be correct (2)");
+
+ is(gWatch.getString(index), string,
+ "The expression at index " + index + " should be correct (3)");
+ is(gWatch.getAllStrings()[index], string,
+ "The expression at index " + index + " should be correct (4)");
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gWatch = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js
new file mode 100644
index 000000000..df24c4d91
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js
@@ -0,0 +1,394 @@
+/* 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 + "browser_dbg_watch-expressions.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gWatch = null;
+let gVars = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+ gVars = gDebugger.DebuggerView.Variables;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+ addExpressions();
+ performTest();
+ });
+
+ function addExpressions()
+ {
+ 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()
+ {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 27,
+ "There should be 27 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() {
+ finishTest();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function finishTest()
+ {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 27,
+ "There should be 27 visible nodes in the watch expressions container");
+
+ closeDebuggerAndFinish();
+ }
+
+ function test1(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test1");
+ checkWatchExpressions("ReferenceError: a is not defined",
+ { type: "object", class: "Object" },
+ { type: "object", class: "String" },
+ { type: "undefined" },
+ 26);
+ callback();
+ });
+ executeSoon(function() {
+ gDebuggee.test(); // ermahgerd!!
+ });
+ }
+
+ function test2(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test2");
+ checkWatchExpressions({ type: "undefined" },
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 26);
+ callback();
+ });
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function test3(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test3");
+ checkWatchExpressions({ type: "object", class: "Object" },
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 26);
+ callback();
+ });
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function test4(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test4");
+ checkWatchExpressions(5,
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 27);
+ callback();
+ });
+ executeSoon(function() {
+ gWatch.addExpression("a = 5");
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ }
+
+ function test5(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test5");
+ checkWatchExpressions(5,
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 27);
+ callback();
+ });
+ executeSoon(function() {
+ gWatch.addExpression("encodeURI(\"\\\")");
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ }
+
+ function test6(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test6");
+ checkWatchExpressions(5,
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 27);
+ callback();
+ })
+ executeSoon(function() {
+ gWatch.addExpression("decodeURI(\"\\\")");
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ }
+
+ function test7(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test7");
+ checkWatchExpressions(5,
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 27);
+ callback();
+ });
+ executeSoon(function() {
+ gWatch.addExpression("?");
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ }
+
+ function test8(callback) {
+ waitForWatchExpressions(function() {
+ info("Performing test8");
+ checkWatchExpressions(5,
+ { type: "object", class: "Window" },
+ { type: "undefined" },
+ "sensational",
+ 27);
+ callback();
+ });
+ executeSoon(function() {
+ gWatch.addExpression("a");
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ }
+
+ function test9(callback) {
+ waitForAfterFramesCleared(function() {
+ info("Performing test9");
+ callback();
+ });
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ function waitForAfterFramesCleared(callback) {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function onClear() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", onClear, false);
+ executeSoon(callback);
+ }, false);
+ }
+
+ function waitForWatchExpressions(callback) {
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+ executeSoon(callback);
+ }, false);
+ }
+
+ function checkWatchExpressions(expected_a,
+ expected_this,
+ expected_prop,
+ expected_arguments,
+ total)
+ {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, total,
+ "There should be " + total + " hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.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 = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, total, "There should be " + total + " 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\" } }");
+
+ 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");
+
+ 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: syntax error", "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_arguments == "object") {
+ is(w14.value.type, expected_arguments.type, "The 14th value type is correct");
+ is(w14.value.class, expected_arguments.class, "The 14th value class is correct");
+ } else {
+ is(w14.value, expected_arguments, "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: syntax error", "The 20th value is correct");
+ is(w21.value, "SyntaxError: syntax error", "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 24st value is correct");
+ is(w25.value, "Error: bazinga", "The 25nd value is correct");
+ is(w26.value, "Error: bazinga", "The 26rd value is correct");
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gWatch = null;
+ gVars = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
new file mode 100644
index 000000000..fc3b2efb5
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
@@ -0,0 +1,127 @@
+/* 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 + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+
+function test()
+{
+ let contextMenu = null;
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onSourceShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded",
+ onFramesAdded);
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onFramesAdded(aEvent) {
+ framesAdded = true;
+ executeSoon(startTest);
+ }
+
+ function onSourceShown(aEvent) {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ testStarted = true;
+ gDebugger.removeEventListener("Debugger:SourceShown", onSourceShown);
+ executeSoon(performTest);
+ }
+ }
+
+ function performTest()
+ {
+ let scripts = gDebugger.DebuggerView.Sources;
+ let editor = gDebugger.editor;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(scripts.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ isnot(editor.getText().indexOf("debugger"), -1,
+ "The correct script was loaded initially.");
+
+ isnot(editor.getText().indexOf("\u263a"), -1,
+ "Unicode characters are converted correctly.");
+
+ contextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
+ ok(contextMenu, "source editor context menupopup");
+ ok(editor.readOnly, "editor is read only");
+
+ editor.focus();
+ editor.setSelection(0, 10);
+
+ contextMenu.addEventListener("popupshown", function onPopupShown() {
+ contextMenu.removeEventListener("popupshown", onPopupShown, false);
+ executeSoon(testContextMenu);
+ }, false);
+ contextMenu.openPopup(editor.editorElement, "overlap", 0, 0, true, false);
+ }
+
+ function testContextMenu()
+ {
+ let document = gDebugger.document;
+
+ ok(document.getElementById("editMenuCommands"),
+ "#editMenuCommands found");
+ ok(!document.getElementById("editMenuKeys"),
+ "#editMenuKeys not found");
+ ok(document.getElementById("sourceEditorCommands"),
+ "#sourceEditorCommands found");
+
+ // Map command ids to their expected disabled state.
+ let commands = {"se-cmd-undo": true, "se-cmd-redo": true,
+ "se-cmd-cut": true, "se-cmd-paste": true,
+ "se-cmd-delete": true, "cmd_findAgain": true,
+ "cmd_findPrevious": true, "cmd_find": false,
+ "cmd_gotoLine": false, "cmd_copy": false,
+ "se-cmd-selectAll": false};
+
+ for (let id in commands) {
+ is(document.getElementById(id).hasAttribute("disabled"), commands[id],
+ id + " hasAttribute('disabled') check");
+ }
+
+ executeSoon(function() {
+ contextMenu.hidePopup();
+ closeDebuggerAndFinish();
+ });
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug737803_editor_actual_location.js b/browser/devtools/debugger/test/browser_dbg_bug737803_editor_actual_location.js
new file mode 100644
index 000000000..79ed9cb32
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug737803_editor_actual_location.js
@@ -0,0 +1,121 @@
+/* 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 + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gSources = null;
+let gEditor = null;
+let gBreakpoints = null;
+
+function test() {
+ let scriptShown = false;
+ let framesAdded = false;
+ let testStarted = false;
+ let resumed = false;
+
+ debug_tab_pane(TAB_URL, function (aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebuggee = aDebuggee;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onSourceShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function () {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function () {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onSourceShown(aEvent) {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest() {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onSourceShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest() {
+ gSources = gDebugger.DebuggerView.Sources;
+ gEditor = gDebugger.editor;
+ gBreakpoints = gPane.getAllBreakpoints();
+ is(Object.keys(gBreakpoints), 0, "There are no breakpoints");
+
+ gEditor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ onEditorBreakpointAdd);
+
+ let location = { url: gSources.selectedValue, line: 4 };
+ executeSoon(function () {
+ gPane.addBreakpoint(location, onBreakpointAdd);
+ });
+ }
+
+ let onBpDebuggerAdd = false;
+ let onBpEditorAdd = false;
+
+ function onBreakpointAdd(aBpClient) {
+ is(aBpClient.location.url, gSources.selectedValue, "URL is the same");
+ is(aBpClient.location.line, 6, "Line number is new");
+ is(aBpClient.requestedLocation.line, 4, "Requested location is correct");
+
+ onBpDebuggerAdd = true;
+ tryFinish();
+ }
+
+ function onEditorBreakpointAdd(aEvent) {
+ gEditor.removeEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ onEditorBreakpointAdd);
+
+ is(gEditor.getBreakpoints().length, 1,
+ "There is only one breakpoint in the editor");
+
+ ok(!gPane.getBreakpoint(gSources.selectedValue, 4),
+ "There are no breakpoints on an invalid line");
+
+ let br = gPane.getBreakpoint(gSources.selectedValue, 6);
+ is(br.location.url, gSources.selectedValue, "URL is correct");
+ is(br.location.line, 6, "Line number is correct");
+
+ onBpEditorAdd = true;
+ tryFinish();
+ }
+
+ function tryFinish() {
+ info("onBpDebuggerAdd: " + onBpDebuggerAdd);
+ info("onBpEditorAdd: " + onBpEditorAdd);
+
+ if (onBpDebuggerAdd && onBpEditorAdd) {
+ closeDebuggerAndFinish();
+ }
+ }
+
+ registerCleanupFunction(function () {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+ gEditor = null;
+ gBreakpoints = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
new file mode 100644
index 000000000..ed5e9c85c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
@@ -0,0 +1,393 @@
+/* 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 + "browser_dbg_conditional-breakpoints.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gBreakpoints = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.ermahgerd(); // ermahgerd!!
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("conditional-breakpoints") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: addBreakpoints }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ gEditor = gDebugger.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gPane.getAllBreakpoints();
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of scripts.");
+
+ isnot(gEditor.getText().indexOf("ermahgerd"), -1,
+ "The correct script was loaded initially.");
+
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct script is selected");
+
+ is(Object.keys(gBreakpoints).length, 13, "thirteen breakpoints");
+ ok(!gPane.getBreakpoint("foo", 3), "getBreakpoint('foo', 3) returns falsey");
+ is(gEditor.getBreakpoints().length, 13, "thirteen breakpoints in the editor");
+
+ executeSoon(test1);
+ }
+
+ function test1(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 14, test2);
+ }
+
+ function test2(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 15, test3);
+ }
+
+ function test3(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 16, test4);
+ }
+
+ function test4(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 17, test5);
+ }
+
+ function test5(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 18, test6);
+ }
+
+ function test6(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 19, test7);
+ }
+
+ function test7(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 21, test8);
+ }
+
+ function test8(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 22, test9);
+ }
+
+ function test9(callback)
+ {
+ resumeAndTestBreakpoint(gSources.selectedValue, 23, test10);
+ }
+
+ function test10(callback)
+ {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+
+ isnot(gSources.selectedItem, null,
+ "There should be a selected script in the scripts pane.")
+ is(gSources.selectedBreakpointItem, null,
+ "There should be no selected breakpoint in the scripts pane.")
+ is(gSources.selectedBreakpointClient, null,
+ "There should be no selected client in the scripts pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(gDebugger.DebuggerView.StackFrames.widget._list.querySelectorAll(".dbg-stackframe").length, 0,
+ "There should be no visible stackframes.");
+ is(gDebugger.DebuggerView.Sources.widget._list.querySelectorAll(".dbg-breakpoint").length, 13,
+ "There should be thirteen visible breakpoints.");
+
+ testReload();
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+ }
+
+ function resumeAndTestBreakpoint(url, line, callback)
+ {
+ resume(line, function() {
+ waitForCaretPos(line - 1, function() {
+ testBreakpoint(gSources.selectedBreakpointItem, gSources.selectedBreakpointClient, url, line, true);
+ callback();
+ });
+ });
+ }
+
+ function testBreakpoint(aBreakpointItem, aBreakpointClient, url, line, editor)
+ {
+ is(aBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(aBreakpointItem.attachment.lineNumber, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!!aBreakpointItem.attachment.disabled, false,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(!!aBreakpointItem.attachment.openPopupFlag, false,
+ "The breakpoint on line " + line + " should not open a popup.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ is(aBreakpointClient.location.url, url,
+ "The breakpoint's client url is correct");
+ is(aBreakpointClient.location.line, line,
+ "The breakpoint's client line is correct");
+ isnot(aBreakpointClient.conditionalExpression, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+
+ if (editor) {
+ is(gEditor.getCaretPosition().line + 1, line,
+ "The editor caret position is not situated on the proper line.");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret position is not situated on the proper column.");
+ }
+ }
+
+ function addBreakpoints(callback)
+ {
+ let currentUrl = gDebugger.DebuggerView.Sources.selectedValue;
+
+ gPane.addBreakpoint({ url: currentUrl, line: 12 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 13 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 14 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 15 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 16 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 17 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 18 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 19 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 20 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 21 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 22 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 23 }, function() {
+ gPane.addBreakpoint({ url: currentUrl, line: 24 }, function() {
+ performTest();
+ }, {
+ conditionalExpression: "b"
+ });
+ }, {
+ conditionalExpression: "a !== null"
+ });
+ }, {
+ conditionalExpression: "a !== undefined"
+ });
+ }, {
+ conditionalExpression: "a"
+ });
+ }, {
+ conditionalExpression: "(function() { return false; })()"
+ });
+ }, {
+ conditionalExpression: "(function() {})"
+ });
+ }, {
+ conditionalExpression: "({})"
+ });
+ }, {
+ conditionalExpression: "/regexp/"
+ });
+ }, {
+ conditionalExpression: "'nasu'"
+ });
+ }, {
+ conditionalExpression: "true"
+ });
+ }, {
+ conditionalExpression: "42"
+ });
+ }, {
+ conditionalExpression: "null"
+ });
+ }, {
+ conditionalExpression: "undefined"
+ });
+ }
+
+ function testReload()
+ {
+ info("Testing reload...");
+
+ function _get(url, line) {
+ return [
+ gDebugger.DebuggerView.Sources.getBreakpoint(url, line),
+ gDebugger.DebuggerController.Breakpoints.getBreakpoint(url, line),
+ url,
+ line,
+ ];
+ }
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown() {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+
+ waitForBreakpoints(13, function() {
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 14));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 15));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 16));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 17));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 18));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 19));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 21));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 22));
+ testBreakpoint.apply(this, _get(gSources.selectedValue, 23));
+
+ isnot(gSources.selectedItem, null,
+ "There should be a selected script in the scripts pane.")
+ is(gSources.selectedBreakpointItem, null,
+ "There should be no selected breakpoint in the scripts pane.")
+ is(gSources.selectedBreakpointClient, null,
+ "There should be no selected client in the scripts pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ closeDebuggerAndFinish();
+ });
+ });
+
+ finalCheck();
+ gDebuggee.location.reload();
+ }
+
+ function finalCheck() {
+ isnot(gEditor.getText().indexOf("ermahgerd"), -1,
+ "The correct script is still loaded.");
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct script is still selected");
+ }
+
+ function resume(expected, callback) {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+ waitForBreakpoint(expected, callback);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }
+
+ let bogusClient = {
+ location: {
+ url: null,
+ line: null
+ }
+ };
+
+ function waitForBreakpoint(expected, callback) {
+ // Poll every few milliseconds until expected breakpoint is hit.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the breakpoint.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if ((gSources.selectedBreakpointClient !== expected) &&
+ (gSources.selectedBreakpointClient || bogusClient).location.line !== expected) {
+ return;
+ }
+ // We arrived at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ function waitForBreakpoints(total, callback)
+ {
+ // Poll every few milliseconds until the breakpoints are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the breakpoints.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gSources.widget._list.querySelectorAll(".dbg-breakpoint").length != total) {
+ return;
+ }
+ // We got all the breakpoints, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ function waitForCaretPos(number, callback)
+ {
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gEditor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gBreakpoints = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js
new file mode 100644
index 000000000..460d8879e
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js
@@ -0,0 +1,583 @@
+/* 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 + "browser_dbg_conditional-breakpoints.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gBreakpoints = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+ resumed = true;
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.ermahgerd(); // ermahgerd!!
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("conditional-breakpoints") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ gEditor = gDebugger.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gBreakpoints = gPane.getAllBreakpoints();
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 1,
+ "Found the expected number of scripts.");
+
+ isnot(gEditor.getText().indexOf("ermahgerd"), -1,
+ "The correct script was loaded initially.");
+
+ is(gSources.selectedValue, gSources.values[0],
+ "The correct script is selected");
+
+ is(Object.keys(gBreakpoints), 0, "no breakpoints");
+ ok(!gPane.getBreakpoint("foo", 3), "getBreakpoint('foo', 3) returns falsey");
+ is(gEditor.getBreakpoints().length, 0, "no breakpoints in the editor");
+
+ executeSoon(addBreakpoint1);
+ }
+
+ function addBreakpoint1()
+ {
+ gPane.addBreakpoint({ url: gSources.selectedValue, line: 12 });
+
+ waitForBreakpoint(12, function() {
+ waitForCaretPos(10, function() {
+ waitForPopup(false, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 12, false, false, false);
+
+ executeSoon(addBreakpoint2);
+ });
+ });
+ });
+ }
+
+ function addBreakpoint2()
+ {
+ gSources._editorContextMenuLineNumber = 12;
+ gSources._onCmdAddBreakpoint();
+
+ waitForBreakpoint(13, function() {
+ waitForCaretPos(12, function() {
+ waitForPopup(false, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 13, false, false, true);
+
+ executeSoon(modBreakpoint2);
+ });
+ });
+ });
+ }
+
+ function modBreakpoint2()
+ {
+ gSources._editorContextMenuLineNumber = 12;
+ gSources._onCmdAddConditionalBreakpoint();
+
+ waitForBreakpoint(13, function() {
+ waitForCaretPos(12, function() {
+ waitForPopup(true, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 13, true, true, true);
+
+ executeSoon(addBreakpoint3);
+ });
+ });
+ });
+ }
+
+ function addBreakpoint3()
+ {
+ gSources._editorContextMenuLineNumber = 13;
+ gSources._onCmdAddConditionalBreakpoint();
+
+ waitForBreakpoint(14, function() {
+ waitForCaretPos(13, function() {
+ waitForPopup(true, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 14, true, true, true);
+
+ executeSoon(modBreakpoint3);
+ });
+ });
+ });
+ }
+
+ function modBreakpoint3()
+ {
+ write("bamboocha");
+ EventUtils.sendKey("RETURN", gDebugger);
+
+ waitForBreakpoint(14, function() {
+ waitForCaretPos(13, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointClient.conditionalExpression, "bamboocha",
+ "The bamboocha expression wasn't fonud on the conditional breakpoint");
+
+ executeSoon(setContextMenu);
+ });
+ });
+ });
+ }
+
+ function setContextMenu()
+ {
+ let contextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
+ info("Testing source editor popup...");
+
+ contextMenu.addEventListener("popupshown", function onPopupShown() {
+ contextMenu.removeEventListener("popupshown", onPopupShown, false);
+ info("Source editor popup shown...");
+
+ contextMenu.addEventListener("popuphidden", function onPopupHidden() {
+ contextMenu.removeEventListener("popuphidden", onPopupHidden, false);
+ info("Source editor popup hidden...");
+
+ is(gSources._editorContextMenuLineNumber, 14,
+ "The context menu line number is incorrect after the popup was hidden.");
+ executeSoon(addBreakpoint4);
+ }, false);
+
+ is(gSources._editorContextMenuLineNumber, 14,
+ "The context menu line number is incorrect after the popup was shown.");
+ contextMenu.hidePopup();
+ }, false);
+
+ is(gSources._editorContextMenuLineNumber, -1,
+ "The context menu line number was incorrect before the popup was shown.");
+ gSources._editorContextMenuLineNumber = 14;
+ contextMenu.openPopup(gEditor.editorElement, "overlap", 0, 0, true, false);
+ }
+
+ function addBreakpoint4()
+ {
+ gEditor.setCaretPosition(14);
+ gSources._onCmdAddBreakpoint();
+
+ waitForBreakpoint(15, function() {
+ waitForCaretPos(14, function() {
+ waitForPopup(false, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 15, false, false, true);
+
+ executeSoon(delBreakpoint4);
+ });
+ });
+ });
+ }
+
+ function delBreakpoint4()
+ {
+ gEditor.setCaretPosition(14);
+ gSources._onCmdAddBreakpoint();
+
+ waitForBreakpoint(null, function() {
+ waitForCaretPos(14, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointItem, null,
+ "There should be no selected breakpoint in the breakpoints pane.")
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ executeSoon(moveHighlight1);
+ });
+ });
+ });
+ }
+
+ function moveHighlight1()
+ {
+ gEditor.setCaretPosition(13);
+
+ waitForBreakpoint(14, function() {
+ waitForCaretPos(13, function() {
+ waitForPopup(false, function() {
+ testBreakpoint(gSources.selectedBreakpointItem,
+ gSources.selectedBreakpointClient,
+ gSources.selectedValue, 14, false, true, true);
+
+ executeSoon(testHighlights1);
+ });
+ });
+ });
+ }
+
+ function testHighlights1()
+ {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 14,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ is(gEditor.getCaretPosition().line, 13,
+ "The source editor caret position should be at line 13");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ gEditor.setCaretPosition(12);
+
+ waitForCaretPos(12, function() {
+ waitForPopup(false, function() {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 13,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ is(gEditor.getCaretPosition().line, 12,
+ "The source editor caret position should be at line 12");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ gEditor.setCaretPosition(11);
+
+ waitForCaretPos(11, function() {
+ waitForPopup(false, function() {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 12,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ is(gEditor.getCaretPosition().line, 11,
+ "The source editor caret position should be at line 11");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ gEditor.setCaretPosition(10);
+
+ waitForCaretPos(10, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointItem, null,
+ "There should not be a selected breakpoint in the breakpoints pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ is(gEditor.getCaretPosition().line, 10,
+ "The source editor caret position should be at line 10");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ gEditor.setCaretPosition(14);
+
+ waitForCaretPos(14, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointItem, null,
+ "There should not be a selected breakpoint in the breakpoints pane.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+ is(gEditor.getCaretPosition().line, 14,
+ "The source editor caret position should be at line 14");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ executeSoon(testHighlights2);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function testHighlights2()
+ {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gSources.widget._list.querySelectorAll(".dbg-breakpoint")[2],
+ gDebugger);
+
+ waitForCaretPos(13, function() {
+ waitForPopup(true, function() {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 14,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, true,
+ "The breakpoint conditional expression popup should be shown.");
+ is(gEditor.getCaretPosition().line, 13,
+ "The source editor caret position should be at line 13");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gSources.widget._list.querySelectorAll(".dbg-breakpoint")[1],
+ gDebugger);
+
+ waitForCaretPos(12, function() {
+ waitForPopup(true, function() {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 13,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, true,
+ "The breakpoint conditional expression popup should be shown.");
+ is(gEditor.getCaretPosition().line, 12,
+ "The source editor caret position should be at line 12");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gSources.widget._list.querySelectorAll(".dbg-breakpoint")[0],
+ gDebugger);
+
+ waitForCaretPos(11, function() {
+ waitForPopup(false, function() {
+ isnot(gSources.selectedBreakpointItem, null,
+ "There should be a selected breakpoint in the breakpoints pane.");
+ is(gSources.selectedBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The selected breakpoint should have the correct location.");
+ is(gSources.selectedBreakpointItem.attachment.lineNumber, 12,
+ "The selected breakpoint should have the correct line number.");
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should be shown.");
+ is(gEditor.getCaretPosition().line, 11,
+ "The source editor caret position should be at line 11");
+ is(gEditor.getCaretPosition().col, 0,
+ "The source editor caret position should be at column 0");
+
+ executeSoon(delBreakpoint2);
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function delBreakpoint2()
+ {
+ gSources._editorContextMenuLineNumber = 12;
+ gSources._onCmdAddBreakpoint();
+
+ waitForBreakpoint(null, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointItem, null,
+ "There should be no selected breakpoint in the breakpoints pane.")
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ executeSoon(delBreakpoint3);
+ });
+ });
+ }
+
+ function delBreakpoint3()
+ {
+ gSources._editorContextMenuLineNumber = 13;
+ gSources._onCmdAddBreakpoint();
+
+ waitForBreakpoint(null, function() {
+ waitForPopup(false, function() {
+ is(gSources.selectedBreakpointItem, null,
+ "There should be no selected breakpoint in the breakpoints pane.")
+ is(gSources._conditionalPopupVisible, false,
+ "The breakpoint conditional expression popup should not be shown.");
+
+ executeSoon(testBreakpoints);
+ });
+ });
+ }
+
+ function testBreakpoints()
+ {
+ is(Object.keys(gBreakpoints).length, 1, "one breakpoint");
+ ok(!gPane.getBreakpoint("foo", 3), "getBreakpoint('foo', 3) returns falsey");
+ is(gEditor.getBreakpoints().length, 1, "one breakpoint in the editor");
+
+ closeDebuggerAndFinish();
+ }
+
+ function testBreakpoint(aBreakpointItem, aBreakpointClient, url, line, popup, conditional, editor)
+ {
+ is(aBreakpointItem.attachment.sourceLocation, gSources.selectedValue,
+ "The breakpoint on line " + line + " wasn't added on the correct source.");
+ is(aBreakpointItem.attachment.lineNumber, line,
+ "The breakpoint on line " + line + " wasn't found.");
+ is(!aBreakpointItem.attachment.disabled, true,
+ "The breakpoint on line " + line + " should be enabled.");
+ is(gSources._conditionalPopupVisible, popup,
+ "The breakpoint conditional expression popup should " + (popup ? "" : "not ") + "be shown.");
+
+ is(aBreakpointClient.location.url, url,
+ "The breakpoint's client url is correct");
+ is(aBreakpointClient.location.line, line,
+ "The breakpoint's client line is correct");
+
+ if (conditional) {
+ isnot(aBreakpointClient.conditionalExpression, undefined,
+ "The breakpoint on line " + line + " should have a conditional expression.");
+ } else {
+ is(aBreakpointClient.conditionalExpression, undefined,
+ "The breakpoint on line " + line + " should not have a conditional expression.");
+ }
+
+ if (editor) {
+ is(gEditor.getCaretPosition().line + 1, line,
+ "The editor caret position is not situated on the proper line.");
+ is(gEditor.getCaretPosition().col, 0,
+ "The editor caret position is not situated on the proper column.");
+ }
+ }
+
+ let bogusClient = {
+ location: {
+ url: null,
+ line: null
+ }
+ };
+
+ function waitForBreakpoint(expected, callback) {
+ // Poll every few milliseconds until expected breakpoint is hit.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the breakpoint.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if ((gSources.selectedBreakpointClient !== expected) &&
+ (gSources.selectedBreakpointClient || bogusClient).location.line !== expected) {
+ return;
+ }
+ // We arrived at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ function waitForCaretPos(number, callback)
+ {
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gEditor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ function waitForPopup(state, callback)
+ {
+ // Poll every few milliseconds until the expression popup is shown.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the popup.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gSources._conditionalPopupVisible != state) {
+ return;
+ }
+ // We got the expression popup at the expected state, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+ }
+
+ function clear() {
+ gSources._cbTextbox.focus();
+ gSources._cbTextbox.value = "";
+ }
+
+ function write(text) {
+ clear();
+ append(text);
+ }
+
+ function append(text) {
+ gSources._cbTextbox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gBreakpoints = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
new file mode 100644
index 000000000..9749073d1
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
@@ -0,0 +1,129 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testNonEnumProperties();
+ });
+}
+
+function testNonEnumProperties() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ 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 variable.
+ testScope.expand();
+ testVar.expand();
+
+ executeSoon(function() {
+ 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();
+
+ executeSoon(function() {
+ 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();
+
+ executeSoon(function() {
+ ok(details.hasAttribute("open"),
+ ".details container should stay visible.");
+
+ ok(nonenum.hasAttribute("open"),
+ ".nonenum container should become visible.");
+
+ // Collapse the variable.
+ testVar.collapse();
+
+ executeSoon(function() {
+ 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();
+
+ executeSoon(function() {
+ 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();
+
+ executeSoon(function() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_bug868163_highight_on_pause.js b/browser/devtools/debugger/test/browser_dbg_bug868163_highight_on_pause.js
new file mode 100644
index 000000000..d8222a3ff
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug868163_highight_on_pause.js
@@ -0,0 +1,78 @@
+/* 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 debugger's tab is highlighted when it is paused and not the
+// currently selected tool.
+
+var gTab = null;
+var gDebugger = null;
+var gToolbox = null;
+var gToolboxTab = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebugger = aPane.panelWin;
+ gToolbox = aPane._toolbox;
+ gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger");
+ testPause();
+ });
+}
+
+function testPause() {
+ is(gDebugger.DebuggerController.activeThread.paused, false,
+ "Should be running after debug_tab_pane.");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ gToolbox.selectTool("webconsole").then(() => {
+ ok(gToolboxTab.classList.contains("highlighted"),
+ "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.classList.contains("highlighted"),
+ "The highlighted class is present");
+ ok(gToolboxTab.hasAttribute("selected") &&
+ gToolboxTab.getAttribute("selected") == "true",
+ "and the tab is selected, so the orange glow will not be present.");
+ }).then(testResume);
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+function testResume() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ 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);
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gTab = null;
+ gDebugger = null;
+ gToolbox = null;
+ gToolboxTab = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_chrome-debugging.js b/browser/devtools/debugger/test/browser_dbg_chrome-debugging.js
new file mode 100644
index 000000000..78e726e66
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_chrome-debugging.js
@@ -0,0 +1,68 @@
+/* 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 chrome debugging works.
+
+var gClient = null;
+var gTab = null;
+var gMozillaTab = null;
+var gThreadClient = null;
+var gNewGlobal = false;
+var gAttached = false;
+var gChromeSource = false;
+
+const DEBUGGER_TAB_URL = EXAMPLE_URL + "browser_dbg_debuggerstatement.html";
+
+function test()
+{
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ gTab = addTab(DEBUGGER_TAB_URL, function() {
+ gClient.listTabs(function(aResponse) {
+ let dbg = aResponse.chromeDebugger;
+ ok(dbg, "Found a chrome debugging actor.");
+
+ gClient.addOneTimeListener("newGlobal", function() gNewGlobal = true);
+ gClient.addListener("newSource", onNewSource);
+
+ gClient.attachThread(dbg, function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ ok(!aResponse.error, "Attached to the chrome debugger.");
+ gAttached = true;
+
+ // Ensure that a new global will be created.
+ gMozillaTab = gBrowser.addTab("about:mozilla");
+
+ finish_test();
+ });
+ });
+ });
+ });
+}
+
+function onNewSource(aEvent, aPacket)
+{
+ gChromeSource = aPacket.source.url.startsWith("chrome:");
+ finish_test();
+}
+
+function finish_test()
+{
+ if (!gAttached || !gChromeSource) {
+ return;
+ }
+ gClient.removeListener("newSource", onNewSource);
+ gThreadClient.resume(function(aResponse) {
+ removeTab(gMozillaTab);
+ removeTab(gTab);
+ gClient.close(function() {
+ ok(gNewGlobal, "Received newGlobal event.");
+ ok(gChromeSource, "Received newSource event for a chrome: script.");
+ finish();
+ });
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_clean-exit.js b/browser/devtools/debugger/test/browser_dbg_clean-exit.js
new file mode 100644
index 000000000..e80f75a2e
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_clean-exit.js
@@ -0,0 +1,43 @@
+/*
+ * 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 gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+const DEBUGGER_TAB_URL = EXAMPLE_URL + "browser_dbg_debuggerstatement.html";
+
+function test() {
+ debug_tab_pane(DEBUGGER_TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testCleanExit();
+ });
+}
+
+function testCleanExit() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(gDebugger.DebuggerController.activeThread.paused, true,
+ "Should be paused after the debugger statement.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ });
+
+ gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_cmd.html b/browser/devtools/debugger/test/browser_dbg_cmd.html
new file mode 100644
index 000000000..30779660a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'/>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="application/javascript;version=1.7"/>
+ let output;
+
+ function init() {
+ output = document.querySelector("input");
+ output.value = "";
+ }
+
+ function doit() {
+ 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>
+</head>
+<body onload="init()">
+ <input type="text" value=""/>
+ <input type="button" value="DOIT" onclick="doit()"/>
+ <br />
+ Use this file to test the following commands:
+ <ul>
+ <li>dbg interrupt</li>
+ <li>dbg continue</li>
+ <li>dbg step over</li>
+ <li>dbg step in</li>
+ <li>dbg step out</li>
+ </ul>
+</body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_cmd.js b/browser/devtools/debugger/test/browser_dbg_cmd.js
new file mode 100644
index 000000000..544eddbaa
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd.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/ */
+
+function test() {
+ const TEST_URI = "http://example.com/browser/browser/devtools/debugger/" +
+ "test/browser_dbg_cmd.html";
+
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ let deferred = Promise.defer();
+
+ let openDone = helpers.audit(options, [{
+ setup: "dbg open",
+ exec: { output: "", completed: false }
+ }]);
+
+ openDone.then(function() {
+ gDevTools.showToolbox(options.target, "jsdebugger").then(function(toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+ ok(dbg, "DebuggerPanel exists");
+
+ function cmd(typed, callback) {
+ dbg._controller.activeThread.addOneTimeListener("paused", callback);
+ helpers.audit(options, [{
+ setup: typed,
+ exec: { output: "" }
+ }]);
+ }
+
+ // Wait for the initial resume...
+ dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
+ info("Starting tests");
+
+ let contentDoc = content.window.document;
+ let output = contentDoc.querySelector("input[type=text]");
+ let btnDoit = contentDoc.querySelector("input[type=button]");
+
+ helpers.audit(options, [{
+ setup: "dbg list",
+ exec: { output: /browser_dbg_cmd.html/ }
+ }]);
+
+ cmd("dbg interrupt", function() {
+ ok(true, "debugger is paused");
+ dbg._controller.activeThread.addOneTimeListener("resumed", function() {
+ ok(true, "debugger continued");
+ dbg._controller.activeThread.addOneTimeListener("paused", function() {
+ cmd("dbg step in", function() {
+ cmd("dbg step in", function() {
+ cmd("dbg step in", function() {
+ is(output.value, "step in", "debugger stepped in");
+ cmd("dbg step over", function() {
+ is(output.value, "step over", "debugger stepped over");
+ cmd("dbg step out", function() {
+ is(output.value, "step out", "debugger stepped out");
+ cmd("dbg continue", function() {
+ cmd("dbg continue", function() {
+ is(output.value, "dbg continue", "debugger continued");
+
+ function closeDebugger(cb) {
+ helpers.audit(options, [{
+ setup: "dbg close",
+ completed: false,
+ exec: { output: "" }
+ }]);
+
+ let toolbox = gDevTools.getToolbox(options.target);
+ if (!toolbox) {
+ ok(true, "Debugger was closed.");
+ cb();
+ } else {
+ toolbox.on("destroyed", function () {
+ ok(true, "Debugger was closed.");
+ cb();
+ });
+ }
+ }
+
+ // We're closing the debugger twice to make sure
+ // 'dbg close' doesn't error when toolbox is already
+ // closed. See bug 884638 for more info.
+
+ closeDebugger(() => {
+ closeDebugger(() => deferred.resolve());
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ EventUtils.sendMouseEvent({type:"click"}, btnDoit);
+ });
+
+ helpers.audit(options, [{
+ setup: "dbg continue",
+ exec: { output: "" }
+ }]);
+ });
+ });
+ });
+ });
+
+ return deferred.promise;
+ }).then(finish);
+} \ No newline at end of file
diff --git a/browser/devtools/debugger/test/browser_dbg_cmd_break.html b/browser/devtools/debugger/test/browser_dbg_cmd_break.html
new file mode 100644
index 000000000..88e19d4ed
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd_break.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Browser GCLI break command test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ function firstCall() {
+ eval("window.line0 = Error().lineNumber; secondCall();");
+ }
+ function secondCall() {
+ eval("debugger;");
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_cmd_break.js b/browser/devtools/debugger/test/browser_dbg_cmd_break.js
new file mode 100644
index 000000000..93e45b615
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd_break.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the break command works as it should
+
+const TEST_URI = "http://example.com/browser/browser/devtools/debugger/" +
+ "test/browser_dbg_cmd_break.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ // To help us run later commands, and clear up after ourselves
+ let client, line0;
+
+ return helpers.audit(options, [
+ {
+ setup: 'break',
+ check: {
+ input: 'break',
+ hints: '',
+ markup: 'IIIII',
+ status: 'ERROR',
+ },
+ },
+ {
+ setup: 'break add',
+ check: {
+ input: 'break add',
+ hints: '',
+ markup: 'IIIIIVIII',
+ status: 'ERROR'
+ },
+ },
+ {
+ setup: 'break add line',
+ check: {
+ input: 'break add line',
+ hints: ' <file> <line>',
+ markup: 'VVVVVVVVVVVVVV',
+ status: 'ERROR'
+ },
+ },
+ {
+ name: 'open toolbox',
+ setup: function() {
+ var deferred = Promise.defer();
+
+ var openDone = gDevTools.showToolbox(options.target, "jsdebugger");
+ openDone.then(function(toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+ ok(dbg, "DebuggerPanel exists");
+
+ // Wait for the initial resume...
+ dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
+ info("Starting tests");
+
+ client = dbg.panelWin.gClient;
+ client.activeThread.addOneTimeListener("framesadded", function() {
+ line0 = '' + options.window.wrappedJSObject.line0;
+ deferred.resolve();
+ });
+
+ // Trigger newScript notifications using eval.
+ content.wrappedJSObject.firstCall();
+ });
+ });
+
+ return deferred.promise;
+ },
+ post: function() {
+ ok(client, "Debugger client exists");
+ is(line0, 10, "line0 is 10");
+ },
+ },
+ {
+ name: 'break add line .../browser_dbg_cmd_break.html 10',
+ setup: function() {
+ // We have to setup in a function to allow line0 to be initialized
+ let line = 'break add line ' + TEST_URI + ' ' + line0;
+ return helpers.setInput(options, line);
+ },
+ check: {
+ hints: '',
+ status: 'VALID',
+ message: '',
+ args: {
+ file: { value: TEST_URI, message: '' },
+ line: { value: 10 }
+ }
+ },
+ exec: {
+ output: 'Added breakpoint',
+ completed: false
+ },
+ },
+ {
+ setup: 'break add line http://example.com/browser/browser/devtools/debugger/test/browser_dbg_cmd_break.html 13',
+ check: {
+ hints: '',
+ status: 'VALID',
+ message: '',
+ args: {
+ file: { value: TEST_URI, message: '' },
+ line: { value: 13 }
+ }
+ },
+ exec: {
+ output: 'Added breakpoint',
+ completed: false
+ },
+ },
+ {
+ setup: 'break list',
+ check: {
+ input: 'break list',
+ hints: '',
+ markup: 'VVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: [
+ /Source/, /Remove/,
+ /cmd_break\.html:10/,
+ /cmd_break\.html:13/
+ ]
+ },
+ },
+ {
+ name: 'cleanup',
+ setup: function() {
+ // a.k.a "return client.activeThread.resume();"
+ var deferred = Promise.defer();
+ client.activeThread.resume(function() {
+ deferred.resolve();
+ });
+ return deferred.promise;
+ },
+ },
+ {
+ setup: 'break del 0',
+ check: {
+ input: 'break del 0',
+ hints: ' -> browser_dbg_cmd_break.html:10',
+ markup: 'VVVVVVVVVVI',
+ status: 'ERROR',
+ args: {
+ breakpoint: {
+ status: 'INCOMPLETE',
+ message: ''
+ },
+ }
+ },
+ },
+ {
+ setup: 'break del browser_dbg_cmd_break.html:10',
+ check: {
+ input: 'break del browser_dbg_cmd_break.html:10',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ breakpoint: { arg: ' browser_dbg_cmd_break.html:10' },
+ }
+ },
+ exec: {
+ output: 'Breakpoint removed',
+ completed: false
+ },
+ },
+ {
+ setup: 'break list',
+ check: {
+ input: 'break list',
+ hints: '',
+ markup: 'VVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: [
+ /Source/, /Remove/,
+ /browser_dbg_cmd_break\.html:13/
+ ]
+ },
+ },
+ {
+ setup: 'break del browser_dbg_cmd_break.html:13',
+ check: {
+ input: 'break del browser_dbg_cmd_break.html:13',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ breakpoint: { arg: ' browser_dbg_cmd_break.html:13' },
+ }
+ },
+ exec: {
+ output: 'Breakpoint removed',
+ completed: false
+ },
+ },
+ {
+ setup: 'break list',
+ check: {
+ input: 'break list',
+ hints: '',
+ markup: 'VVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: 'No breakpoints set'
+ },
+ post: function() {
+ client = undefined;
+
+ let toolbox = gDevTools.getToolbox(options.target);
+ return toolbox.destroy();
+ }
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints.html b/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints.html
new file mode 100644
index 000000000..19a71432e
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Conditional Breakpoints Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <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>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_createChrome.js b/browser/devtools/debugger/test/browser_dbg_createChrome.js
new file mode 100644
index 000000000..a55a152e6
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_createChrome.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/
+ */
+
+// Tests that a chrome debugger can be created in a new process.
+
+var gProcess = null;
+var gTab = null;
+var gDebuggee = null;
+
+function test() {
+ // Windows XP test slaves are terribly slow at this test.
+ requestLongerTimeout(4);
+
+ debug_chrome(STACK_URL, aOnClosing, function(aTab, aDebuggee, aProcess) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gProcess = aProcess;
+
+ info("Starting test");
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ 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._dbgProfile,
+ "The remote debugger profile wasn't created properly!");
+ ok(gProcess._dbgProfile.localDir,
+ "The remote debugger profile doesn't have a localDir...");
+ ok(gProcess._dbgProfile.rootDir,
+ "The remote debugger profile doesn't have a rootDir...");
+ ok(gProcess._dbgProfile.name,
+ "The remote debugger profile doesn't have a name...");
+
+ info("profile localDir: " + gProcess._dbgProfile.localDir.path);
+ info("profile rootDir: " + gProcess._dbgProfile.rootDir.path);
+ info("profile name: " + gProcess._dbgProfile.name);
+
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"]
+ .createInstance(Ci.nsIToolkitProfileService);
+
+ let profile = profileService.getProfileByName(gProcess._dbgProfile.name);
+
+ ok(profile,
+ "The remote debugger profile wasn't *actually* created properly!");
+ is(profile.localDir.path, gProcess._dbgProfile.localDir.path,
+ "The remote debugger profile doesn't have the correct localDir!");
+ is(profile.rootDir.path, gProcess._dbgProfile.rootDir.path,
+ "The remote debugger profile doesn't have the correct rootDir!");
+
+ gProcess.close();
+ }}, 0);
+}
+
+function aOnClosing() {
+ 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 localDir: " + gProcess._dbgProfile.localDir.path);
+ info("profile rootDir: " + gProcess._dbgProfile.rootDir.path);
+ info("profile name: " + gProcess._dbgProfile.name);
+
+ executeSoon(function() {
+ finish();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gProcess = null;
+ gTab = null;
+ gDebuggee = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html
new file mode 100644
index 000000000..a4bd8443b
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset='utf-8'/><title>Browser Debugger Test Tab</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="text/javascript">
+
+function runDebuggerStatement()
+{
+ debugger;
+}
+
+</script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
new file mode 100644
index 000000000..de18429f0
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
@@ -0,0 +1,70 @@
+/* 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 the behavior of the debugger statement.
+
+var gClient = null;
+var gTab = null;
+const DEBUGGER_TAB_URL = EXAMPLE_URL + "browser_dbg_debuggerstatement.html";
+
+function test()
+{
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ gTab = addTab(DEBUGGER_TAB_URL, function() {
+ attach_tab_actor_for_url(gClient, DEBUGGER_TAB_URL, function(actor, response) {
+ test_early_debugger_statement(response);
+ });
+ });
+ });
+}
+
+function test_early_debugger_statement(aActor)
+{
+ let paused = function(aEvent, aPacket) {
+ ok(false, "Pause shouldn't be called before we've attached!\n");
+ finish_test();
+ };
+ gClient.addListener("paused", paused);
+ // This should continue without nesting an event loop and calling
+ // the onPaused hook, because we haven't attached yet.
+ gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
+
+ gClient.removeListener("paused", paused);
+
+ // Now attach and resume...
+ gClient.request({ to: aActor.threadActor, type: "attach" }, function(aResponse) {
+ gClient.request({ to: aActor.threadActor, type: "resume" }, function(aResponse) {
+ test_debugger_statement(aActor);
+ });
+ });
+}
+
+function test_debugger_statement(aActor)
+{
+ var stopped = false;
+ gClient.addListener("paused", function(aEvent, aPacket) {
+ stopped = true;
+
+ gClient.request({ to: aActor.threadActor, type: "resume" }, function() {
+ finish_test();
+ });
+ });
+
+ // Reach around the debugging protocol and execute the debugger
+ // statement.
+ gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
+ ok(stopped, "Should trigger the pause handler on a debugger statement.");
+}
+
+function finish_test()
+{
+ removeTab(gTab);
+ gClient.close(function() {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_displayName.html b/browser/devtools/debugger/test/browser_dbg_displayName.html
new file mode 100644
index 000000000..3311a5ad1
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_displayName.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset='utf-8'/><title>Browser Debugger Test Tab</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<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>
+</head>
+
+<body></body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_displayName.js b/browser/devtools/debugger/test/browser_dbg_displayName.js
new file mode 100644
index 000000000..40f07a255
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_displayName.js
@@ -0,0 +1,78 @@
+/*
+ * 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.
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_displayName.html";
+
+function test() {
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testAnonCall();
+ });
+}
+
+function testAnonCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+
+ is(frames.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"),
+ "anonFunc", "Frame name should be anonFunc");
+
+ testInferredName();
+ }}, 0);
+ });
+
+ gDebuggee.evalCall();
+}
+
+function testInferredName() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+
+ is(frames.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"),
+ "a/<", "Frame name should be a/<");
+
+ resumeAndFinish();
+ }}, 0);
+ });
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+function resumeAndFinish() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ removeTab(gTab);
+ gPane = null;
+ gDebuggee = null;
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_frame-parameters.html b/browser/devtools/debugger/test/browser_dbg_frame-parameters.html
new file mode 100644
index 000000000..ed82aaad4
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_frame-parameters.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Function Call Parameter Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ function test(aArg, bArg, cArg, dArg, eArg, fArg) {
+ var a = 1;
+ var b = { a: a };
+ var c = { a: 1, b: "beta", c: true, d: b };
+ var myVar = {
+ _prop: 42,
+ get prop() { return this._prop; },
+ set prop(val) { this._prop = val; }
+ };
+
+ debugger;
+ }
+ function load() {
+ var a = { a: 1, b: "beta", c: true };
+ var e = eval("test(a, 'beta', 3, false, null)");
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", load, false);
+ var buttonAsProto = Object.create(button);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_function-search-01.html b/browser/devtools/debugger/test/browser_dbg_function-search-01.html
new file mode 100644
index 000000000..9d970a633
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_function-search-01.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Function Search</title>
+ <script type="text/javascript" src="test-function-search-01.js"></script>
+ <script type="text/javascript" src="test-function-search-02.js"></script>
+ <script type="text/javascript" src="test-function-search-03.js"></script>
+ </head>
+
+ <body>
+ <p>Peanut butter jelly time!</p>
+ </body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_function-search-02.html b/browser/devtools/debugger/test/browser_dbg_function-search-02.html
new file mode 100644
index 000000000..f06b19f1a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_function-search-02.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Function Search</title>
+ <script type="text/javascript" src="test-function-search-01.js"></script>
+ <script type="text/javascript" src="test-function-search-02.js"></script>
+ <script type="text/javascript" src="test-function-search-03.js"></script>
+ </head>
+
+ <body>
+ <p>Peanut butter jelly time!</p>
+
+ <script type="text/javascript;version=1.8">
+ function inline() {}
+ let arrow = () => {}
+
+ let foo = bar => {}
+ let foo2 = bar2 = baz2 => 42;
+
+ setTimeout((foo, bar, baz) => {});
+ setTimeout((foo, bar, baz) => 42);
+ </script>
+
+ </body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_function-search.js b/browser/devtools/debugger/test/browser_dbg_function-search.js
new file mode 100644
index 000000000..4fbac5d81
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_function-search.js
@@ -0,0 +1,499 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_function-search-02.html";
+
+/**
+ * Tests if the function searching works properly.
+ */
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gSearchBox = null;
+let gFilteredFunctions = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ Services.tm.currentThread.dispatch({ run: testFunctionsFilter }, 0);
+ });
+ });
+}
+
+function testFunctionsFilter()
+{
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gFilteredFunctions = gDebugger.DebuggerView.FilteredFunctions;
+
+ htmlSearch(function() {
+ showSource("test-function-search-01.js", function() {
+ firstSearch(function() {
+ showSource("test-function-search-02.js", function() {
+ secondSearch(function() {
+ showSource("test-function-search-03.js", function() {
+ thirdSearch(function() {
+ saveSearch(function() {
+ filterSearch(function() {
+ bogusSearch(function() {
+ anotherSearch(function() {
+ emptySearch(function() {
+ closeDebuggerAndFinish();
+ });
+ })
+ })
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
+
+function htmlSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ ok(gFilteredFunctions.selectedValue,
+ "An item should be selected in the filtered functions view");
+ ok(gFilteredFunctions.selectedLabel,
+ "An item should be selected in the filtered functions view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.html") != -1) {
+
+ executeSoon(function() {
+ let expectedResults = [
+ ["inline", "-02.html", "", 16, 15],
+ ["arrow", "-02.html", "", 17, 10],
+ ["foo", "-02.html", "", 19, 10],
+ ["foo2", "-02.html", "", 20, 10],
+ ["bar2", "-02.html", "", 20, 17]
+ ];
+
+ for (let [label, value, description, line, col] of expectedResults) {
+ is(gFilteredFunctions.selectedItem.label,
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The corect label (" + label + ") is currently selected.");
+ ok(gFilteredFunctions.selectedItem.value.contains(value),
+ "The corect value (" + value + ") is attached.");
+ is(gFilteredFunctions.selectedItem.description, description,
+ "The corect description (" + description + ") is currently shown.");
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource());
+ ok(gEditor.getCaretPosition().line == line &&
+ gEditor.getCaretPosition().col == col,
+ "The editor didn't jump to the correct line.");
+
+ ok(gSources.selectedLabel, label,
+ "The current source isn't the correct one, according to the label.");
+ ok(gSources.selectedValue, value,
+ "The current source isn't the correct one, according to the value.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(gEditor.getCaretPosition().line == expectedResults[0][3] &&
+ gEditor.getCaretPosition().col == expectedResults[0][4],
+ "The editor didn't jump to the correct line again.");
+
+ executeSoon(callback);
+ });
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ write("@");
+}
+
+function firstSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ ok(gFilteredFunctions.selectedValue,
+ "An item should be selected in the filtered functions view");
+ ok(gFilteredFunctions.selectedLabel,
+ "An item should be selected in the filtered functions view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-01.js") != -1) {
+
+ executeSoon(function() {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["test", "-01.js", "", 3, 9],
+ ["anonymousExpression", "-01.js", "test.prototype", 8, 2],
+ ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 10, 2],
+ ["a_test", "-01.js", "foo", 21, 2],
+ ["n_test" + s + "x", "-01.js", "foo", 23, 2],
+ ["a_test", "-01.js", "foo.sub", 26, 4],
+ ["n_test" + s + "y", "-01.js", "foo.sub", 28, 4],
+ ["a_test", "-01.js", "foo.sub.sub", 31, 6],
+ ["n_test" + s + "z", "-01.js", "foo.sub.sub", 33, 6],
+ ["test_SAME_NAME", "-01.js", "foo.sub.sub.sub", 36, 8]
+ ];
+
+ for (let [label, value, description, line, col] of expectedResults) {
+ is(gFilteredFunctions.selectedItem.label,
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The corect label (" + label + ") is currently selected.");
+ ok(gFilteredFunctions.selectedItem.value.contains(value),
+ "The corect value (" + value + ") is attached.");
+ is(gFilteredFunctions.selectedItem.description, description,
+ "The corect description (" + description + ") is currently shown.");
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource());
+ ok(gEditor.getCaretPosition().line == line &&
+ gEditor.getCaretPosition().col == col,
+ "The editor didn't jump to the correct line.");
+
+ ok(gSources.selectedLabel, label,
+ "The current source isn't the correct one, according to the label.");
+ ok(gSources.selectedValue, value,
+ "The current source isn't the correct one, according to the value.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(gEditor.getCaretPosition().line == expectedResults[0][3] &&
+ gEditor.getCaretPosition().col == expectedResults[0][4],
+ "The editor didn't jump to the correct line again.");
+
+ executeSoon(callback);
+ });
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ write("@");
+}
+
+function secondSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ ok(gFilteredFunctions.selectedValue,
+ "An item should be selected in the filtered functions view");
+ ok(gFilteredFunctions.selectedLabel,
+ "An item should be selected in the filtered functions view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+
+ executeSoon(function() {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["test2", "-02.js", "", 3, 4],
+ ["test3" + s + "test3_NAME", "-02.js", "", 7, 4],
+ ["test4_SAME_NAME", "-02.js", "", 10, 4],
+ ["x" + s + "X", "-02.js", "test.prototype", 13, 0],
+ ["y" + s + "Y", "-02.js", "test.prototype.sub", 15, 0],
+ ["z" + s + "Z", "-02.js", "test.prototype.sub.sub", 17, 0],
+ ["t", "-02.js", "test.prototype.sub.sub.sub", 19, 0],
+ ["x", "-02.js", "", 19, 31],
+ ["y", "-02.js", "", 19, 40],
+ ["z", "-02.js", "", 19, 49]
+ ];
+
+ for (let [label, value, description, line, col] of expectedResults) {
+ is(gFilteredFunctions.selectedItem.label,
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The corect label (" + label + ") is currently selected.");
+ ok(gFilteredFunctions.selectedItem.value.contains(value),
+ "The corect value (" + value + ") is attached.");
+ is(gFilteredFunctions.selectedItem.description, description,
+ "The corect description (" + description + ") is currently shown.");
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource());
+ ok(gEditor.getCaretPosition().line == line &&
+ gEditor.getCaretPosition().col == col,
+ "The editor didn't jump to the correct line.");
+
+ ok(gSources.selectedLabel, label,
+ "The current source isn't the correct one, according to the label.");
+ ok(gSources.selectedValue, value,
+ "The current source isn't the correct one, according to the value.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(gEditor.getCaretPosition().line == expectedResults[0][3] &&
+ gEditor.getCaretPosition().col == expectedResults[0][4],
+ "The editor didn't jump to the correct line again.");
+
+ executeSoon(callback);
+ });
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ write("@");
+}
+
+function thirdSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ ok(gFilteredFunctions.selectedValue,
+ "An item should be selected in the filtered functions view");
+ ok(gFilteredFunctions.selectedLabel,
+ "An item should be selected in the filtered functions view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-03.js") != -1) {
+
+ executeSoon(function() {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["namedEventListener", "-03.js", "", 3, 42],
+ ["a" + s + "A", "-03.js", "bar", 9, 4],
+ ["b" + s + "B", "-03.js", "bar.alpha", 14, 4],
+ ["c" + s + "C", "-03.js", "bar.alpha.beta", 19, 4],
+ ["d" + s + "D", "-03.js", "theta", 24, 4],
+ ["fun", "-03.js", "", 28, 6],
+ ["foo", "-03.js", "", 28, 12],
+ ["bar", "-03.js", "", 28, 18],
+ ["t_foo", "-03.js", "", 28, 24],
+ ["w_bar" + s + "baz", "-03.js", "window", 28, 37]
+ ];
+
+ for (let [label, value, description, line, col] of expectedResults) {
+ is(gFilteredFunctions.selectedItem.label,
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The corect label (" + label + ") is currently selected.");
+ ok(gFilteredFunctions.selectedItem.value.contains(value),
+ "The corect value (" + value + ") is attached.");
+ is(gFilteredFunctions.selectedItem.description, description,
+ "The corect description (" + description + ") is currently shown.");
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource());
+ ok(gEditor.getCaretPosition().line == line &&
+ gEditor.getCaretPosition().col == col,
+ "The editor didn't jump to the correct line.");
+
+ ok(gSources.selectedLabel, label,
+ "The current source isn't the correct one, according to the label.");
+ ok(gSources.selectedValue, value,
+ "The current source isn't the correct one, according to the value.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(gEditor.getCaretPosition().line == expectedResults[0][3] &&
+ gEditor.getCaretPosition().col == expectedResults[0][4],
+ "The editor didn't jump to the correct line again.");
+
+ executeSoon(callback);
+ });
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ write("@");
+}
+
+function filterSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ ok(gFilteredFunctions.selectedValue,
+ "An item should be selected in the filtered functions view");
+ ok(gFilteredFunctions.selectedLabel,
+ "An item should be selected in the filtered functions view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-03.js") != -1) {
+
+ executeSoon(function() {
+ let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " ";
+ let expectedResults = [
+ ["namedEventListener", "-03.js", "", 3, 42],
+ ["a" + s + "A", "-03.js", "bar", 9, 4],
+ ["bar", "-03.js", "", 28, 18],
+ ["w_bar" + s + "baz", "-03.js", "window", 28, 37],
+ ["test3" + s + "test3_NAME", "-02.js", "", 7, 4],
+ ["test4_SAME_NAME", "-02.js", "", 10, 4],
+ ["anonymousExpression", "-01.js", "test.prototype", 8, 2],
+ ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 10, 2],
+ ["a_test", "-01.js", "foo", 21, 2],
+ ["a_test", "-01.js", "foo.sub", 26, 4]
+ ];
+
+ for (let [label, value, description, line, col] of expectedResults) {
+ is(gFilteredFunctions.selectedItem.label,
+ gDebugger.SourceUtils.trimUrlLength(label + "()"),
+ "The corect label (" + label + ") is currently selected.");
+ ok(gFilteredFunctions.selectedItem.value.contains(value),
+ "The corect value (" + value + ") is attached.");
+ is(gFilteredFunctions.selectedItem.description, description,
+ "The corect description (" + description + ") is currently shown.");
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource());
+ ok(gEditor.getCaretPosition().line == line &&
+ gEditor.getCaretPosition().col == col,
+ "The editor didn't jump to the correct line.");
+
+ ok(gSources.selectedLabel, label,
+ "The current source isn't the correct one, according to the label.");
+ ok(gSources.selectedValue, value,
+ "The current source isn't the correct one, according to the value.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+
+ ok(gEditor.getCaretPosition().line == expectedResults[0][3] &&
+ gEditor.getCaretPosition().col == expectedResults[0][4],
+ "The editor didn't jump to the correct line again.");
+
+ executeSoon(callback);
+ });
+ } else {
+ ok(false, "How did you get here? Go away, you.");
+ }
+ });
+
+ write("@a");
+}
+
+function bogusSearch(callback) {
+ gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ ok(true, "Popup was successfully hidden after no matches were found!");
+ executeSoon(callback);
+ });
+
+ write("@bogus");
+}
+
+function anotherSearch(callback) {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ ok(true, "Popup was successfully shown after some matches were found!");
+ executeSoon(callback);
+ });
+
+ write("@NAME");
+}
+
+function emptySearch(callback) {
+ gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ ok(true, "Popup was successfully hidden when nothing was searched!");
+ executeSoon(callback);
+ });
+
+ clear();
+}
+
+function showSource(label, callback) {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ executeSoon(callback);
+ });
+ gSources.selectedLabel = label;
+}
+
+function saveSearch(callback) {
+ gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ executeSoon(callback);
+ });
+ if (Math.random() >= 0.5) {
+ EventUtils.sendKey("RETURN", gDebugger);
+ } else {
+ EventUtils.sendMouseEvent({ type: "click" },
+ gFilteredFunctions.selectedItem.target,
+ gDebugger);
+ }
+}
+
+function waitForCaretPos(number, callback)
+{
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ info("caret: " + gEditor.getCaretPosition().line);
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gEditor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+ gFilteredFunctions = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_globalactor-01.js b/browser/devtools/debugger/test/browser_dbg_globalactor-01.js
new file mode 100644
index 000000000..ab18c4562
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_globalactor-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check extension-added global actor API.
+ */
+
+var gClient = null;
+
+function test()
+{
+ DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ gClient.listTabs(function(aResponse) {
+ let globalActor = aResponse.testGlobalActor1;
+ ok(globalActor, "Found the test tab actor.")
+ ok(globalActor.indexOf("testone") >= 0,
+ "testTabActor's actorPrefix should be used.");
+ gClient.request({ to: globalActor, type: "ping" }, function(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" }, function(aResponse) {
+ is(aResponse.pong, "pong", "Actor should respond to requests.");
+
+ // Make sure that lazily-created actors are created only once.
+ let conn = transport._serverConnection;
+ // First we look for the pool of global actors.
+ let extraPools = conn._extraPools;
+
+ let globalPool;
+ for (let pool of extraPools) {
+ if (Object.keys(pool._actors).some(function(elem) {
+ // Tab actors are in the global pool.
+ let re = new RegExp(conn._prefix + "tab", "g");
+ return elem.match(re) !== null;
+ })) {
+ globalPool = pool;
+ break;
+ }
+ }
+ // Then we look if the global pool contains only one test actor.
+ let actorPrefix = conn._prefix + "testone";
+ let actors = Object.keys(globalPool._actors).join();
+ info("Global actors: " + actors);
+ isnot(actors.indexOf(actorPrefix), -1, "The test actor exists in the pool.");
+ is(actors.indexOf(actorPrefix), actors.lastIndexOf(actorPrefix),
+ "Only one actor exists in the pool.");
+
+ finish_test();
+ });
+ });
+ });
+ });
+}
+
+function finish_test()
+{
+ gClient.close(function() {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_iframes.html b/browser/devtools/debugger/test/browser_dbg_iframes.html
new file mode 100644
index 000000000..e94dd2f65
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_iframes.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset='utf-8'/><title>Browser Debugger IFrame Test Tab</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+
+<body>
+ <iframe src="browser_dbg_debuggerstatement.html"></iframe>
+</body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_iframes.js b/browser/devtools/debugger/test/browser_dbg_iframes.js
new file mode 100644
index 000000000..7f344c38a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_iframes.js
@@ -0,0 +1,67 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that iframes can be added as debuggees.
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+const TEST_URL = EXAMPLE_URL + "browser_dbg_iframes.html";
+
+function test() {
+ debug_tab_pane(TEST_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ is(gDebugger.DebuggerController.activeThread.paused, false,
+ "Should be running after debug_tab_pane.");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.paused, true,
+ "Should be paused after an interrupt request.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have one frame in the stack.");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+ closeDebuggerAndFinish();
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }}, 0);
+ });
+
+ let iframe = gTab.linkedBrowser.contentWindow.wrappedJSObject.frames[0];
+ is(iframe.document.title, "Browser Debugger Test Tab", "Found the iframe");
+
+ function handler() {
+ if (iframe.document.readyState != "complete") {
+ return;
+ }
+ iframe.window.removeEventListener("load", handler, false);
+ executeSoon(iframe.runDebuggerStatement);
+ };
+ iframe.window.addEventListener("load", handler, false);
+ handler();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_listtabs-01.js b/browser/devtools/debugger/test/browser_dbg_listtabs-01.js
new file mode 100644
index 000000000..12b0ce77c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs-01.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Make sure the listTabs request works as specified.
+
+var gTab1 = null;
+var gTab1Actor = null;
+
+var gTab2 = null;
+var gTab2Actor = null;
+
+var gClient = null;
+
+function test()
+{
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ test_first_tab();
+ });
+}
+
+/**
+ * Verify that a new tab shows up in a listTabs call.
+ */
+function test_first_tab()
+{
+ gTab1 = addTab(TAB1_URL, function() {
+ gClient.listTabs(function(aResponse) {
+ for each (let tab in aResponse.tabs) {
+ if (tab.url == TAB1_URL) {
+ gTab1Actor = tab.actor;
+ }
+ }
+ ok(gTab1Actor, "Should find a tab actor for tab1.");
+ test_second_tab();
+ });
+ });
+}
+
+function test_second_tab()
+{
+ gTab2 = addTab(TAB2_URL, function() {
+ gClient.listTabs(function(aResponse) {
+ // Verify that tab1 has the same actor it used to.
+ let foundTab1 = false;
+ for each (let tab in aResponse.tabs) {
+ if (tab.url == TAB1_URL) {
+ is(tab.actor, gTab1Actor, "Tab1's actor shouldn't have changed.");
+ foundTab1 = true;
+ }
+ if (tab.url == TAB2_URL) {
+ gTab2Actor = tab.actor;
+ }
+ }
+ ok(foundTab1, "Should have found an actor for tab 1.");
+ ok(gTab2Actor != null, "Should find an actor for tab2.");
+
+ test_remove_tab();
+ });
+ });
+}
+
+function test_remove_tab()
+{
+ removeTab(gTab1);
+ gTab1 = null;
+ gClient.listTabs(function(aResponse) {
+ // Verify that tab1 is no longer included in listTabs.
+ let foundTab1 = false;
+ for each (let tab in aResponse.tabs) {
+ if (tab.url == TAB1_URL) {
+ ok(false, "Tab1 should be gone.");
+ }
+ }
+ ok(!foundTab1, "Tab1 should be gone.");
+ test_attach_removed_tab();
+ });
+}
+
+function test_attach_removed_tab()
+{
+ removeTab(gTab2);
+ gTab2 = null;
+ gClient.addListener("paused", function(aEvent, aPacket) {
+ ok(false, "Attaching to an exited tab actor shouldn't generate a pause.");
+ finish_test();
+ });
+
+ gClient.request({ to: gTab2Actor, type: "attach" }, function(aResponse) {
+ is(aResponse.type, "exited", "Tab should consider itself exited.");
+ finish_test();
+ });
+}
+
+function finish_test()
+{
+ gClient.close(function() {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_listtabs-02.js b/browser/devtools/debugger/test/browser_dbg_listtabs-02.js
new file mode 100644
index 000000000..c48d374ca
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs-02.js
@@ -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/. */
+
+// Make sure the root actor's live tab list implementation works as specified.
+
+let testPage = ("data:text/html;charset=utf-8,"
+ + encodeURIComponent("<title>JS Debugger BrowserTabList test page</title>" +
+ "<body>Yo.</body>"));
+// The tablist object whose behavior we observe.
+let tabList;
+let firstActor, actorA;
+let tabA, tabB, tabC;
+let newWin;
+// Stock onListChanged handler.
+let onListChangedCount = 0;
+function onListChangedHandler() {
+ onListChangedCount++;
+}
+
+function test() {
+ tabList = new DebuggerServer.BrowserTabList("fake DebuggerServerConnection");
+ tabList._testing = true;
+ tabList.onListChanged = onListChangedHandler;
+
+ checkSingleTab();
+ // Open a new tab. We should be notified.
+ is(onListChangedCount, 0, "onListChanged handler call count");
+ tabA = addTab(testPage, onTabA);
+}
+
+function checkSingleTab() {
+ var tabActors = [t for (t of tabList)];
+ is(tabActors.length, 1, "initial tab list: contains initial tab");
+ firstActor = tabActors[0];
+ is(firstActor.url, "about:blank", "initial tab list: initial tab URL is 'about:blank'");
+ is(firstActor.title, "New Tab", "initial tab list: initial tab title is 'New Tab'");
+}
+
+function onTabA() {
+ is(onListChangedCount, 1, "onListChanged handler call count");
+
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 2, "tabA opened: two tabs in list");
+ ok(tabActors.has(firstActor), "tabA opened: initial tab present");
+
+ info("actors: " + [a.url for (a of tabActors)]);
+ actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+ ok(actorA.url.match(/^data:text\/html;/), "tabA opened: new tab URL");
+ is(actorA.title, "JS Debugger BrowserTabList test page", "tabA opened: new tab title");
+
+ tabB = addTab(testPage, onTabB);
+}
+
+function onTabB() {
+ is(onListChangedCount, 2, "onListChanged handler call count");
+
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 3, "tabB opened: three tabs in list");
+
+ // Test normal close.
+ gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", onClose, false);
+ ok(!aEvent.detail, "This was a normal tab close");
+ // Let the actor's TabClose handler finish first.
+ executeSoon(testTabClose);
+ }, false);
+ gBrowser.removeTab(tabA);
+}
+
+function testTabClose() {
+ is(onListChangedCount, 3, "onListChanged handler call count");
+
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 2, "tabA closed: two tabs in list");
+ ok(tabActors.has(firstActor), "tabA closed: initial tab present");
+
+ info("actors: " + [a.url for (a of tabActors)]);
+ actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+ ok(actorA.url.match(/^data:text\/html;/), "tabA closed: new tab URL");
+ is(actorA.title, "JS Debugger BrowserTabList test page", "tabA closed: new tab title");
+
+ // Test tab close by moving tab to a window.
+ tabC = addTab(testPage, onTabC);
+}
+
+function onTabC() {
+ is(onListChangedCount, 4, "onListChanged handler call count");
+
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 3, "tabC opened: three tabs in list");
+
+ gBrowser.tabContainer.addEventListener("TabClose", function onClose2(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", onClose2, false);
+ ok(aEvent.detail, "This was a tab closed by moving");
+ // Let the actor's TabClose handler finish first.
+ executeSoon(testWindowClose);
+ }, false);
+ newWin = gBrowser.replaceTabWithWindow(tabC);
+}
+
+function testWindowClose() {
+ is(onListChangedCount, 5, "onListChanged handler call count");
+
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 3, "tabC closed: three tabs in list");
+ ok(tabActors.has(firstActor), "tabC closed: initial tab present");
+
+ info("actors: " + [a.url for (a of tabActors)]);
+ actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+ ok(actorA.url.match(/^data:text\/html;/), "tabC closed: new tab URL");
+ is(actorA.title, "JS Debugger BrowserTabList test page", "tabC closed: new tab title");
+
+ // Cleanup.
+ newWin.addEventListener("unload", function onUnload(aEvent) {
+ newWin.removeEventListener("unload", onUnload, false);
+ ok(!aEvent.detail, "This was a normal window close");
+ // Let the actor's TabClose handler finish first.
+ executeSoon(checkWindowClose);
+ }, false);
+ newWin.close();
+}
+
+function checkWindowClose() {
+ is(onListChangedCount, 6, "onListChanged handler call count");
+
+ // Check that closing a XUL window leaves the other actors intact.
+ var tabActors = new Set([t for (t of tabList)]);
+ is(tabActors.size, 2, "newWin closed: two tabs in list");
+ ok(tabActors.has(firstActor), "newWin closed: initial tab present");
+
+ info("actors: " + [a.url for (a of tabActors)]);
+ actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+ ok(actorA.url.match(/^data:text\/html;/), "newWin closed: new tab URL");
+ is(actorA.title, "JS Debugger BrowserTabList test page", "newWin closed: new tab title");
+
+ // Test normal close.
+ gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", onClose, false);
+ ok(!aEvent.detail, "This was a normal tab close");
+ // Let the actor's TabClose handler finish first.
+ executeSoon(finishTest);
+ }, false);
+ gBrowser.removeTab(tabB);
+}
+
+function finishTest() {
+ checkSingleTab();
+ finish();
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_location-changes-blank.js b/browser/devtools/debugger/test/browser_dbg_location-changes-blank.js
new file mode 100644
index 000000000..478a7677b
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-blank.js
@@ -0,0 +1,108 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 scripts works.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("browser_dbg_stack") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.simpleCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testSimpleCall }, 0);
+ }
+ }
+}
+
+function testSimpleCall() {
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have only one frame.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ isnot(gDebugger.DebuggerView.Sources.selectedValue, null,
+ "There should be a selected script.");
+ isnot(gDebugger.editor.getText().length, 0,
+ "The source editor should have some text displayed.");
+ isnot(gDebugger.editor.getText(), gDebugger.L10N.getStr("loadingText"),
+ "The source editor text should not be 'Loading...'");
+
+ testLocationChange();
+}
+
+function testLocationChange()
+{
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+
+ gDebugger.addEventListener("Debugger:AfterSourcesAdded", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ is(gDebugger.DebuggerView.Sources.selectedValue, "",
+ "There should be no selected script.");
+ is(gDebugger.editor.getText().length, 0,
+ "The source editor not have any text displayed.");
+
+ let menulist = gDebugger.DebuggerView.Sources.widget;
+ let noScripts = gDebugger.L10N.getStr("noSourcesText");
+ is(menulist.getAttribute("label"), noScripts,
+ "The menulist should display a notice that there are no scripts availalble.");
+ is(menulist.getAttribute("tooltiptext"), "",
+ "The menulist shouldn't have any tooltip text attributed when there are no scripts available.");
+
+ closeDebuggerAndFinish();
+ });
+ });
+ content.location = "about:blank";
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_location-changes-bp.js b/browser/devtools/debugger/test/browser_dbg_location-changes-bp.js
new file mode 100644
index 000000000..569171fd7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-bp.js
@@ -0,0 +1,163 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 + "test-location-changes-bp.html";
+const SCRIPT_URL = EXAMPLE_URL + "test-location-changes-bp.js";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var sourcesShown = false;
+var tabNavigated = false;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testAddBreakpoint();
+ });
+}
+
+function testAddBreakpoint()
+{
+ let controller = gDebugger.DebuggerController;
+ controller.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(controller.activeThread.state, "paused",
+ "The debugger statement was reached.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have one frame.");
+
+ gPane.addBreakpoint({ url: SCRIPT_URL, line: 5 }, testResume);
+ }}, 0);
+ });
+
+ gDebuggee.runDebuggerStatement();
+}
+
+function testResume()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint wasn't hit yet.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("resumed", function() {
+ thread.addOneTimeListener("paused", function() {
+ executeSoon(testBreakpointHit);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"));
+ });
+
+ thread.resume();
+}
+
+function testBreakpointHit()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint was hit.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("paused", function test(aEvent, aPacket) {
+ thread.addOneTimeListener("resumed", function() {
+ executeSoon(testReloadPage);
+ });
+
+ is(aPacket.why.type, "debuggerStatement", "Execution has advanced to the next line.");
+ isnot(aPacket.why.type, "breakpoint", "No ghost breakpoint was hit.");
+ thread.resume();
+ });
+
+ thread.resume();
+}
+
+function testReloadPage()
+{
+ let controller = gDebugger.DebuggerController;
+ controller._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ tabNavigated = true;
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+ clickAgain();
+ });
+
+ gDebugger.addEventListener("Debugger:SourceShown", function onSourcesShown() {
+ sourcesShown = true;
+ gDebugger.removeEventListener("Debugger:SourceShown", onSourcesShown);
+ clickAgain();
+ });
+
+ content.location.reload();
+}
+
+function clickAgain()
+{
+ if (!sourcesShown || !tabNavigated) {
+ return;
+ }
+
+ let controller = gDebugger.DebuggerController;
+ controller.activeThread.addOneTimeListener("framesadded", function() {
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint was hit.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("paused", function test(aEvent, aPacket) {
+ thread.addOneTimeListener("resumed", function() {
+ executeSoon(closeDebuggerAndFinish);
+ });
+
+ is(aPacket.why.type, "debuggerStatement", "Execution has advanced to the next line.");
+ isnot(aPacket.why.type, "breakpoint", "No ghost breakpoint was hit.");
+ thread.resume();
+ });
+
+ thread.resume();
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"));
+}
+
+function testBreakpointHitAfterReload()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "The breakpoint was hit.");
+
+ let thread = gDebugger.DebuggerController.activeThread;
+ thread.addOneTimeListener("paused", function test(aEvent, aPacket) {
+ thread.addOneTimeListener("resumed", function() {
+ executeSoon(closeDebuggerAndFinish);
+ });
+
+ is(aPacket.why.type, "debuggerStatement", "Execution has advanced to the next line.");
+ isnot(aPacket.why.type, "breakpoint", "No ghost breakpoint was hit.");
+ thread.resume();
+ });
+
+ thread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_location-changes-new.js b/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
new file mode 100644
index 000000000..e2b1d8e2c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
@@ -0,0 +1,108 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 scripts works.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("browser_dbg_stack") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.simpleCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testSimpleCall }, 0);
+ }
+ }
+}
+
+function testSimpleCall() {
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have only one frame.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ isnot(gDebugger.DebuggerView.Sources.selectedValue, null,
+ "There should be a selected script.");
+ isnot(gDebugger.editor.getText().length, 0,
+ "The source editor should have some text displayed.");
+ isnot(gDebugger.editor.getText(), gDebugger.L10N.getStr("loadingText"),
+ "The source editor text should not be 'Loading...'");
+
+ testLocationChange();
+}
+
+function testLocationChange()
+{
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ isnot(gDebugger.DebuggerView.Sources.selectedValue, null,
+ "There should be a selected script.");
+ isnot(gDebugger.editor.getText().length, 0,
+ "The source editor should have some text displayed.");
+
+ let menulist = gDebugger.DebuggerView.Sources.widget;
+ let noScripts = gDebugger.L10N.getStr("noSourcesText");
+ isnot(menulist.getAttribute("label"), noScripts,
+ "The menulist should not display a notice that there are no scripts availalble.");
+ isnot(menulist.getAttribute("tooltiptext"), "",
+ "The menulist should have a tooltip text attributed.");
+
+ closeDebuggerAndFinish();
+ });
+ });
+ content.location = EXAMPLE_URL + "browser_dbg_iframes.html";
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_location-changes.js b/browser/devtools/debugger/test/browser_dbg_location-changes.js
new file mode 100644
index 000000000..5f1122e63
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes.js
@@ -0,0 +1,69 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that changing the tab location URL works.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({
+ run: function() {
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have only one frame.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ testLocationChange();
+ }
+ }, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+function testLocationChange()
+{
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+
+ closeDebuggerAndFinish();
+ });
+ content.location = TAB1_URL;
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_menustatus.js b/browser/devtools/debugger/test/browser_dbg_menustatus.js
new file mode 100644
index 000000000..07472654f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_menustatus.js
@@ -0,0 +1,45 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// We make sure the menuitems in the application menubar
+// are checked.
+
+function test() {
+ var tab1 = addTab("about:blank", function() {
+ var tab2 = addTab("about:blank", function() {
+ gBrowser.selectedTab = tab2;
+
+ let pane = DebuggerUI.toggleDebugger();
+ ok(pane, "toggleDebugger() should return a pane.");
+ let frame = pane._frame;
+
+ wait_for_connect_and_resume(function() {
+ let cmd = document.getElementById("Tools:Debugger");
+ is(cmd.getAttribute("checked"), "true", "<command Tools:Debugger> is checked.");
+
+ gBrowser.selectedTab = tab1;
+
+ is(cmd.getAttribute("checked"), "false", "<command Tools:Debugger> is unchecked after tab switch.");
+
+ gBrowser.selectedTab = tab2;
+
+ is(cmd.getAttribute("checked"), "true", "<command Tools:Debugger> is checked.");
+
+ let pane = DebuggerUI.toggleDebugger();
+
+ is(cmd.getAttribute("checked"), "false", "<command Tools:Debugger> is unchecked once closed.");
+ });
+
+ window.addEventListener("Debugger:Shutdown", function dbgShutdown() {
+ window.removeEventListener("Debugger:Shutdown", dbgShutdown, true);
+ removeTab(tab1);
+ removeTab(tab2);
+
+ finish();
+ }, true);
+ });
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_multiple-windows.js b/browser/devtools/debugger/test/browser_dbg_multiple-windows.js
new file mode 100644
index 000000000..aab2d046c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_multiple-windows.js
@@ -0,0 +1,115 @@
+/* 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.
+
+var gTab1 = null;
+var gTab1Actor = null;
+
+var gSecondWindow = null;
+
+var gClient = null;
+var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+
+function test()
+{
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ test_first_tab();
+ });
+}
+
+function test_first_tab()
+{
+ gTab1 = addTab(TAB1_URL, function() {
+ gClient.listTabs(function(aResponse) {
+ for each (let tab in aResponse.tabs) {
+ if (tab.url == TAB1_URL) {
+ gTab1Actor = tab.actor;
+ }
+ }
+ ok(gTab1Actor, "Should find a tab actor for tab1.");
+ is(aResponse.selected, 1, "Tab1 is selected.");
+ test_open_window();
+ });
+ });
+}
+
+function test_open_window()
+{
+ gSecondWindow = window.open(TAB2_URL, "secondWindow");
+ ok(!!gSecondWindow, "Second window created.");
+ gSecondWindow.focus();
+ let top = windowMediator.getMostRecentWindow("navigator:browser");
+ var main2 = gSecondWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ is(top, main2, "The second window is on top.");
+ executeSoon(function() {
+ gClient.listTabs(function(aResponse) {
+ is(aResponse.selected, 2, "Tab2 is selected.");
+
+ test_focus_first();
+ });
+ });
+}
+
+function test_focus_first()
+{
+ window.content.addEventListener("focus", function onFocus() {
+ window.content.removeEventListener("focus", onFocus, false);
+ let top = windowMediator.getMostRecentWindow("navigator:browser");
+ var main1 = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ is(top, main1, "The first window is on top.");
+
+ gClient.listTabs(function(aResponse) {
+ is(aResponse.selected, 1, "Tab1 is selected after focusing on it.");
+
+ test_remove_tab();
+ });
+ }, false);
+ window.content.focus();
+}
+
+function test_remove_tab()
+{
+ gSecondWindow.close();
+ gSecondWindow = null;
+ removeTab(gTab1);
+ gTab1 = null;
+ gClient.listTabs(function(aResponse) {
+ // Verify that tabs are no longer included in listTabs.
+ let foundTab1 = false;
+ let foundTab2 = false;
+ for (let tab of aResponse.tabs) {
+ if (tab.url == TAB1_URL) {
+ foundTab1 = true;
+ } else if (tab.url == TAB2_URL) {
+ foundTab2 = true;
+ }
+ }
+ ok(!foundTab1, "Tab1 should be gone.");
+ ok(!foundTab2, "Tab2 should be gone.");
+ is(aResponse.selected, 0, "The original tab is selected.");
+ finish_test();
+ });
+}
+
+function finish_test()
+{
+ gClient.close(function() {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_nav-01.js b/browser/devtools/debugger/test/browser_dbg_nav-01.js
new file mode 100644
index 000000000..3e3d4f432
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_nav-01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check tab attach/navigation.
+ */
+
+var gTab1 = null;
+var gTab1Actor = null;
+
+var gClient = null;
+
+function test()
+{
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function(aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ get_tab();
+ });
+}
+
+function get_tab()
+{
+ gTab1 = addTab(TAB1_URL, function() {
+ get_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
+ gTab1Actor = aGrip.actor;
+ gClient.request({ to: aGrip.actor, type: "attach" }, function(aResponse) {
+ gClient.addListener("tabNavigated", function onTabNavigated(aEvent, aPacket) {
+ dump("onTabNavigated state " + aPacket.state + "\n");
+ if (aPacket.state == "start") {
+ return;
+ }
+ gClient.removeListener("tabNavigated", onTabNavigated);
+
+ is(aPacket.url, TAB2_URL, "Got a tab navigation notification.");
+ gClient.addOneTimeListener("tabDetached", function (aEvent, aPacket) {
+ ok(true, "Got a tab detach notification.");
+ finish_test();
+ });
+ removeTab(gTab1);
+ });
+ gTab1.linkedBrowser.loadURI(TAB2_URL);
+ });
+ });
+ });
+}
+
+function finish_test()
+{
+ gClient.close(function() {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_pane-collapse.js b/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
new file mode 100644
index 000000000..b53b74fed
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
@@ -0,0 +1,159 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the debugger panes collapse properly.
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gView = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gView = gDebugger.DebuggerView;
+
+ testPanesState();
+
+ gView.toggleInstrumentsPane({ visible: true, animated: false });
+ testInstrumentsPaneCollapse();
+ testPanesStartupPref();
+ });
+}
+
+function testPanesState() {
+ let instrumentsPane =
+ gDebugger.document.getElementById("instruments-pane");
+ let instrumentsPaneToggleButton =
+ gDebugger.document.getElementById("instruments-pane-toggle");
+
+ ok(instrumentsPane.hasAttribute("pane-collapsed") &&
+ instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The debugger view instruments pane should initially be hidden.");
+ is(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view instruments pane should initially be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._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, gDebugger.Prefs.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.hasAttribute("pane-collapsed") &&
+ !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The instruments pane should at this point be visible.");
+
+ gView.toggleInstrumentsPane({ visible: false, animated: true });
+
+ is(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ let margin = -(width + 1) + "px";
+ is(width, gDebugger.Prefs.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.hasAttribute("pane-collapsed") &&
+ instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The instruments pane should not be visible after collapsing.");
+
+ gView.toggleInstrumentsPane({ visible: true, animated: false });
+
+ is(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ is(width, gDebugger.Prefs.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.hasAttribute("pane-collapsed") &&
+ !instrumentsPaneToggleButton.hasAttribute("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(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view panes should still initially be preffed as hidden.");
+
+ ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+ !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view panes should initially be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should still not be checked.");
+
+ gDebugger.DebuggerView.Options._showPanesOnStartupItem.setAttribute("checked", "true");
+ gDebugger.DebuggerView.Options._toggleShowPanesOnStartup();
+
+ executeSoon(function() {
+ ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+ !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gDebugger.Prefs.panesVisibleOnStartup, true,
+ "The debugger view panes should now be preffed as visible.");
+ is(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should now be checked.");
+
+ gDebugger.DebuggerView.Options._showPanesOnStartupItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._toggleShowPanesOnStartup();
+
+ executeSoon(function() {
+ ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+ !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The debugger instruments pane should at this point be visible.");
+ is(gDebugger.Prefs.panesVisibleOnStartup, false,
+ "The debugger view panes should now be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
+ "The options menu item should now be unchecked.");
+
+ executeSoon(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gView = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_panesize-inner.js b/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
new file mode 100644
index 000000000..ead34bec9
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
@@ -0,0 +1,77 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ var tab1 = addTab(TAB1_URL, function() {
+ gBrowser.selectedTab = tab1;
+ let target1 = TargetFactory.forTab(tab1);
+
+ ok(!gDevTools.getToolbox(target1),
+ "Shouldn't have a debugger panel for this tab yet.");
+
+ gDevTools.showToolbox(target1, "jsdebugger").then(function(toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+ ok(dbg, "We should have a debugger panel.");
+
+ let preferredSw = Services.prefs.getIntPref("devtools.debugger.ui.panes-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() * 200) + 100;
+ } while (someWidth1 == preferredSw ||
+ someWidth2 == preferredIw)
+
+ let someWidth1 = parseInt(Math.random() * 200) + 100;
+ let someWidth2 = parseInt(Math.random() * 200) + 100;
+
+ info("Preferred sources width: " + preferredSw);
+ info("Preferred instruments width: " + preferredIw);
+ info("Generated sources width: " + someWidth1);
+ info("Generated instruments width: " + someWidth2);
+
+ let content = dbg.panelWin;
+ let sources;
+ let instruments;
+
+ wait_for_connect_and_resume(function() {
+ ok(content.Prefs.sourcesWidth,
+ "The debugger preferences should have a saved sourcesWidth value.");
+ ok(content.Prefs.instrumentsWidth,
+ "The debugger preferences should have a saved instrumentsWidth value.");
+
+ sources = content.document.getElementById("sources-pane");
+ instruments = content.document.getElementById("instruments-pane");
+
+ is(content.Prefs.sourcesWidth, sources.getAttribute("width"),
+ "The sources pane width should be the same as the preferred value.");
+ is(content.Prefs.instrumentsWidth, instruments.getAttribute("width"),
+ "The instruments pane width should be the same as the preferred value.");
+
+ sources.setAttribute("width", someWidth1);
+ instruments.setAttribute("width", someWidth2);
+
+ removeTab(tab1);
+ }, tab1);
+
+ window.addEventListener("Debugger:Shutdown", function dbgShutdown() {
+ window.removeEventListener("Debugger:Shutdown", dbgShutdown, true);
+
+ is(content.Prefs.sourcesWidth, sources.getAttribute("width"),
+ "The sources pane width should have been saved by now.");
+ is(content.Prefs.instrumentsWidth, instruments.getAttribute("width"),
+ "The instruments pane width should have been saved by now.");
+
+ // Cleanup after ourselves!
+ Services.prefs.setIntPref("devtools.debugger.ui.panes-sources-width", preferredSw);
+ Services.prefs.setIntPref("devtools.debugger.ui.panes-instruments-width", preferredIw);
+
+ finish();
+ }, true);
+ });
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_pause-exceptions.html b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.html
new file mode 100644
index 000000000..31bcfa922
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Pause on Exceptions Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <button>Click me!</button>
+ <ul></ul>
+ </body>
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ function load() {
+ try {
+ debugger;
+ throw new Error("boom");
+ } catch (e) {
+ var list = document.querySelector("ul");
+ var item = document.createElement("li");
+ item.innerHTML = e.message;
+ list.appendChild(item);
+ }
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", load, false);
+ });
+ </script>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_pause-exceptions.js b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.js
new file mode 100644
index 000000000..7aac1b7d0
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.js
@@ -0,0 +1,135 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the pause-on-exceptions toggle works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_pause-exceptions.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gPrevPref = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ gPrevPref = Services.prefs.getBoolPref(
+ "devtools.debugger.pause-on-exceptions");
+ Services.prefs.setBoolPref(
+ "devtools.debugger.pause-on-exceptions", true);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ testWithFrame();
+ });
+}
+
+function testWithFrame()
+{
+ let count = 0;
+ gPane.panelWin.gClient.addOneTimeListener("paused", function() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function testA() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global object
+ // scope and the regular one.
+ if (++count < 2) {
+ is(count, 1, "A. First Debugger:FetchedVariables event received.");
+ return;
+ }
+ is(count, 2, "A. Second Debugger:FetchedVariables event received.");
+ gDebugger.removeEventListener("Debugger:FetchedVariables", testA, false);
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should be paused now.");
+
+ // Pause on exceptions should be already enabled.
+ is(gPrevPref, false,
+ "The pause-on-exceptions functionality should be disabled by default.");
+ is(gDebugger.Prefs.pauseOnExceptions, true,
+ "The pause-on-exceptions pref should be true from startup.");
+ is(gDebugger.DebuggerView.Options._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "Pause on exceptions should be enabled from startup. ")
+
+ count = 0;
+ gPane.panelWin.gClient.addOneTimeListener("resumed", function() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function testB() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global object
+ // scope and the regular one.
+ if (++count < 2) {
+ is(count, 1, "B. First Debugger:FetchedVariables event received.");
+ return;
+ }
+ is(count, 2, "B. Second Debugger:FetchedVariables event received.");
+ gDebugger.removeEventListener("Debugger:FetchedVariables", testB, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.firstChild,
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have one frame.");
+
+ is(scopes.children.length, 3, "Should have 3 variable scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+ "Should have the right property name for the exception.");
+
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "[object Error]",
+ "Should have the right property value for the exception.");
+
+ // Disable pause on exceptions.
+ gDebugger.DebuggerView.Options._pauseOnExceptionsItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._togglePauseOnExceptions();
+
+ is(gDebugger.Prefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should have been set to false.");
+
+ resumeAndFinish();
+
+ }}, 0);
+ }, false);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+ }, false);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gPane.panelWin.gClient.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ closeDebuggerAndFinish();
+
+ }}, 0);
+ });
+
+ // Resume to let the exception reach it's catch clause.
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_pause-resume.js b/browser/devtools/debugger/test/browser_dbg_pause-resume.js
new file mode 100644
index 000000000..1f886cd17
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-resume.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/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gView = null;
+var gLH = null;
+var gL10N = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gView = gDebugger.DebuggerView;
+ gLH = gDebugger.LayoutHelpers;
+ gL10N = gDebugger.L10N;
+
+ testPause();
+ });
+}
+
+function testPause() {
+ is(gDebugger.DebuggerController.activeThread.paused, false,
+ "Should be running after debug_tab_pane.");
+
+ let button = gDebugger.document.getElementById("resume");
+ is(button.getAttribute("tooltiptext"),
+ gL10N.getFormatStr("pauseButtonTooltip",
+ gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+ "Button tooltip should be pause when running.");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.paused, true,
+ "Should be paused after an interrupt request.");
+
+ is(button.getAttribute("tooltiptext"),
+ gL10N.getFormatStr("resumeButtonTooltip",
+ gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+ "Button tooltip should be resume when paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames when paused in the main loop.");
+
+ testResume();
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+function testResume() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(gDebugger.DebuggerController.activeThread.paused, false,
+ "Should be paused after an interrupt request.");
+
+ let button = gDebugger.document.getElementById("resume");
+ is(button.getAttribute("tooltiptext"),
+ gL10N.getFormatStr("pauseButtonTooltip",
+ gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+ "Button tooltip should be pause when running.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gView = null;
+ gLH = null;
+ gL10N = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_pause-warning.js b/browser/devtools/debugger/test/browser_dbg_pause-warning.js
new file mode 100644
index 000000000..c72363343
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-warning.js
@@ -0,0 +1,97 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gView = null;
+var gToolbox = null;
+var gTarget = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gView = gDebugger.DebuggerView;
+
+ gTarget = TargetFactory.forTab(gBrowser.selectedTab);
+ gToolbox = gDevTools.getToolbox(gTarget);
+
+ testPause();
+ });
+}
+
+function testPause() {
+ let button = gDebugger.document.getElementById("resume");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+ is(gDebugger.DebuggerController.activeThread.paused, true,
+ "Debugger is paused.");
+
+ ok(gTarget.isThreadPaused, "target.isThreadPaused has been updated");
+
+ gToolbox.once("inspector-selected", testNotificationIsUp1);
+ gToolbox.selectTool("inspector");
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+function testNotificationIsUp1() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+ ok(notification, "Notification is present");
+ gToolbox.once("jsdebugger-selected", testNotificationIsHidden);
+ gToolbox.selectTool("jsdebugger");
+}
+
+function testNotificationIsHidden() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+ ok(!notification, "Notification is hidden");
+ gToolbox.once("inspector-selected", testNotificationIsUp2);
+ gToolbox.selectTool("inspector");
+}
+
+function testNotificationIsUp2() {
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+ ok(notification, "Notification is present");
+ testResume();
+}
+
+function testResume() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ ok(!gTarget.isThreadPaused, "target.isThreadPaused has been updated");
+ let notificationBox = gToolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+ ok(!notification, "No notification once debugger resumed");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gView = null;
+ gToolbox = null;
+ gTarget = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_progress-listener-bug.js b/browser/devtools/debugger/test/browser_dbg_progress-listener-bug.js
new file mode 100644
index 000000000..76f934df7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_progress-listener-bug.js
@@ -0,0 +1,70 @@
+/*
+ * 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 gPane = null;
+var gTab = null;
+var gOldListener = null;
+
+const TEST_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+function test() {
+ installListener();
+
+ debug_tab_pane(TEST_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ let gDebugger = gPane.panelWin;
+
+ is(gDebugger.DebuggerController._isInitialized, true,
+ "Controller should be initialized after debug_tab_pane.");
+ is(gDebugger.DebuggerView._isInitialized, true,
+ "View should be initialized after debug_tab_pane.");
+
+ closeDebuggerAndFinish();
+ });
+}
+
+// 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;
+ }
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-01.js b/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
new file mode 100644
index 000000000..8a018fbf9
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
@@ -0,0 +1,165 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ testlabelshortening();
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+function testlabelshortening() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ let sv = gDebugger.SourceUtils;
+ let vs = gDebugger.DebuggerView.Sources;
+ let ss = gDebugger.DebuggerController.SourceScripts;
+ vs.empty();
+ vs.widget.removeEventListener("select", vs._onScriptsChange, false);
+
+ is(sv.trimUrlQuery("a/b/c.d?test=1&random=4#reference"), "a/b/c.d",
+ "Trimming the url query isn't done properly.");
+
+ let ellipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString);
+ let nanana = new Array(20).join(NaN);
+
+ let superLargeLabel = new Array(100).join("Beer can in Jamaican sounds like Bacon!");
+ let trimmedLargeLabel = sv.trimUrlLength(superLargeLabel, 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.");
+
+ 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: nanana, leaf: "Batman!" + "{trim me, now and forevermore}" }
+ ];
+
+ urls.forEach(function(url) {
+ executeSoon(function() {
+ let loc = url.href + url.leaf;
+ vs.push([sv.trimUrlLength(sv.getSourceLabel(loc)), loc], { forced: true });
+ });
+ });
+
+ executeSoon(function() {
+ info("Script labels:");
+ info(vs.labels.toSource());
+
+ info("Script locations:");
+ info(vs.values.toSource());
+
+ urls.forEach(function(url) {
+ let loc = url.href + url.leaf;
+ if (url.dupe) {
+ ok(!vs.containsValue(loc), "Shouldn't contain script: " + loc);
+ } else {
+ ok(vs.containsValue(loc), "Should contain script: " + loc);
+ }
+ });
+
+ ok(vs.containsLabel("random/subrandom/"),
+ "Script (0) label is incorrect.");
+ ok(vs.containsLabel("random/suprandom/?a=1"),
+ "Script (1) label is incorrect.");
+ ok(vs.containsLabel("random/?a=1"),
+ "Script (2) label is incorrect.");
+ ok(vs.containsLabel("page.html"),
+ "Script (3) label is incorrect.");
+
+ ok(vs.containsLabel("script.js"),
+ "Script (4) label is incorrect.");
+ ok(vs.containsLabel("random/script.js"),
+ "Script (5) label is incorrect.");
+ ok(vs.containsLabel("random/x/script.js"),
+ "Script (6) label is incorrect.");
+ ok(vs.containsLabel("script.js?a=1"),
+ "Script (7) label is incorrect.");
+
+ ok(vs.containsLabel("script_t1.js"),
+ "Script (8) label is incorrect.");
+ ok(vs.containsLabel("script_t2_1.js"),
+ "Script (9) label is incorrect.");
+ ok(vs.containsLabel("script_t2_2.js"),
+ "Script (10) label is incorrect.");
+ ok(vs.containsLabel("script_t2_3.js"),
+ "Script (11) label is incorrect.");
+ ok(vs.containsLabel("script_t3_1.js"),
+ "Script (12) label is incorrect.");
+ ok(vs.containsLabel("script_t3_2.js"),
+ "Script (13) label is incorrect.");
+ ok(vs.containsLabel("script_t3_3.js"),
+ "Script (14) label is incorrect.");
+
+ ok(vs.containsLabel(nanana + "Batman!" + ellipsis),
+ "Script (15) label is incorrect.");
+
+ is(vs.itemCount, urls.filter(function(url) !url.dupe).length,
+ "Didn't get the correct number of scripts in the list.");
+
+ is(vs.getItemByValue("http://some.address.com/random/subrandom/").label, "random/subrandom/",
+ "Scripts.getItemByValue isn't functioning properly (0).");
+ is(vs.getItemByValue("http://some.address.com/random/suprandom/?a=1").label, "random/suprandom/?a=1",
+ "Scripts.getItemByValue isn't functioning properly (1).");
+
+ is(vs.getItemByLabel("random/subrandom/").value, "http://some.address.com/random/subrandom/",
+ "Scripts.getItemByLabel isn't functioning properly (0).");
+ is(vs.getItemByLabel("random/suprandom/?a=1").value, "http://some.address.com/random/suprandom/?a=1",
+ "Scripts.getItemByLabel isn't functioning properly (1).");
+
+
+ closeDebuggerAndFinish();
+ });
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-02.js b/browser/devtools/debugger/test/browser_dbg_propertyview-02.js
new file mode 100644
index 000000000..e2a6bdd08
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-02.js
@@ -0,0 +1,134 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let testScope = gDebugger.DebuggerView.Variables.addScope("test");
+
+ ok(testScope,
+ "Should have created a scope.");
+
+ is(testScope.id.substring(0, 4), "test",
+ "The newly created scope should have the default id set.");
+
+ is(testScope.target.querySelector(".name").getAttribute("value"), "test",
+ "Any new scope should have the designated title.");
+
+ is(testScope.target.querySelector(".variables-view-element-details").childNodes.length, 0,
+ "Any new scope should have a container with no child nodes.");
+
+ is(gDebugger.DebuggerView.Variables._list.childNodes.length, 3,
+ "Should have 3 scopes created.");
+
+
+ ok(!testScope.expanded,
+ "Any new created scope should be initially collapsed.");
+
+ ok(testScope.visible,
+ "Any new created scope should be initially visible.");
+
+ let expandCallbackSender = null;
+ let collapseCallbackSender = null;
+ let toggleCallbackSender = null;
+ let hideCallbackSender = null;
+ let showCallbackSender = null;
+
+ testScope.onexpand = function(sender) { expandCallbackSender = sender; };
+ testScope.oncollapse = function(sender) { collapseCallbackSender = sender; };
+ testScope.ontoggle = function(sender) { toggleCallbackSender = sender; };
+ testScope.onhide = function(sender) { hideCallbackSender = sender; };
+ testScope.onshow = function(sender) { showCallbackSender = sender; };
+
+ testScope.expand();
+ ok(testScope.expanded,
+ "The testScope shouldn't be collapsed anymore.");
+ is(expandCallbackSender, testScope,
+ "The expandCallback wasn't called as it should.");
+
+ testScope.collapse();
+ ok(!testScope.expanded,
+ "The testScope should be collapsed again.");
+ is(collapseCallbackSender, 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(toggleCallbackSender, testScope,
+ "The toggleCallback wasn't called as it should.");
+
+
+ testScope.hide();
+ ok(!testScope.visible,
+ "The testScope should be invisible after hiding.");
+ is(hideCallbackSender, testScope,
+ "The hideCallback wasn't called as it should.");
+
+ testScope.show();
+ ok(testScope.visible,
+ "The testScope should be visible again.");
+ is(showCallbackSender, 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.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testScope.target.querySelector(".title"),
+ gDebugger);
+
+ ok(testScope.expanded,
+ "Clicking the testScope tilte should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testScope.target.querySelector(".title"),
+ gDebugger);
+
+ ok(!testScope.expanded,
+ "Clicking again the testScope tilte should collapse it.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-03.js b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
new file mode 100644
index 000000000..684af5fb7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
@@ -0,0 +1,211 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
+ let testVar = testScope.addItem("something");
+ let duplVar = testScope.addItem("something");
+
+ info("Scope id: " + testScope.target.id);
+ info("Scope name: " + testScope.target.name);
+ info("Variable id: " + testVar.target.id);
+ info("Variable name: " + testVar.target.name);
+
+ ok(testScope,
+ "Should have created a scope.");
+ ok(testVar,
+ "Should have created a variable.");
+
+ ok(testScope.id.contains("test-scope"),
+ "Should have the correct scope id.");
+ ok(testScope.target.id.contains("test-scope"),
+ "Should have the correct scope id on the element.");
+ is(testScope.name, "test-scope",
+ "Should have the correct scope name.");
+
+ ok(testVar.id.contains("something"),
+ "Should have the correct variable id.");
+ ok(testVar.target.id.contains("something"),
+ "Should have the correct variable id on the element.");
+ is(testVar.name, "something",
+ "Should have the correct variable name.");
+
+ is(duplVar, null,
+ "Shouldn't be able to duplicate variables in the same scope.");
+
+ is(testVar.target.querySelector(".name").getAttribute("value"), "something",
+ "Any new variable should have the designated title.");
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
+ "Any new variable should have a details container with no child nodes.");
+
+
+ let properties = testVar.addItems({ "child": { "value": { "type": "object",
+ "class": "Object" } } });
+
+
+ ok(!testVar.expanded,
+ "Any new created variable should be initially collapsed.");
+
+ ok(testVar.visible,
+ "Any new created variable should be initially visible.");
+
+
+ testVar.expand();
+ ok(testVar.expanded,
+ "The testVar shouldn't be collapsed anymore.");
+
+ testVar.collapse();
+ ok(!testVar.expanded,
+ "The testVar should be collapsed again.");
+
+ testVar.expanded = true;
+ ok(testVar.expanded,
+ "The testVar shouldn't be collapsed anymore.");
+
+ testVar.toggle();
+ ok(!testVar.expanded,
+ "The testVar should be collapsed again.");
+
+
+ testVar.hide();
+ ok(!testVar.visible,
+ "The testVar should be invisible after hiding.");
+
+ testVar.show();
+ ok(testVar.visible,
+ "The testVar should be visible again.");
+
+ 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.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".name"),
+ gDebugger);
+
+ ok(testVar.expanded,
+ "Clicking the testVar name should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".name"),
+ gDebugger);
+
+ ok(!testVar.expanded,
+ "Clicking again the testVar name should collapse it.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".arrow"),
+ gDebugger);
+
+ ok(testVar.expanded,
+ "Clicking the testVar arrow should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".arrow"),
+ gDebugger);
+
+ ok(!testVar.expanded,
+ "Clicking again the testVar arrow should collapse it.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.target.querySelector(".title"),
+ gDebugger);
+
+ ok(testVar.expanded,
+ "Clicking the testVar title div should expand it again.");
+
+
+ testScope.show();
+ testScope.expand();
+ testVar.show();
+ testVar.expand();
+
+ ok(!testVar.get("child").expanded,
+ "The testVar child property should remember it is collapsed even if it is hidden.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.get("child").target.querySelector(".name"),
+ gDebugger);
+
+ ok(testVar.get("child").expanded,
+ "Clicking the testVar child property name should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.get("child").target.querySelector(".name"),
+ gDebugger);
+
+ ok(!testVar.get("child").expanded,
+ "Clicking again the testVar child property name should collapse it.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.get("child").target.querySelector(".arrow"),
+ gDebugger);
+
+ ok(testVar.get("child").expanded,
+ "Clicking the testVar child property arrow should expand it.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.get("child").target.querySelector(".arrow"),
+ gDebugger);
+
+ ok(!testVar.get("child").expanded,
+ "Clicking again the testVar child property arrow should collapse it.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ testVar.get("child").target.querySelector(".title"),
+ gDebugger);
+
+ ok(testVar.get("child").expanded,
+ "Clicking the testVar child property title div should expand it again.");
+
+
+ gDebugger.DebuggerView.Variables.empty();
+ is(gDebugger.DebuggerView.Variables._list.childNodes.length, 0,
+ "The scopes should have been removed from the parent container tree.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-04.js b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
new file mode 100644
index 000000000..47ab452c7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
@@ -0,0 +1,78 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let testScope = gDebugger.DebuggerView.Variables.addScope("test");
+ let testVar = testScope.addItem("something");
+
+ let properties = testVar.addItems({
+ "child": {
+ "value": {
+ "type": "object",
+ "class": "Object"
+ },
+ "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.");
+
+ ok(testVar.get("child"),
+ "The added detail property should be accessible from the variable.");
+
+
+ let properties2 = testVar.get("child").addItems({
+ "grandchild": {
+ "value": {
+ "type": "object",
+ "class": "Object"
+ },
+ "enumerable": true
+ }
+ });
+
+ is(testVar.get("child").target.querySelector(".variables-view-element-details").childNodes.length, 1,
+ "A new detail node should have been added in the variable tree.");
+
+ ok(testVar.get("child").get("grandchild"),
+ "The added detail property should be accessible from the variable.");
+
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-05.js b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
new file mode 100644
index 000000000..74e172ef7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
@@ -0,0 +1,95 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ requestLongerTimeout(3);
+
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let testScope = gDebugger.DebuggerView.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.");
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
+ "Adding a value property shouldn't add any new tree nodes.");
+
+
+ testVar.setGrip({ "type": "object", "class": "Window" });
+
+ is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
+ "Adding type and class properties shouldn't add any new tree nodes.");
+
+ is(testVar.target.querySelector(".value").getAttribute("value"), "[object Window]",
+ "The information for the variable wasn't set correctly.");
+
+
+ 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.");
+
+
+ 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.");
+
+
+ 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.");
+
+
+ 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.");
+
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-06.js b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
new file mode 100644
index 000000000..a36eee392
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
@@ -0,0 +1,198 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let globalScope = gDebugger.DebuggerView.Variables.addScope("Test-Global");
+ let localScope = gDebugger.DebuggerView.Variables.addScope("Test-Local");
+
+ let windowVar = globalScope.addItem("window");
+ let documentVar = globalScope.addItem("document");
+ let localVar0 = localScope.addItem("localVariable");
+
+ 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");
+
+ localVar0.setGrip(42);
+ localVar1.setGrip(true);
+ localVar2.setGrip("nasu");
+
+ localVar3.setGrip({ "type": "undefined" });
+ localVar4.setGrip({ "type": "null" });
+ localVar5.setGrip({ "type": "object", "class": "Object" });
+
+ 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
+ },
+ "someUndefined": {
+ "value": { "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
+ },
+ "someUndefined": {
+ "value": { "type": "undefined" },
+ "enumerable": true
+ },
+ "someAccessor": {
+ "get": { "type": "object", "class": "Function" },
+ "set": { "type": "undefined" },
+ "enumerable": true
+ }
+ });
+
+ windowVar.setGrip({ "type": "object", "class": "Window" });
+ windowVar.addItems({
+ "helloWorld": { "value": "hello world" }
+ });
+
+ documentVar.setGrip({ "type": "object", "class": "HTMLDocument" });
+ documentVar.addItems({
+ "onload": { "value": { "type": "null" } },
+ "onunload": { "value": { "type": "null" } },
+ "onfocus": { "value": { "type": "null" } },
+ "onblur": { "value": { "type": "null" } },
+ "onclick": { "value": { "type": "null" } },
+ "onkeypress": { "value": { "type": "null" } }
+ });
+
+
+ ok(windowVar, "The windowVar hasn't been created correctly.");
+ ok(documentVar, "The documentVar hasn't been created correctly.");
+ 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.");
+
+ for each (let elt in globalScope.target.querySelector(".nonenum").childNodes) {
+ info("globalScope :: " + { id: elt.id, className: elt.className }.toSource());
+ }
+ is(globalScope.target.querySelector(".nonenum").childNodes.length, 2,
+ "The globalScope doesn't contain all the created variable elements.");
+
+ for each (let elt in localScope.target.querySelector(".nonenum").childNodes) {
+ info("localScope :: " + { id: elt.id, className: elt.className }.toSource());
+ }
+ is(localScope.target.querySelector(".nonenum").childNodes.length, 6,
+ "The localScope doesn't contain all the created variable elements.");
+
+ is(localVar5.target.querySelector(".variables-view-element-details").childNodes.length, 8,
+ "The localVar5 doesn't contain all the created properties.");
+ is(localVar5.get("someProp5").target.querySelector(".variables-view-element-details").childNodes.length, 8,
+ "The localVar5.someProp5 doesn't contain all the created properties.");
+
+ is(windowVar.target.querySelector(".value").getAttribute("value"), "[object Window]",
+ "The grip information for the windowVar wasn't set correctly.");
+ is(documentVar.target.querySelector(".value").getAttribute("value"), "[object 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 Object]",
+ "The grip information for the localVar5 wasn't set correctly.");
+
+ is(localVar5.get("someProp0").target.querySelector(".value").getAttribute("value"), "42",
+ "The grip information for the localVar0 wasn't set correctly.");
+ is(localVar5.get("someProp1").target.querySelector(".value").getAttribute("value"), "true",
+ "The grip information for the localVar1 wasn't set correctly.");
+ is(localVar5.get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"",
+ "The grip information for the localVar2 wasn't set correctly.");
+ is(localVar5.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the localVar3 wasn't set correctly.");
+ is(localVar5.get("someProp4").target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the localVar4 wasn't set correctly.");
+ is(localVar5.get("someProp5").target.querySelector(".value").getAttribute("value"), "[object Object]",
+ "The grip information for the localVar5 wasn't set correctly.");
+ is(localVar5.get("someUndefined").target.querySelector(".value").getAttribute("value"), "undefined",
+ "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-localVar0 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp1").target.querySelector(".value").getAttribute("value"), "true",
+ "The grip information for the sub-localVar1 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"",
+ "The grip information for the sub-localVar2 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
+ "The grip information for the sub-localVar3 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp4").target.querySelector(".value").getAttribute("value"), "null",
+ "The grip information for the sub-localVar4 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someProp5").target.querySelector(".value").getAttribute("value"), "[object Object]",
+ "The grip information for the sub-localVar5 wasn't set correctly.");
+ is(localVar5.get("someProp5").get("someUndefined").target.querySelector(".value").getAttribute("value"), "undefined",
+ "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.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-07.js b/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
new file mode 100644
index 000000000..b8dfce0af
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
@@ -0,0 +1,106 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view displays function parameters.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testFrameParameters();
+ });
+}
+
+function testFrameParameters()
+{
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ localScope = gDebugger.DebuggerView.Variables._list.querySelector(".variables-view-scope"),
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+
+ is(localNodes.length, 12,
+ "The localScope should contain all the created variable elements.");
+
+ is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Window]",
+ "Should have the right property value for 'this'.");
+
+ is(localNodes[1].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for 'aArg'.");
+
+ is(localNodes[2].querySelector(".value").getAttribute("value"), '"beta"',
+ "Should have the right property value for 'bArg'.");
+
+ is(localNodes[3].querySelector(".value").getAttribute("value"), "3",
+ "Should have the right property value for 'cArg'.");
+
+ is(localNodes[4].querySelector(".value").getAttribute("value"), "false",
+ "Should have the right property value for 'dArg'.");
+
+ is(localNodes[5].querySelector(".value").getAttribute("value"), "null",
+ "Should have the right property value for 'eArg'.");
+
+ is(localNodes[6].querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right property value for 'fArg'.");
+
+ is(localNodes[7].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for 'a'.");
+
+ is(localNodes[8].querySelector(".value").getAttribute("value"), "[object Arguments]",
+ "Should have the right property value for 'arguments'.");
+
+ is(localNodes[9].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for 'b'.");
+
+ is(localNodes[10].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for 'c'.");
+
+ resumeAndFinish();
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames.");
+
+ closeDebuggerAndFinish();
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-08.js b/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
new file mode 100644
index 000000000..c050676ab
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
@@ -0,0 +1,244 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view displays the properties of objects.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testFrameParameters();
+ });
+}
+
+function testFrameParameters()
+{
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[0],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ localNonEnums = localScope.querySelector(".nonenum").childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+
+ is(localNodes.length + localNonEnums.length, 12,
+ "The localScope and localNonEnums should contain all the created variable elements.");
+
+ is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Window]",
+ "Should have the right property value for 'this'.");
+ is(localNodes[8].querySelector(".value").getAttribute("value"), "[object Arguments]",
+ "Should have the right property value for 'arguments'.");
+ is(localNodes[10].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for 'c'.");
+
+
+ let gVars = gDebugger.DebuggerView.Variables;
+
+ is(gVars.getScopeForNode(
+ gVars._list.querySelectorAll(".variables-view-scope")[0]).target,
+ gVars._list.querySelectorAll(".variables-view-scope")[0],
+ "getScopeForNode([0]) didn't return the expected scope.");
+ is(gVars.getScopeForNode(
+ gVars._list.querySelectorAll(".variables-view-scope")[1]).target,
+ gVars._list.querySelectorAll(".variables-view-scope")[1],
+ "getScopeForNode([1]) didn't return the expected scope.");
+ is(gVars.getScopeForNode(
+ gVars._list.querySelectorAll(".variables-view-scope")[2]).target,
+ gVars._list.querySelectorAll(".variables-view-scope")[2],
+ "getScopeForNode([2]) didn't return the expected scope.");
+
+ is(gVars.getScopeForNode(gVars._list.querySelectorAll(".variables-view-scope")[0]).expanded, true,
+ "The local scope should be expanded by default.");
+ is(gVars.getScopeForNode(gVars._list.querySelectorAll(".variables-view-scope")[1]).expanded, false,
+ "The block scope should be collapsed by default.");
+ is(gVars.getScopeForNode(gVars._list.querySelectorAll(".variables-view-scope")[2]).expanded, false,
+ "The global scope should be collapsed by default.");
+
+
+ let thisNode = gVars.getItemForNode(localNodes[0]);
+ let argumentsNode = gVars.getItemForNode(localNodes[8]);
+ let cNode = gVars.getItemForNode(localNodes[10]);
+
+ is(thisNode.expanded, false,
+ "The thisNode should not be expanded at this point.");
+ is(argumentsNode.expanded, false,
+ "The argumentsNode should not be expanded at this point.");
+ is(cNode.expanded, false,
+ "The cNode should not be expanded at this point.");
+
+ // Expand the 'this', 'arguments' and 'c' tree nodes. This causes
+ // their properties to be retrieved and displayed.
+ thisNode.expand();
+ argumentsNode.expand();
+ cNode.expand();
+
+ is(thisNode.expanded, true,
+ "The thisNode should be expanded at this point.");
+ is(argumentsNode.expanded, true,
+ "The argumentsNode should be expanded at this point.");
+ is(cNode.expanded, true,
+ "The cNode should be expanded at this point.");
+
+ // Poll every few milliseconds until the properties are retrieved.
+ // It's important to set the timer in the chrome window, because the
+ // content window timers are disabled while the debuggee is paused.
+ let count = 0;
+ let intervalID = window.setInterval(function(){
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the properties.");
+ window.clearInterval(intervalID);
+ return resumeAndFinish();
+ }
+ if (!thisNode._retrieved ||
+ !argumentsNode._retrieved ||
+ !cNode._retrieved) {
+ return;
+ }
+ window.clearInterval(intervalID);
+
+ is(thisNode.target.querySelector(".value")
+ .getAttribute("value"), "[object Window]",
+ "Should have the right property value for 'this'.");
+
+ is(thisNode.get("window").target.querySelector(".name")
+ .getAttribute("value"), "window",
+ "Should have the right property name for 'window'.");
+ ok(thisNode.get("window").target.querySelector(".value")
+ .getAttribute("value").search(/object/) != -1,
+ "'window' should be an object.");
+
+ is(thisNode.get("document").target.querySelector(".name")
+ .getAttribute("value"), "document",
+ "Should have the right property name for 'document'.");
+ ok(thisNode.get("document").target.querySelector(".value")
+ .getAttribute("value").search(/object/) != -1,
+ "'document' should be an object.");
+
+
+ is(argumentsNode.target.querySelector(".value")
+ .getAttribute("value"), "[object Arguments]",
+ "Should have the right property value for 'arguments'.");
+
+ is(argumentsNode.target.querySelectorAll(".variables-view-property > .title > .name")[0]
+ .getAttribute("value"), "0",
+ "Should have the right property name for 'arguments[0]'.");
+ ok(argumentsNode.target.querySelectorAll(".variables-view-property > .title > .value")[0]
+ .getAttribute("value").search(/object/) != -1,
+ "'arguments[0]' should be an object.");
+
+ is(argumentsNode.target.querySelectorAll(".variables-view-property > .title > .name")[7]
+ .getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__'.");
+ ok(argumentsNode.target.querySelectorAll(".variables-view-property > .title > .value")[7]
+ .getAttribute("value").search(/object/) != -1,
+ "'__proto__' should be an object.");
+
+
+ is(cNode.target.querySelector(".value")
+ .getAttribute("value"), "[object Object]",
+ "Should have the right property value for 'c'.");
+
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .name")[0]
+ .getAttribute("value"), "a",
+ "Should have the right property name for 'c.a'.");
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .value")[0]
+ .getAttribute("value"), "1",
+ "Should have the right value for 'c.a'.");
+
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .name")[1]
+ .getAttribute("value"), "b",
+ "Should have the right property name for 'c.b'.");
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .value")[1]
+ .getAttribute("value"), "\"beta\"",
+ "Should have the right value for 'c.b'.");
+
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .name")[2]
+ .getAttribute("value"), "c",
+ "Should have the right property name for 'c.c'.");
+ is(cNode.target.querySelectorAll(".variables-view-property > .title > .value")[2]
+ .getAttribute("value"), "true",
+ "Should have the right value for 'c.c'.");
+
+
+ is(gVars.getItemForNode(
+ cNode.target.querySelectorAll(".variables-view-property")[0]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[0],
+ "getItemForNode([0]) didn't return the expected property.");
+
+ is(gVars.getItemForNode(
+ cNode.target.querySelectorAll(".variables-view-property")[1]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[1],
+ "getItemForNode([1]) didn't return the expected property.");
+
+ is(gVars.getItemForNode(
+ cNode.target.querySelectorAll(".variables-view-property")[2]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[2],
+ "getItemForNode([2]) didn't return the expected property.");
+
+
+ is(cNode.find(
+ cNode.target.querySelectorAll(".variables-view-property")[0]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[0],
+ "find([0]) didn't return the expected property.");
+
+ is(cNode.find(
+ cNode.target.querySelectorAll(".variables-view-property")[1]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[1],
+ "find([1]) didn't return the expected property.");
+
+ is(cNode.find(
+ cNode.target.querySelectorAll(".variables-view-property")[2]).target,
+ cNode.target.querySelectorAll(".variables-view-property")[2],
+ "find([2]) didn't return the expected property.");
+
+
+ resumeAndFinish();
+ }, 100);
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames.");
+
+ closeDebuggerAndFinish();
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-09.js b/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
new file mode 100644
index 000000000..c0f486d36
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
@@ -0,0 +1,105 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view populates the global scope pane.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ testFrameParameters();
+ });
+}
+
+function testFrameParameters()
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ globalScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[2],
+ globalNodes = globalScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+ "Should have three frames.");
+
+ is(globalNodes[1].querySelector(".name").getAttribute("value"), "SpecialPowers",
+ "Should have the right property name for |SpecialPowers|.");
+
+ is(globalNodes[1].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for |SpecialPowers|.");
+
+ let globalScopeObject = gDebugger.DebuggerView.Variables.getScopeForNode(globalScope);
+ let documentNode = globalScopeObject.get("document");
+
+ is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for |document|.");
+
+ is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+ "Should have the right property value for |document|.");
+
+ let len = globalNodes.length - 1;
+ is(globalNodes[len].querySelector(".name").getAttribute("value"), "window",
+ "Should have the right property name for |window|.");
+
+ is(globalNodes[len].querySelector(".value").getAttribute("value"), "[object Window]",
+ "Should have the right property value for |window|.");
+
+ resumeAndFinish();
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+ Services.tm.currentThread.dispatch({ run: function() {
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-10.js b/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
new file mode 100644
index 000000000..d62a09c4f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
@@ -0,0 +1,110 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view is correctly populated in |with| frames.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ testWithFrame();
+ });
+}
+
+function testWithFrame()
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 4 Debugger:FetchedVariables events, one from the global object
+ // scope, two from the |with| scopes and the regular one.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4],
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes,
+ globalNodes = globalScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 2,
+ "Should have three frames.");
+
+ is(scopes.childNodes.length, 5, "Should have 5 variable scopes.");
+
+ is(innerNodes[1].querySelector(".name").getAttribute("value"), "one",
+ "Should have the right property name for |one|.");
+
+ is(innerNodes[1].querySelector(".value").getAttribute("value"), "1",
+ "Should have the right property value for |one|.");
+
+ let globalScopeObject = gDebugger.DebuggerView.Variables.getScopeForNode(globalScope);
+ let documentNode = globalScopeObject.get("document");
+
+ is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for |document|.");
+
+ is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+ "Should have the right property value for |document|.");
+
+ let len = globalNodes.length - 1;
+ is(globalNodes[len].querySelector(".name").getAttribute("value"), "window",
+ "Should have the right property name for |window|.");
+
+ is(globalNodes[len].querySelector(".value").getAttribute("value"), "[object Window]",
+ "Should have the right property value for |window|.");
+
+ resumeAndFinish();
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+ Services.tm.currentThread.dispatch({ run: function() {
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-11.js b/browser/devtools/debugger/test/browser_dbg_propertyview-11.js
new file mode 100644
index 000000000..219a98096
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-11.js
@@ -0,0 +1,241 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly displays WebIDL attributes in DOM
+ * objects.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+let gPane = null;
+let gTab = null;
+let gDebugger = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = true;
+ testFrameParameters();
+ });
+}
+
+function testFrameParameters()
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let anonymousScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[1],
+ globalScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[2],
+ anonymousNodes = anonymousScope.querySelector(".variables-view-element-details").childNodes,
+ globalNodes = globalScope.querySelector(".variables-view-element-details").childNodes,
+ gVars = gDebugger.DebuggerView.Variables;
+
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(anonymousNodes[1].querySelector(".name").getAttribute("value"), "button",
+ "Should have the right property name for |button|.");
+
+ is(anonymousNodes[1].querySelector(".value").getAttribute("value"), "[object HTMLButtonElement]",
+ "Should have the right property value for |button|.");
+
+ is(anonymousNodes[2].querySelector(".name").getAttribute("value"), "buttonAsProto",
+ "Should have the right property name for |buttonAsProto|.");
+
+ is(anonymousNodes[2].querySelector(".value").getAttribute("value"), "[object Object]",
+ "Should have the right property value for |buttonAsProto|.");
+
+ let globalScopeObject = gVars.getScopeForNode(globalScope);
+ let documentNode = globalScopeObject.get("document");
+
+ is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
+ "Should have the right property name for |document|.");
+
+ is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+ "Should have the right property value for |document|.");
+
+ let buttonNode = gVars.getItemForNode(anonymousNodes[1]);
+ let buttonAsProtoNode = gVars.getItemForNode(anonymousNodes[2]);
+
+ is(buttonNode.expanded, false,
+ "The buttonNode should not be expanded at this point.");
+ is(buttonAsProtoNode.expanded, false,
+ "The buttonAsProtoNode should not be expanded at this point.");
+ is(documentNode.expanded, false,
+ "The documentNode should not be expanded at this point.");
+
+ // Expand the 'button', 'buttonAsProto' and 'document' tree nodes. This
+ // causes their properties to be retrieved and displayed.
+ buttonNode.expand();
+ buttonAsProtoNode.expand();
+ documentNode.expand();
+
+ is(buttonNode.expanded, true,
+ "The buttonNode should be expanded at this point.");
+ is(buttonAsProtoNode.expanded, true,
+ "The buttonAsProtoNode should be expanded at this point.");
+ is(documentNode.expanded, true,
+ "The documentNode should be expanded at this point.");
+
+ // Poll every few milliseconds until the properties are retrieved.
+ // It's important to set the timer in the chrome window, because the
+ // content window timers are disabled while the debuggee is paused.
+ let count1 = 0;
+ let intervalID = window.setInterval(function(){
+ info("count1: " + count1);
+ if (++count1 > 50) {
+ ok(false, "Timed out while polling for the properties.");
+ window.clearInterval(intervalID);
+ return resumeAndFinish();
+ }
+ if (!buttonNode._retrieved ||
+ !buttonAsProtoNode._retrieved ||
+ !documentNode._retrieved) {
+ return;
+ }
+ window.clearInterval(intervalID);
+
+ // Test the prototypes of these objects.
+ is(buttonNode.get("__proto__").target.querySelector(".name")
+ .getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__' in buttonNode.");
+ ok(buttonNode.get("__proto__").target.querySelector(".value")
+ .getAttribute("value").search(/object/) != -1,
+ "'__proto__' in buttonNode should be an object.");
+
+ is(buttonAsProtoNode.get("__proto__").target.querySelector(".name")
+ .getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__' in buttonAsProtoNode.");
+ ok(buttonAsProtoNode.get("__proto__").target.querySelector(".value")
+ .getAttribute("value").search(/object/) != -1,
+ "'__proto__' in buttonAsProtoNode should be an object.");
+
+ is(documentNode.get("__proto__").target.querySelector(".name")
+ .getAttribute("value"), "__proto__",
+ "Should have the right property name for '__proto__' in documentNode.");
+ ok(documentNode.get("__proto__").target.querySelector(".value")
+ .getAttribute("value").search(/object/) != -1,
+ "'__proto__' in documentNode should be an object.");
+
+ // Now the main course: make sure that the native getters for WebIDL
+ // attributes have been called and a value has been returned.
+ is(buttonNode.get("type").target.querySelector(".name")
+ .getAttribute("value"), "type",
+ "Should have the right property name for 'type' in buttonProtoNode.");
+ is(buttonNode.get("type").target.querySelector(".value")
+ .getAttribute("value"), '"submit"',
+ "'type' in buttonProtoNode should have the right value.");
+ is(buttonNode.get("formMethod").target.querySelector(".name")
+ .getAttribute("value"), "formMethod",
+ "Should have the right property name for 'formMethod' in buttonProtoNode.");
+ is(buttonNode.get("formMethod").target.querySelector(".value")
+ .getAttribute("value"), '""',
+ "'formMethod' in buttonProtoNode should have the right value.");
+
+ is(documentNode.get("domain").target.querySelector(".name")
+ .getAttribute("value"), "domain",
+ "Should have the right property name for 'domain' in documentProtoNode.");
+ is(documentNode.get("domain").target.querySelector(".value")
+ .getAttribute("value"), '"example.com"',
+ "'domain' in documentProtoNode should have the right value.");
+ is(documentNode.get("cookie").target.querySelector(".name")
+ .getAttribute("value"), "cookie",
+ "Should have the right property name for 'cookie' in documentProtoNode.");
+ is(documentNode.get("cookie").target.querySelector(".value")
+ .getAttribute("value"), '""',
+ "'cookie' in documentProtoNode should have the right value.");
+
+ let buttonAsProtoProtoNode = buttonAsProtoNode.get("__proto__");
+
+ is(buttonAsProtoProtoNode.expanded, false,
+ "The buttonAsProtoProtoNode should not be expanded at this point.");
+
+ // Expand the prototypes of 'button', 'buttonAsProto' and 'document'
+ // tree nodes. This causes their properties to be retrieved and
+ // displayed.
+ buttonAsProtoProtoNode.expand();
+
+ is(buttonAsProtoProtoNode.expanded, true,
+ "The buttonAsProtoProtoNode should be expanded at this point.");
+
+
+ // Poll every few milliseconds until the properties are retrieved.
+ // It's important to set the timer in the chrome window, because the
+ // content window timers are disabled while the debuggee is paused.
+ let count2 = 0;
+ let intervalID1 = window.setInterval(function(){
+ info("count2: " + count2);
+ if (++count2 > 50) {
+ ok(false, "Timed out while polling for the properties.");
+ window.clearInterval(intervalID1);
+ return resumeAndFinish();
+ }
+ if (!buttonAsProtoProtoNode._retrieved) {
+ return;
+ }
+ window.clearInterval(intervalID1);
+
+ // Test this more involved case that reuses an object that is
+ // present in another cache line.
+ is(buttonAsProtoProtoNode.get("type").target.querySelector(".name")
+ .getAttribute("value"), "type",
+ "Should have the right property name for 'type' in buttonAsProtoProtoProtoNode.");
+ is(buttonAsProtoProtoNode.get("type").target.querySelector(".value")
+ .getAttribute("value"), '"submit"',
+ "'type' in buttonAsProtoProtoProtoNode should have the right value.");
+ is(buttonAsProtoProtoNode.get("formMethod").target.querySelector(".name")
+ .getAttribute("value"), "formMethod",
+ "Should have the right property name for 'formMethod' in buttonAsProtoProtoProtoNode.");
+ is(buttonAsProtoProtoNode.get("formMethod").target.querySelector(".value")
+ .getAttribute("value"), '""',
+ "'formMethod' in buttonAsProtoProtoProtoNode should have the right value.");
+
+ resumeAndFinish();
+ }, 100);
+ }, 100);
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function resumeAndFinish() {
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+ Services.tm.currentThread.dispatch({ run: function() {
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-12.js b/browser/devtools/debugger/test/browser_dbg_propertyview-12.js
new file mode 100644
index 000000000..192bcfcc9
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-12.js
@@ -0,0 +1,95 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * 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.
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testFSN();
+ });
+}
+
+function testFSN() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function _onFetchedVariables() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", _onFetchedVariables, false);
+ runTest();
+ }, false);
+
+ gDebuggee.eval("(" + 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.DebuggerView.Variables._parent
+ .querySelectorAll(".variable-or-property");
+
+ for (let v of variables) {
+ let name = v.querySelector(".name").getAttribute("value");
+ let tester = testers[name];
+ delete testers[name];
+ ok(tester, "We should have a tester for the '" + name + "' variable.");
+ tester(v);
+ }
+
+ is(Object.keys(testers).length, 0,
+ "We should have run and removed all the testers.");
+
+ closeDebuggerAndFinish();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js b/browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js
new file mode 100644
index 000000000..5a4ce4a62
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js
@@ -0,0 +1,147 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view remains responsive when faced with
+ * huge ammounts of data.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_big-data.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+requestLongerTimeout(10);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ gDebugger.DebuggerView.Variables.lazyAppend = true;
+ testWithFrame();
+ });
+}
+
+function testWithFrame()
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test1() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test1, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes,
+ arrayNodes = innerNodes[4].querySelector(".variables-view-element-details").childNodes;
+
+ is(innerNodes[3].querySelector(".name").getAttribute("value"), "buffer",
+ "Should have the right property name for |buffer|.");
+
+ is(innerNodes[3].querySelector(".value").getAttribute("value"), "[object ArrayBuffer]",
+ "Should have the right property value for |buffer|.");
+
+ is(innerNodes[4].querySelector(".name").getAttribute("value"), "z",
+ "Should have the right property name for |z|.");
+
+ is(innerNodes[4].querySelector(".value").getAttribute("value"), "[object Int8Array]",
+ "Should have the right property value for |z|.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, innerNodes[3].querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, innerNodes[4].querySelector(".arrow"), gDebugger);
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test2() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test2, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let total = 10000;
+ let loaded = 0;
+ let paints = 0;
+
+ waitForProperties(total, {
+ onLoading: function(count) {
+ ok(count >= loaded, "Should have loaded more properties.");
+ info("Displayed " + count + " properties, not finished yet.");
+ info("Remaining " + (total - count) + " properties to display.");
+ loaded = count;
+ paints++;
+
+ loadScope.hidden = true;
+ globalScope.hidden = true;
+ scopes.parentNode.scrollTop = scopes.parentNode.scrollHeight;
+ },
+ onFinished: function(count) {
+ ok(count == total, "Displayed all the properties.");
+ isnot(paints, 0, "Debugger was unresponsive, sad panda.");
+
+ for (let i = 0; i < arrayNodes.length; i++) {
+ let node = arrayNodes[i];
+ let name = node.querySelector(".name").getAttribute("value");
+ // Don't make the test runner dump to the console for every test
+ // unless something goes wrong.
+ if (name !== i + "") {
+ ok(false, "The array items aren't in the correct order.");
+ }
+ }
+
+ closeDebuggerAndFinish();
+ }
+ });
+ }}, 0);
+ }, false);
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function waitForProperties(total, callbacks)
+{
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes,
+ arrayNodes = innerNodes[4].querySelector(".variables-view-element-details").childNodes;
+
+ // Poll every few milliseconds until the properties are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > total) {
+ ok(false, "Timed out while polling for the properties.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ // Still need to wait for a few more properties to be fetched.
+ if (arrayNodes.length < total) {
+ callbacks.onLoading(arrayNodes.length);
+ return;
+ }
+ // We got all the properties, it's safe to callback.
+ window.clearInterval(intervalID);
+ callbacks.onFinished(arrayNodes.length);
+ }, 100);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js
new file mode 100644
index 000000000..b50df0930
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js
@@ -0,0 +1,352 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view knows how to edit getters and setters.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gVars = null;
+var gWatch = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gVars = gDebugger.DebuggerView.Variables;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ gVars.switch = function() {};
+ gVars.delete = function() {};
+
+ prepareVariablesView();
+ });
+}
+
+function prepareVariablesView() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ testVariablesView();
+
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function testVariablesView()
+{
+ executeSoon(function() {
+ addWatchExpressions(function() {
+ testEdit("set", "this._prop = value + ' BEER CAN'", function() {
+ testEdit("set", "{ this._prop = value + ' BEACON' }", function() {
+ testEdit("set", "{ this._prop = value + ' BEACON;'; }", function() {
+ testEdit("set", "{ return this._prop = value + ' BEACON;;'; }", function() {
+ testEdit("set", "function(value) { this._prop = value + ' BACON' }", function() {
+ testEdit("get", "'brelx BEER CAN'", function() {
+ testEdit("get", "{ 'brelx BEACON' }", function() {
+ testEdit("get", "{ 'brelx BEACON;'; }", function() {
+ testEdit("get", "{ return 'brelx BEACON;;'; }", function() {
+ testEdit("get", "function() { return 'brelx BACON'; }", function() {
+ testEdit("get", "bogus", function() {
+ testEdit("set", "sugob", function() {
+ testEdit("get", "", function() {
+ testEdit("set", "", function() {
+ waitForWatchExpressions(function() {
+ testEdit("self", "2507", function() {
+ closeDebuggerAndFinish();
+ }, {
+ "myVar.prop": 2507,
+ "myVar.prop + 42": "250742"
+ });
+ })
+ gWatch.deleteExpression({ name: "myVar.prop = 'xlerb'" });
+ }, {
+ "myVar.prop": "xlerb",
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+ });
+ }, {
+ "myVar.prop": "ReferenceError: bogus is not defined",
+ "myVar.prop + 42": "ReferenceError: bogus is not defined",
+ "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+ });
+ }, {
+ "myVar.prop": "ReferenceError: bogus is not defined",
+ "myVar.prop + 42": "ReferenceError: bogus is not defined",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "brelx BACON",
+ "myVar.prop + 42": "brelx BACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "brelx BEACON;;",
+ "myVar.prop + 42": "brelx BEACON;;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": undefined,
+ "myVar.prop + 42": NaN,
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "brelx BEER CAN",
+ "myVar.prop + 42": "brelx BEER CAN42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "xlerb BACON",
+ "myVar.prop + 42": "xlerb BACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "xlerb BEACON;;",
+ "myVar.prop + 42": "xlerb BEACON;;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "xlerb BEACON;",
+ "myVar.prop + 42": "xlerb BEACON;42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "xlerb BEACON",
+ "myVar.prop + 42": "xlerb BEACON42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ }, {
+ "myVar.prop": "xlerb BEER CAN",
+ "myVar.prop + 42": "xlerb BEER CAN42",
+ "myVar.prop = 'xlerb'": "xlerb"
+ });
+ });
+ });
+}
+
+function addWatchExpressions(callback)
+{
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 1, "There should be 1 evaluation availalble");
+
+ let w1 = scope.get("myVar.prop");
+ let w2 = scope.get("myVar.prop + 42");
+ let w3 = scope.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.");
+
+
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 2, "There should be 2 evaluations availalble");
+
+ let w1 = scope.get("myVar.prop");
+ let w2 = scope.get("myVar.prop + 42");
+ let w3 = scope.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.");
+
+
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 3, "There should be 3 evaluations availalble");
+
+ let w1 = scope.get("myVar.prop");
+ let w2 = scope.get("myVar.prop + 42");
+ let w3 = scope.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.");
+
+ callback();
+ });
+
+ gWatch.addExpression("myVar.prop = 'xlerb'");
+ gDebugger.editor.focus();
+ });
+
+ gWatch.addExpression("myVar.prop + 42");
+ gDebugger.editor.focus();
+ });
+
+ gWatch.addExpression("myVar.prop");
+ gDebugger.editor.focus();
+}
+
+function testEdit(what, string, callback, expected)
+{
+ let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[1],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ myVar = gVars.getItemForNode(localNodes[11]);
+
+ waitForProperties(function() {
+ let prop = myVar.get("prop");
+ let getterOrSetter = (what != "self" ? prop.get(what) : prop);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ getterOrSetter._target.querySelector(".title > .value"),
+ gDebugger);
+
+ waitForElement(".element-value-input", true, function() {
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ let w1 = scope.get(Object.keys(expected)[0]);
+ let w2 = scope.get(Object.keys(expected)[1]);
+ let w3 = scope.get(Object.keys(expected)[2]);
+
+ if (w1) {
+ if (isNaN(expected[w1.name]) && typeof expected[w1.name] == "number") {
+ ok(isNaN(w1.value),
+ "The first expression value is correct after the edit (NaN).");
+ } else if (expected[w1.name] === undefined) {
+ is(w1.value.type, "undefined",
+ "The first expression value is correct after the edit (undefined).");
+ } else {
+ is(w1.value, expected[w1.name],
+ "The first expression value is correct after the edit.");
+ }
+ info(w1.value + " is equal to " + expected[w1.name]);
+ }
+ if (w2) {
+ if (isNaN(expected[w2.name]) && typeof expected[w2.name] == "number") {
+ ok(isNaN(w2.value),
+ "The second expression value is correct after the edit (NaN).");
+ } else if (expected[w2.name] === undefined) {
+ is(w2.value.type, "undefined",
+ "The second expression value is correct after the edit (undefined).");
+ } else {
+ is(w2.value, expected[w2.name],
+ "The second expression value is correct after the edit.");
+ }
+ info(w2.value + " is equal to " + expected[w2.name]);
+ }
+ if (w3) {
+ if (isNaN(expected[w3.name]) && typeof expected[w3.name] == "number") {
+ ok(isNaN(w3.value),
+ "The third expression value is correct after the edit (NaN).");
+ } else if (expected[w3.name] === undefined) {
+ is(w3.value.type, "undefined",
+ "The third expression value is correct after the edit (undefined).");
+ } else {
+ is(w3.value, expected[w3.name],
+ "The third expression value is correct after the edit.");
+ }
+ info(w3.value + " is equal to " + expected[w3.name]);
+ }
+
+ callback();
+ });
+
+ info("Changing the " + what + "ter with '" + string + "'.");
+
+ write(string);
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ });
+
+ myVar.expand();
+ gVars.clearHierarchy();
+}
+
+function waitForWatchExpressions(callback) {
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+ executeSoon(callback);
+ }, false);
+}
+
+function waitForProperties(callback) {
+ gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
+ executeSoon(callback);
+ }, false);
+}
+
+function waitForElement(selector, exists, callback)
+{
+ // Poll every few milliseconds until the element are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the element.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (!!gVars._list.querySelector(selector) != exists) {
+ return;
+ }
+ // We got the element, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+function write(text) {
+ if (!text) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ return;
+ }
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gVars = null;
+ gWatch = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js
new file mode 100644
index 000000000..81b0d3b75
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js
@@ -0,0 +1,185 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view is able to override getter properties
+ * to plain value properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gVars = null;
+var gWatch = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gVars = gDebugger.DebuggerView.Variables;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+ gVars.switch = function() {};
+ gVars.delete = function() {};
+
+ prepareVariablesView();
+ });
+}
+
+function prepareVariablesView() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ testVariablesView();
+
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function testVariablesView()
+{
+ executeSoon(function() {
+ addWatchExpressions(function() {
+ testEdit("\"xlerb\"", "xlerb", function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+function addWatchExpressions(callback)
+{
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 1, "There should be 1 evaluation availalble");
+
+ let expr = scope.get("myVar.prop");
+ ok(expr, "The watch expression should be present in the scope");
+ is(expr.value, 42, "The value is correct.");
+
+ callback();
+ });
+
+ gWatch.addExpression("myVar.prop");
+ gDebugger.editor.focus();
+}
+
+function testEdit(string, expected, callback)
+{
+ let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[1],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ myVar = gVars.getItemForNode(localNodes[11]);
+
+ waitForProperties(function() {
+ let prop = myVar.get("prop");
+
+ is(prop.ownerView.name, "myVar",
+ "The right owner property name wasn't found.");
+ is(prop.name, "prop",
+ "The right property name wasn't found.");
+
+ is(prop.ownerView.value.type, "object",
+ "The right owner property value type wasn't found.");
+ is(prop.ownerView.value.class, "Object",
+ "The right owner property value class wasn't found.");
+
+ is(prop.name, "prop",
+ "The right property name wasn't found.");
+ is(prop.value, undefined,
+ "The right property value wasn't found.");
+ ok(prop.getter,
+ "The right property getter wasn't found.");
+ ok(prop.setter,
+ "The right property setter wasn't found.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ prop._target.querySelector(".variables-view-edit"),
+ gDebugger);
+
+ waitForElement(".element-value-input", true, function() {
+ waitForWatchExpressions(function() {
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+
+ let expr = scope.get("myVar.prop");
+ is(expr.value, expected, "The value is correct.");
+
+ callback();
+ });
+
+ write(string);
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+ });
+
+ myVar.expand();
+ gVars.clearHierarchy();
+}
+
+function waitForWatchExpressions(callback) {
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+ executeSoon(callback);
+ }, false);
+}
+
+function waitForProperties(callback) {
+ gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
+ executeSoon(callback);
+ }, false);
+}
+
+function waitForElement(selector, exists, callback)
+{
+ // Poll every few milliseconds until the element are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the element.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (!!gVars._list.querySelector(selector) != exists) {
+ return;
+ }
+ // We got the element, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+function write(text) {
+ if (!text) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ return;
+ }
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gVars = null;
+ gWatch = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-data.js b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
new file mode 100644
index 000000000..6cf2c0560
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
@@ -0,0 +1,888 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly populates itself.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gVariablesView = null;
+var gScope = null;
+var gVariable = null;
+
+function test()
+{
+ debug_tab_pane(TAB1_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gVariablesView = gDebugger.DebuggerView.Variables;
+
+ gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
+ testVariablesView();
+ });
+}
+
+function testVariablesView()
+{
+ 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;
+
+ testHierarchy();
+ testHeader();
+ testFirstLevelContents();
+ testSecondLevelContents();
+ testThirdLevelContents();
+ testIntegrity(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);
+
+ executeSoon(function() {
+ testKeyboardAccessibility(function() {
+ testClearHierarchy();
+
+ closeDebuggerAndFinish();
+ });
+ });
+}
+
+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");
+
+ 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.");
+
+
+ let someProp6 = gVariable.get("someProp6");
+
+ 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");
+ let __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 testIntegrity(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("non-header"), 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("non-header"), 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("non-header"), 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("non-header"), true,
+ "The non-header attribute should not be applied to variables without headers.");
+}
+
+function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
+ is(fooScope.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all scopes.");
+ is(fooScope.descriptorTooltip, gVariablesView.descriptorTooltip,
+ "The descriptorTooltip 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.");
+ 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.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all variables.");
+ is(barVar.descriptorTooltip, gVariablesView.descriptorTooltip,
+ "The descriptorTooltip 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.");
+ 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.editableValueTooltip, gVariablesView.editableValueTooltip,
+ "The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
+ "The deleteButtonTooltip property should persist from the view to all properties.");
+ is(bazProperty.descriptorTooltip, gVariablesView.descriptorTooltip,
+ "The descriptorTooltip 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.");
+ 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 testKeyboardAccessibility(callback) {
+ gDebugger.DebuggerView.Filtering._doVariablesFocus();
+ gDebugger.DebuggerView.Variables.pageSize = 5;
+
+ 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 again.");
+
+
+ ok(!gVariablesView._list.querySelector(".element-value-input"),
+ "There shouldn't be a value input element created.");
+
+ EventUtils.synthesizeKey("VK_ENTER", {}, gDebugger);
+ waitForElement(".element-value-input", true, function() {
+
+ ok(gVariablesView._list.querySelector(".element-value-input"),
+ "There should be a value input element created.");
+
+ EventUtils.sendKey("ESCAPE", gDebugger);
+ waitForElement(".element-value-input", false, function() {
+
+ ok(!gVariablesView._list.querySelector(".element-value-input"),
+ "There shouldn't be a value input element anymore.");
+
+ ok(!gVariablesView._list.querySelector(".element-name-input"),
+ "There shouldn't be a name input element created.");
+
+ EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, gDebugger);
+ waitForElement(".element-name-input", true, function() {
+
+ ok(gVariablesView._list.querySelector(".element-name-input"),
+ "There should be a name input element created.");
+
+ EventUtils.sendKey("ESCAPE", gDebugger);
+ waitForElement(".element-name-input", false, function() {
+
+ ok(!gVariablesView._list.querySelector(".element-name-input"),
+ "There shouldn't be a name input element anymore.");
+
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ executeSoon(function() {
+ is(gVariablesView._parent.scrollTop, 0,
+ "The variables view shouldn't scroll when pressing the DOWN key.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ executeSoon(function() {
+ is(gVariablesView._parent.scrollTop, 0,
+ "The variables view shouldn't scroll when pressing the UP key.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The someProp5 item should be focused now.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "0",
+ "The 0 item should be focused now.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "bar",
+ "The bar item should be focused now.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "bar",
+ "The bar item should still be focused now.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo item should be focused now.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo item should still be focused now.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "bar",
+ "The bar item should be focused now.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp7",
+ "The someProp7 item should be focused now.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The __proto__ item should be focused now.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "set",
+ "The set item should be focused now.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "get",
+ "The get item should be focused now.");
+
+ EventUtils.sendKey("HOME", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The someProp0 item should be focused now.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The someProp0 item should still be focused now.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The someProp0 item should still be focused now.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The someProp0 item should still be focused now.");
+
+ for (let i = 0; i < 16; i++) {
+ // Advance to the first collapsed __proto__ property.
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The __proto__ item should be focused now.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The __proto__ item shouldn't be expanded yet.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The __proto__ item should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The __proto__ item should be expanded now.");
+
+ for (let i = 0; i < 3; i++) {
+ // Advance to the fifth top-level someProp5 property.
+ EventUtils.sendKey("LEFT", gDebugger);
+ }
+ is(gVariablesView.getFocusedItem().name, "5",
+ "The fifth array item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The fifth array item should not be expanded now.");
+
+ for (let i = 0; i < 6; i++) {
+ // Advance to the fifth top-level someProp5 property.
+ EventUtils.sendKey("UP", gDebugger);
+ }
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The someProp5 item should be focused now.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The someProp5 item should already be expanded.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The someProp5 item should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The someProp5 item should not be expanded now.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp5",
+ "The someProp5 item should still be focused.");
+
+ 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("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "someProp0",
+ "The someProp0 item should still be focused.");
+
+ for (let i = 0; i < 32; i++) {
+ // Advance to the last property in this scope.
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The top-level __proto__ item should be focused.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo scope should be focused now.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The foo scope should already be expanded.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo scope should be focused now.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The foo scope shouldn't be expanded now.");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "bar",
+ "The bar variable should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The bar variable shouldn't be expanded.");
+ is(gVariablesView.getFocusedItem().visible, true,
+ "The bar variable shouldn't be hidden.");
+
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "bar",
+ "The bar variable should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The bar variable should still not be expanded.");
+ is(gVariablesView.getFocusedItem().visible, false,
+ "The bar variable should be hidden.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo scope should be focused.");
+
+ EventUtils.sendKey("UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The top-level __proto__ item should be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The top-level __proto__ item should not be expanded.");
+
+ EventUtils.sendKey("RIGHT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The top-level __proto__ item should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, true,
+ "The top-level __proto__ item should be expanded.");
+
+ EventUtils.sendKey("LEFT", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The top-level __proto__ item should still be focused.");
+ is(gVariablesView.getFocusedItem().expanded, false,
+ "The top-level __proto__ item should not be expanded.");
+
+ EventUtils.sendKey("END", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo scope should be focused.");
+
+ EventUtils.sendKey("PAGE_UP", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "__proto__",
+ "The __proto__ property should be focused.");
+
+ EventUtils.sendKey("PAGE_DOWN", gDebugger);
+ is(gVariablesView.getFocusedItem().name, "foo",
+ "The foo scope should be focused.");
+
+ executeSoon(callback);
+ });
+ });
+ });
+ });
+ });
+ });
+}
+
+function waitForElement(selector, exists, callback)
+{
+ // Poll every few milliseconds until the element are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the element.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (!!gVariablesView._list.querySelector(selector) != exists) {
+ return;
+ }
+ // We got the element, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+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() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gVariablesView = null;
+ gScope = null;
+ gVariable = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js b/browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js
new file mode 100644
index 000000000..6dba43421
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js
@@ -0,0 +1,119 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * 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 + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+requestLongerTimeout(3);
+
+function test() {
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ testFrameEval();
+ });
+}
+
+function testFrameEval() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ var localScope = gDebugger.DebuggerView.Variables._list.querySelector(".variables-view-scope"),
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ varA = localNodes[7];
+
+ is(varA.querySelector(".name").getAttribute("value"), "a",
+ "Should have the right name for 'a'.");
+
+ is(varA.querySelector(".value").getAttribute("value"), 1,
+ "Should have the right initial value for 'a'.");
+
+ testModification(varA, function(aVar) {
+ testModification(aVar, function(aVar) {
+ testModification(aVar, function(aVar) {
+ resumeAndFinish();
+ }, "document.title", '"Debugger Function Call Parameter Test"');
+ }, "b", "[object Object]");
+ }, "{ a: 1 }", "[object Object]");
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.querySelector("button"),
+ content.window);
+}
+
+function testModification(aVar, aCallback, aNewValue, aNewResult) {
+ function makeChangesAndExitInputMode() {
+ EventUtils.sendString(aNewValue, gDebugger);
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ aVar.querySelector(".value"),
+ gDebugger);
+
+ executeSoon(function() {
+ ok(aVar.querySelector(".element-value-input"),
+ "There should be an input element created.");
+
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global
+ // object scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ // Get the variable reference anew, since the old ones were discarded when
+ // we resumed.
+ var localScope = gDebugger.DebuggerView.Variables._list.querySelector(".variables-view-scope"),
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ varA = localNodes[7];
+
+ is(varA.querySelector(".value").getAttribute("value"), aNewResult,
+ "Should have the right value for 'a'.");
+
+ executeSoon(function() {
+ aCallback(varA);
+ });
+ }, false);
+
+ makeChangesAndExitInputMode();
+ });
+}
+
+function resumeAndFinish() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js b/browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js
new file mode 100644
index 000000000..231b3a83a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js
@@ -0,0 +1,514 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * 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 + "browser_dbg_watch-expressions.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gWatch = null;
+var gVars = null;
+
+requestLongerTimeout(3);
+
+function test() {
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gWatch = gDebugger.DebuggerView.WatchExpressions;
+ gVars = gDebugger.DebuggerView.Variables;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ testFrameEval();
+ });
+}
+
+function testFrameEval() {
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function test() {
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[1],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ aArg = localNodes[1],
+ varT = localNodes[3];
+
+ is(aArg.querySelector(".name").getAttribute("value"), "aArg",
+ "Should have the right name for 'aArg'.");
+ is(varT.querySelector(".name").getAttribute("value"), "t",
+ "Should have the right name for 't'.");
+
+ is(aArg.querySelector(".value").getAttribute("value"), "undefined",
+ "Should have the right initial value for 'aArg'.");
+ is(varT.querySelector(".value").getAttribute("value"), "\"Browser Debugger Watch Expressions Test\"",
+ "Should have the right initial value for 't'.");
+
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.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 = gVars._currHierarchy.get(label);
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 5, "There should be 5 evaluations availalble");
+
+ is(scope.get("this")._isContentVisible, true,
+ "Should have the right visibility state for 'this'.");
+ is(scope.get("this").target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'this'.");
+ is(scope.get("this").name, "this",
+ "Should have the right name for 'this'.");
+ is(scope.get("this").value.type, "object",
+ "Should have the right value type for 'this'.");
+ is(scope.get("this").value.class, "Window",
+ "Should have the right value type for 'this'.");
+
+ is(scope.get("ermahgerd")._isContentVisible, true,
+ "Should have the right visibility state for 'ermahgerd'.");
+ is(scope.get("ermahgerd").target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'ermahgerd'.");
+ is(scope.get("ermahgerd").name, "ermahgerd",
+ "Should have the right name for 'ermahgerd'.");
+ is(scope.get("ermahgerd").value.type, "object",
+ "Should have the right value type for 'ermahgerd'.");
+ is(scope.get("ermahgerd").value.class, "Function",
+ "Should have the right value type for 'ermahgerd'.");
+
+ is(scope.get("aArg")._isContentVisible, true,
+ "Should have the right visibility state for 'aArg'.");
+ is(scope.get("aArg").target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'aArg'.");
+ is(scope.get("aArg").name, "aArg",
+ "Should have the right name for 'aArg'.");
+ is(scope.get("aArg").value.type, "undefined",
+ "Should have the right value for 'aArg'.");
+ is(scope.get("aArg").value.class, undefined,
+ "Should have the right value for 'aArg'.");
+
+ is(scope.get("document.title")._isContentVisible, true,
+ "Should have the right visibility state for 'document.title'.");
+ is(scope.get("document.title").target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'document.title'.");
+ is(scope.get("document.title").name, "document.title",
+ "Should have the right name for 'document.title'.");
+ is(scope.get("document.title").value, "42",
+ "Should have the right value for 'document.title'.");
+ is(typeof scope.get("document.title").value, "string",
+ "Should have the right value type for 'document.title'.");
+
+ is(scope.get("document.title = 42")._isContentVisible, true,
+ "Should have the right visibility state for 'document.title = 42'.");
+ is(scope.get("document.title = 42").target.querySelectorAll(".variables-view-delete").length, 1,
+ "Should have the one close button visible for 'document.title = 42'.");
+ is(scope.get("document.title = 42").name, "document.title = 42",
+ "Should have the right name for 'document.title = 42'.");
+ is(scope.get("document.title = 42").value, 42,
+ "Should have the right value for 'document.title = 42'.");
+ is(typeof scope.get("document.title = 42").value, "number",
+ "Should have the right value type for 'document.title = 42'.");
+
+ testModification(scope.get("document.title = 42").target, test1, function(scope) {
+ testModification(scope.get("aArg").target, test2, function(scope) {
+ testModification(scope.get("aArg = 44").target, test3, function(scope) {
+ testModification(scope.get("document.title = 43").target, test4, function(scope) {
+ testModification(scope.get("document.title").target, test5, function(scope) {
+ testExprDeletion(scope.get("this").target, test6, function(scope) {
+ testExprDeletion(null, test7, function(scope) {
+ resumeAndFinish();
+ }, 44, 0, true, true);
+ }, 44);
+ }, " \t\r\n", "\"43\"", 44, 1, true);
+ }, " \t\r\ndocument.title \t\r\n", "\"43\"", 44);
+ }, " \t\r\ndocument.title \t\r\n", "\"43\"", 44);
+ }, "aArg = 44", 44, 44);
+ }, "document.title = 43", 43, "undefined");
+ }}, 0);
+ }, false);
+
+ addWatchExpression("this");
+ addWatchExpression("ermahgerd");
+ addWatchExpression("aArg");
+ addWatchExpression("document.title");
+ addCmdWatchExpression("document.title = 42");
+
+ executeSoon(function() {
+ gDebuggee.ermahgerd(); // ermahgerd!!
+ });
+}
+
+function testModification(aVar, aTest, aCallback, aNewValue, aNewResult, aArgResult,
+ aLocalScopeIndex = 1, aDeletionFlag = null)
+{
+ function makeChangesAndExitInputMode() {
+ EventUtils.sendString(aNewValue, gDebugger);
+ EventUtils.sendKey("RETURN", gDebugger);
+ }
+
+ EventUtils.sendMouseEvent({ type: "dblclick" },
+ aVar.querySelector(".name"),
+ gDebugger);
+
+ executeSoon(function() {
+ ok(aVar.querySelector(".element-name-input"),
+ "There should be an input element created.");
+
+ let testContinued = false;
+ let fetchedVariables = false;
+ let fetchedExpressions = false;
+
+ let countV = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function testV() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global
+ // object scope and the regular one.
+ if (++countV < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + countV);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", testV, false);
+ fetchedVariables = true;
+ executeSoon(continueTest);
+ }, false);
+
+ let countE = 0;
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function testE() {
+ // We expect only one Debugger:FetchedWatchExpressions event, since all
+ // expressions are evaluated at the same time.
+ if (++countE < 1) {
+ info("Number of received Debugger:FetchedWatchExpressions events: " + countE);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", testE, false);
+ fetchedExpressions = true;
+ executeSoon(continueTest);
+ }, false);
+
+ function continueTest() {
+ if (testContinued || !fetchedVariables || !fetchedExpressions) {
+ return;
+ }
+ testContinued = true;
+
+ // Get the variable reference anew, since the old ones were discarded when
+ // we resumed.
+ var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[aLocalScopeIndex],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ aArg = localNodes[1];
+
+ is(aArg.querySelector(".value").getAttribute("value"), aArgResult,
+ "Should have the right value for 'aArg'.");
+
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+ info("Found the watch expressions scope: " + scope);
+
+ let aExp = scope.get(aVar.querySelector(".name").getAttribute("value"));
+ info("Found the watch expression variable: " + aExp);
+
+ if (aDeletionFlag) {
+ ok(fetchedVariables, "The variables should have been fetched.");
+ ok(fetchedExpressions, "The variables should have been fetched.");
+ is(aExp, undefined, "The watch expression should not have been found.");
+ performCallback(scope);
+ return;
+ }
+
+ is(aExp.target.querySelector(".name").getAttribute("value"), aNewValue.trim(),
+ "Should have the right name for '" + aNewValue + "'.");
+ is(aExp.target.querySelector(".value").getAttribute("value"), aNewResult,
+ "Should have the right value for '" + aNewValue + "'.");
+
+ performCallback(scope);
+ }
+
+ makeChangesAndExitInputMode();
+ });
+
+ function performCallback(scope) {
+ executeSoon(function() {
+ aTest(scope);
+ aCallback(scope);
+ });
+ }
+}
+
+function testExprDeletion(aVar, aTest, aCallback, aArgResult,
+ aLocalScopeIndex = 1, aFinalFlag = null, aRemoveAllFlag = null)
+{
+ let testContinued = false;
+ let fetchedVariables = false;
+ let fetchedExpressions = false;
+
+ let countV = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function testV() {
+ // We expect 2 Debugger:FetchedVariables events, one from the global
+ // object scope and the regular one.
+ if (++countV < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + countV);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", testV, false);
+ fetchedVariables = true;
+ executeSoon(continueTest);
+ }, false);
+
+ let countE = 0;
+ gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function testE() {
+ // We expect only one Debugger:FetchedWatchExpressions event, since all
+ // expressions are evaluated at the same time.
+ if (++countE < 1) {
+ info("Number of received Debugger:FetchedWatchExpressions events: " + countE);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", testE, false);
+ fetchedExpressions = true;
+ executeSoon(continueTest);
+ }, false);
+
+ function continueTest() {
+ if ((testContinued || !fetchedVariables || !fetchedExpressions) && !aFinalFlag) {
+ return;
+ }
+ testContinued = true;
+
+ // Get the variable reference anew, since the old ones were discarded when
+ // we resumed.
+ var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".variables-view-scope")[aLocalScopeIndex],
+ localNodes = localScope.querySelector(".variables-view-element-details").childNodes,
+ aArg = localNodes[1];
+
+ is(aArg.querySelector(".value").getAttribute("value"), aArgResult,
+ "Should have the right value for 'aArg'.");
+
+ let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+ let scope = gVars._currHierarchy.get(label);
+ info("Found the watch expressions scope: " + scope);
+
+ if (aFinalFlag) {
+ ok(fetchedVariables, "The variables should have been fetched.");
+ ok(!fetchedExpressions, "The variables should never have been fetched.");
+ is(scope, undefined, "The watch expressions scope should not have been found.");
+ performCallback(scope);
+ return;
+ }
+
+ let aExp = scope.get(aVar.querySelector(".name").getAttribute("value"));
+ info("Found the watch expression variable: " + aExp);
+
+ is(aExp, undefined, "Should not have found the watch expression after deletion.");
+ performCallback(scope);
+ }
+
+ function performCallback(scope) {
+ executeSoon(function() {
+ aTest(scope);
+ aCallback(scope);
+ });
+ }
+
+ if (aRemoveAllFlag) {
+ gWatch._onCmdRemoveAllExpressions();
+ return;
+ }
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ aVar.querySelector(".variables-view-delete"),
+ gDebugger);
+}
+
+function test1(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 5, "There should be 5 evaluations availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.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.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.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.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.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 test2(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
+ "There should be 5 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 5, "There should be 5 evaluations availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.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.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.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.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.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 test3(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 4,
+ "There should be 4 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 4, "There should be 4 evaluations availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.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.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.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.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 test4(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 3,
+ "There should be 3 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 3, "There should be 3 evaluations availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.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.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.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 test5(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 2,
+ "There should be 2 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 2, "There should be 2 evaluations availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.inputNode.value, "ermahgerd",
+ "The second textbox input value is not the correct one");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd",
+ "The second textbox input value is not the correct one");
+ is(gWatch.getItemAtIndex(1).attachment.inputNode.value, "this",
+ "The third textbox input value is not the correct one");
+ is(gWatch.getItemAtIndex(1).attachment.currentExpression, "this",
+ "The third textbox input value is not the correct one");
+}
+
+function test6(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 1,
+ "There should be 1 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ ok(scope, "There should be a wach expressions scope in the variables view");
+ is(scope._store.size, 1, "There should be 1 evaluation availalble");
+
+ is(gWatch.getItemAtIndex(0).attachment.inputNode.value, "ermahgerd",
+ "The third textbox input value is not the correct one");
+ is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd",
+ "The third textbox input value is not the correct one");
+}
+
+function test7(scope) {
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+ "There should be 0 hidden nodes in the watch expressions container");
+ is(gWatch.widget._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+ "There should be 0 visible nodes in the watch expressions container");
+
+ is(scope, undefined, "There should be no watch expressions scope available.");
+ is(gWatch.itemCount, 0, "The watch expressions container should be empty.");
+}
+
+function addWatchExpression(string) {
+ gWatch.addExpression(string);
+ gDebugger.editor.focus();
+}
+
+function addCmdWatchExpression(string) {
+ gWatch._onCmdAddExpression(string);
+ gDebugger.editor.focus();
+}
+
+function resumeAndFinish() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gWatch = null;
+ gVars = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
new file mode 100644
index 000000000..1763916d3
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
@@ -0,0 +1,516 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes by name.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ testSearchbox();
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testSearchbox()
+{
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should not initially be a searchbox available in the variables view.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gDebugger.DebuggerView.Variables._enableSearch();
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available after enabling.");
+ ok(gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+
+ gDebugger.DebuggerView.Variables._disableSearch();
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gDebugger.DebuggerView.Variables._enableSearch();
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available after enabling.");
+ ok(gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+
+ let placeholder = "freshly squeezed mango juice";
+
+ gDebugger.DebuggerView.Variables.searchPlaceholder = placeholder;
+ is(gDebugger.DebuggerView.Variables.searchPlaceholder, placeholder,
+ "The placeholder getter didn't return the expected string");
+
+ ok(gDebugger.DebuggerView.Variables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox.");
+
+
+ gDebugger.DebuggerView.Variables._disableSearch();
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling again.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gDebugger.DebuggerView.Variables._enableSearch();
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available after enabling again.");
+ ok(gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ ok(gDebugger.DebuggerView.Variables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox again.");
+
+
+ gDebugger.DebuggerView.Variables.searchEnabled = false;
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There shouldn't be a searchbox available after disabling again.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gDebugger.DebuggerView.Variables.searchEnabled = true;
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available after enabling again.");
+ ok(gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+
+ ok(gDebugger.DebuggerView.Variables._searchboxNode.getAttribute("placeholder"),
+ placeholder, "There correct placeholder should be applied to the searchbox again.");
+}
+
+function testVariablesFiltering()
+{
+ ok(!gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should not be hidden at this point.");
+
+ function test1()
+ {
+ write("location");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ ignoreExtraMatchedProperties();
+ locationItem.toggle();
+ locationItem.toggle();
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
+ "There should be 3 variables displayed in the global scope");
+
+ ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 6,
+ "There should be more than 6 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "this", "The only inner variable displayed should be 'this'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "window", "The third inner property displayed should be 'window'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
+ "document", "The fourth inner property displayed should be 'document'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
+ "location", "The fifth inner property displayed should be 'location'");
+
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "location", "The first global variable displayed should be 'location'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "locationbar", "The second global variable displayed should be 'locationbar'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "Location", "The third global variable displayed should be 'Location'");
+ }
+
+ function test2()
+ {
+ innerScopeItem.collapse();
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+ thisItem.collapse();
+ windowItem.collapse();
+ documentItem.collapse();
+ locationItem.collapse();
+
+ is(innerScopeItem.expanded, false,
+ "The innerScope expanded getter should return false");
+ is(mathScopeItem.expanded, false,
+ "The mathScope expanded getter should return false");
+ is(testScopeItem.expanded, false,
+ "The testScope expanded getter should return false");
+ is(loadScopeItem.expanded, false,
+ "The loadScope expanded getter should return false");
+ is(globalScopeItem.expanded, false,
+ "The globalScope expanded getter should return false");
+
+ is(thisItem.expanded, false,
+ "The local scope 'this' should not be expanded");
+ is(windowItem.expanded, false,
+ "The local scope 'this.window' should not be expanded");
+ is(documentItem.expanded, false,
+ "The local scope 'this.window.document' should not be expanded");
+ is(locationItem.expanded, false,
+ "The local scope 'this.window.document.location' should not be expanded");
+
+ write("location");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ ignoreExtraMatchedProperties();
+ locationItem.toggle();
+ locationItem.toggle();
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
+ "There should be 3 variables displayed in the global scope");
+
+ ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 6,
+ "There should be more than 6 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "this", "The only inner variable displayed should be 'this'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "window", "The third inner property displayed should be 'window'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
+ "document", "The fourth inner property displayed should be 'document'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
+ "location", "The fifth inner property displayed should be 'location'");
+
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "location", "The first global variable displayed should be 'location'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "locationbar", "The second global variable displayed should be 'locationbar'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "Location", "The second global variable displayed should be 'Location'");
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ let thisItem = innerScopeItem.get("this");
+ let windowItem = thisItem.get("window");
+ let documentItem = windowItem.get("document");
+ let locationItem = documentItem.get("location");
+
+ gSearchBox = gDebugger.DebuggerView.Variables._searchboxNode;
+
+ executeSoon(function() {
+ test1();
+ executeSoon(function() {
+ test2();
+ executeSoon(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 4 Debugger:FetchedVariables events, one from the global object
+ // scope, two from the |with| scopes and the regular one.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, false,
+ "The mathScope expanded getter should return false");
+ is(testScopeItem.expanded, false,
+ "The testScope expanded getter should return false");
+ is(loadScopeItem.expanded, false,
+ "The loadScope expanded getter should return false");
+ is(globalScopeItem.expanded, false,
+ "The globalScope expanded getter should return false");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+
+ let thisItem = innerScopeItem.get("this");
+ is(thisItem.expanded, false,
+ "The local scope 'this' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test2() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test2, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let windowItem = thisItem.get("window");
+ is(windowItem.expanded, false,
+ "The local scope 'this.window' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test3() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test3, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let documentItem = windowItem.get("document");
+ is(documentItem.expanded, false,
+ "The local scope 'this.window.document' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test4() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test4, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let locationItem = documentItem.get("location");
+ is(locationItem.expanded, false,
+ "The local scope 'this.window.document.location' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test5() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test5, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ locationItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ documentItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ windowItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ thisItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function ignoreExtraMatchedProperties()
+{
+ for (let [, item] of gDebugger.DebuggerView.Variables._currHierarchy) {
+ let name = item.name.toLowerCase();
+ let value = item._valueString || "";
+
+ if ((name.contains("tracemallocdumpallocations")) ||
+ (name.contains("geolocation")) ||
+ (name.contains("webgl"))) {
+ item.target.setAttribute("non-match", "");
+ }
+ }
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
new file mode 100644
index 000000000..ec5f8c07e
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
@@ -0,0 +1,438 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes by value.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ testSearchbox();
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testSearchbox()
+{
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should not initially be a searchbox available in the variables view.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should not be found.");
+
+ gDebugger.DebuggerView.Variables._enableSearch();
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available after enabling.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "The searchbox element should be found.");
+ ok(gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should be hidden at this point.");
+}
+
+function testVariablesFiltering()
+{
+ ok(!gDebugger.DebuggerView.Variables._searchboxContainer.hidden,
+ "The searchbox container should not be hidden at this point.");
+
+ function test1()
+ {
+ write("htmldocument");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ locationItem.toggle();
+ locationItem.toggle();
+ documentItem.toggle();
+ documentItem.toggle();
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 2,
+ "There should be 2 variables displayed in the global scope");
+
+ ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 3,
+ "There should be more than 3 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "this", "The only inner variable displayed should be 'this'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "document", "The first inner property displayed should be 'document'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "window", "The second inner property displayed should be 'window'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "document", "The third inner property displayed should be 'document'");
+
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "document", "The first global variable displayed should be 'document'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "HTMLDocument", "The first global variable displayed should be 'HTMLDocument'");
+ }
+
+ function test2()
+ {
+ innerScopeItem.collapse();
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+ thisItem.collapse();
+ windowItem.collapse();
+ documentItem.collapse();
+ locationItem.collapse();
+
+ is(innerScopeItem.expanded, false,
+ "The innerScope expanded getter should return false");
+ is(mathScopeItem.expanded, false,
+ "The mathScope expanded getter should return false");
+ is(testScopeItem.expanded, false,
+ "The testScope expanded getter should return false");
+ is(loadScopeItem.expanded, false,
+ "The loadScope expanded getter should return false");
+ is(globalScopeItem.expanded, false,
+ "The globalScope expanded getter should return false");
+
+ is(thisItem.expanded, false,
+ "The local scope 'this' should not be expanded");
+ is(windowItem.expanded, false,
+ "The local scope 'this.window' should not be expanded");
+ is(documentItem.expanded, false,
+ "The local scope 'this.window.document' should not be expanded");
+ is(locationItem.expanded, false,
+ "The local scope 'this.window.document.location' should not be expanded");
+
+ write("htmldocument");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ documentItem.toggle();
+ documentItem.toggle();
+ locationItem.toggle();
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 2,
+ "There should be 2 variables displayed in the global scope");
+
+ ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 3,
+ "There should be more than 3 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+ is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "this", "The only inner variable displayed should be 'this'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "document", "The first inner property displayed should be 'document'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "window", "The second inner property displayed should be 'window'");
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
+ "document", "The third inner property displayed should be 'document'");
+
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "document", "The first global variable displayed should be 'document'");
+ is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+ "HTMLDocument", "The first global variable displayed should be 'HTMLDocument'");
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ let thisItem = innerScopeItem.get("this");
+ let windowItem = thisItem.get("window");
+ let documentItem = windowItem.get("document");
+ let locationItem = documentItem.get("location");
+
+ gSearchBox = gDebugger.DebuggerView.Variables._searchboxNode;
+
+ executeSoon(function() {
+ test1();
+ executeSoon(function() {
+ test2();
+ executeSoon(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 4 Debugger:FetchedVariables events, one from the global object
+ // scope, two from the |with| scopes and the regular one.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, false,
+ "The mathScope expanded getter should return false");
+ is(testScopeItem.expanded, false,
+ "The testScope expanded getter should return false");
+ is(loadScopeItem.expanded, false,
+ "The loadScope expanded getter should return false");
+ is(globalScopeItem.expanded, false,
+ "The globalScope expanded getter should return false");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+
+ let thisItem = innerScopeItem.get("this");
+ is(thisItem.expanded, false,
+ "The local scope 'this' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test2() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test2, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let windowItem = thisItem.get("window");
+ is(windowItem.expanded, false,
+ "The local scope 'this.window' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test3() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test3, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let documentItem = windowItem.get("document");
+ is(documentItem.expanded, false,
+ "The local scope 'this.window.document' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test4() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test4, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let locationItem = documentItem.get("location");
+ is(locationItem.expanded, false,
+ "The local scope 'this.window.document.location' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test5() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test5, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded");
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ locationItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ documentItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ windowItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ thisItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-03.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-03.js
new file mode 100644
index 000000000..7f92b9325
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-03.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/ */
+
+/**
+ * Make sure that the property view filter prefs work properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gPrevPref = null;
+
+function test()
+{
+ gPrevPref = Services.prefs.getBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible");
+ Services.prefs.setBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible", false);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ testSearchbox();
+ testPref();
+ });
+}
+
+function testSearchbox()
+{
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should not initially be a searchbox available in the variables view.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should not be found.");
+}
+
+function testPref()
+{
+ is(gDebugger.Prefs.variablesSearchboxVisible, false,
+ "The debugger searchbox should be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should not be checked.");
+
+ gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.setAttribute("checked", "true");
+ gDebugger.DebuggerView.Options._toggleShowVariablesFilterBox();
+
+ executeSoon(function() {
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available in the variables view.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should be found.");
+ is(gDebugger.Prefs.variablesSearchboxVisible, true,
+ "The debugger searchbox should now be preffed as visible.");
+ is(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be checked.");
+
+ gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._toggleShowVariablesFilterBox();
+
+ executeSoon(function() {
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should not be a searchbox available in the variables view.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should not be found.");
+ is(gDebugger.Prefs.variablesSearchboxVisible, false,
+ "The debugger searchbox should now be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be unchecked.");
+
+ executeSoon(function() {
+ Services.prefs.setBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible", gPrevPref);
+
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gPrevPref = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-04.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-04.js
new file mode 100644
index 000000000..89663783b
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-04.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/ */
+
+/**
+ * Make sure that the property view filter prefs work properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gPrevPref = null;
+
+function test()
+{
+ gPrevPref = Services.prefs.getBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible");
+ Services.prefs.setBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible", true);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ testSearchbox();
+ testPref();
+ });
+}
+
+function testSearchbox()
+{
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should initially be a searchbox available in the variables view.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should be found.");
+}
+
+function testPref()
+{
+ is(gDebugger.Prefs.variablesSearchboxVisible, true,
+ "The debugger searchbox should be preffed as visible.");
+ is(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should be checked.");
+
+ gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.setAttribute("checked", "false");
+ gDebugger.DebuggerView.Options._toggleShowVariablesFilterBox();
+
+ executeSoon(function() {
+ ok(!gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should not be a searchbox available in the variables view.");
+ ok(!gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should not be found.");
+ is(gDebugger.Prefs.variablesSearchboxVisible, false,
+ "The debugger searchbox should now be preffed as hidden.");
+ isnot(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be unchecked.");
+
+ gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.setAttribute("checked", "true");
+ gDebugger.DebuggerView.Options._toggleShowVariablesFilterBox();
+
+ executeSoon(function() {
+ ok(gDebugger.DebuggerView.Variables._searchboxNode,
+ "There should be a searchbox available in the variables view.");
+ ok(gDebugger.DebuggerView.Variables._parent.parentNode.querySelector(".variables-view-searchinput"),
+ "There searchbox element should be found.");
+ is(gDebugger.Prefs.variablesSearchboxVisible, true,
+ "The debugger searchbox should now be preffed as visible.");
+ is(gDebugger.DebuggerView.Options._showVariablesFilterBoxItem.getAttribute("checked"), "true",
+ "The options menu item should now be checked.");
+
+ executeSoon(function() {
+ Services.prefs.setBoolPref(
+ "devtools.debugger.ui.variables-searchbox-visible", gPrevPref);
+
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gPrevPref = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
new file mode 100644
index 000000000..af3c4b85d
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
@@ -0,0 +1,284 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testVariablesFiltering()
+{
+ function test1()
+ {
+ write("*one");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the load scope");
+
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "one", "The only inner variable displayed should be 'one'");
+
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "button", "The only load variable displayed should be 'button'");
+
+ let oneItem = innerScopeItem.get("one");
+ is(oneItem.expanded, false,
+ "The one item in the inner scope should not be expanded");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(oneItem.expanded, true,
+ "The one item in the inner scope should now be expanded");
+ }
+
+ function test2()
+ {
+ write("*two");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
+ "There should be 1 variable displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be 0 variables displayed in the load scope");
+
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the load scope");
+
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
+ "two", "The only inner variable displayed should be 'two'");
+
+ let twoItem = innerScopeItem.get("two");
+ is(twoItem.expanded, false,
+ "The two item in the inner scope should not be expanded");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ is(twoItem.expanded, true,
+ "The two item in the inner scope should now be expanded");
+ }
+
+ function test3()
+ {
+ backspace(3);
+
+ is(gSearchBox.value, "*",
+ "Searchbox value is incorrect after 3 backspaces");
+
+ // variable count includes `__proto__` for object scopes
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+ "There should be 4 variables displayed in the inner scope");
+ isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the math scope");
+ isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the test scope");
+ isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the load scope");
+ isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ ok(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length > 1,
+ "There should be more than one property displayed in the load scope");
+ isnot(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be some properties displayed in the global scope");
+ }
+
+ function test4()
+ {
+ backspace(1);
+
+ is(gSearchBox.value, "",
+ "Searchbox value is incorrect after 1 backspace");
+
+ // variable count includes `__proto__` for object scopes
+ is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+ "There should be 4 variables displayed in the inner scope");
+ isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the math scope");
+ isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the test scope");
+ isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the load scope");
+ isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
+ "There should be some variables displayed in the global scope");
+
+ is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the inner scope");
+ is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the math scope");
+ is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be 0 properties displayed in the test scope");
+ ok(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length > 1,
+ "There should be more than one properties displayed in the load scope");
+ isnot(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "There should be some properties displayed in the global scope");
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ executeSoon(function() {
+ test1();
+ executeSoon(function() {
+ test2();
+ executeSoon(function() {
+ test3();
+ executeSoon(function() {
+ test4();
+ executeSoon(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+ });
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 4 Debugger:FetchedVariables events, one from the global object
+ // scope, two from the |with| scopes and the regular one.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger)
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js
new file mode 100644
index 000000000..962f6542a
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js
@@ -0,0 +1,249 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = false;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testVariablesFiltering()
+{
+ let f = {
+ test1: function()
+ {
+ assertExpansion(1, [true, false, false, false, false]);
+ clear();
+ },
+ test2: function()
+ {
+ assertExpansion(2, [true, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test3: function()
+ {
+ assertExpansion(3, [true, false, false, false, false]);
+ gDebugger.editor.focus();
+ },
+ test4: function()
+ {
+ assertExpansion(4, [true, false, false, false, false]);
+ write("*");
+ },
+ test5: function() {
+ assertExpansion(5, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test6: function() {
+ assertExpansion(6, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test7: function() {
+ assertExpansion(7, [true, true, true, true, true]);
+ backspace(1);
+ },
+ test8: function() {
+ assertExpansion(8, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test9: function() {
+ assertExpansion(9, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test10: function() {
+ assertExpansion(10, [true, true, true, true, true]);
+ innerScopeItem.collapse();
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+ },
+ test11: function() {
+ assertExpansion(11, [false, false, false, false, false]);
+ clear();
+ },
+ test12: function() {
+ assertExpansion(12, [false, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test13: function() {
+ assertExpansion(13, [false, false, false, false, false]);
+ gDebugger.editor.focus();
+ },
+ test14: function() {
+ assertExpansion(14, [false, false, false, false, false]);
+ write("*");
+ },
+ test15: function() {
+ assertExpansion(15, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test16: function() {
+ assertExpansion(16, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test17: function() {
+ assertExpansion(17, [true, true, true, true, true]);
+ backspace(1);
+ },
+ test18: function() {
+ assertExpansion(18, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test19: function() {
+ assertExpansion(19, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test20: function() {
+ assertExpansion(20, [true, true, true, true, true]);
+ }
+ };
+
+ function assertExpansion(n, array) {
+ is(innerScopeItem.expanded, array[0],
+ "The innerScope should " + (array[0] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(mathScopeItem.expanded, array[1],
+ "The mathScope should " + (array[1] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(testScopeItem.expanded, array[2],
+ "The testScope should " + (array[2] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(loadScopeItem.expanded, array[3],
+ "The loadScope should " + (array[3] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(globalScopeItem.expanded, array[4],
+ "The globalScope should " + (array[4] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ executeSoon(function() {
+ for (let i = 1; i <= Object.keys(f).length; i++) {
+ f["test" + i]();
+ }
+ closeDebuggerAndFinish();
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the inner object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger)
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js
new file mode 100644
index 000000000..d18e0bbb2
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js
@@ -0,0 +1,254 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testVariablesFiltering()
+{
+ let f = {
+ test1: function()
+ {
+ assertExpansion(1, [true, false, false, false, false]);
+ clear();
+ },
+ test2: function()
+ {
+ assertExpansion(2, [true, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test3: function()
+ {
+ assertExpansion(3, [true, false, false, false, false]);
+ gDebugger.editor.focus();
+ },
+ test4: function()
+ {
+ assertExpansion(4, [true, false, false, false, false]);
+ write("*");
+ },
+ test5: function() {
+ assertExpansion(5, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test6: function() {
+ assertExpansion(6, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test7: function() {
+ assertExpansion(7, [true, true, true, true, true]);
+ backspace(1);
+ },
+ test8: function() {
+ assertExpansion(8, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test9: function() {
+ assertExpansion(9, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test10: function() {
+ assertExpansion(10, [true, true, true, true, true]);
+ innerScopeItem.collapse();
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+ },
+ test11: function() {
+ assertExpansion(11, [false, false, false, false, false]);
+ clear();
+ },
+ test12: function() {
+ assertExpansion(12, [false, false, false, false, false]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test13: function() {
+ assertExpansion(13, [false, false, false, false, false]);
+ gDebugger.editor.focus();
+ },
+ test14: function() {
+ assertExpansion(14, [false, false, false, false, false]);
+ write("*");
+ },
+ test15: function() {
+ assertExpansion(15, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test16: function() {
+ assertExpansion(16, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test17: function() {
+ assertExpansion(17, [true, true, true, true, true]);
+ backspace(1);
+ },
+ test18: function() {
+ assertExpansion(18, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ },
+ test19: function() {
+ assertExpansion(19, [true, true, true, true, true]);
+ gDebugger.editor.focus();
+ },
+ test20: function() {
+ assertExpansion(20, [true, true, true, true, true]);
+ }
+ };
+
+ function assertExpansion(n, array) {
+ is(innerScopeItem.expanded, array[0],
+ "The innerScope should " + (array[0] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(mathScopeItem.expanded, array[1],
+ "The mathScope should " + (array[1] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(testScopeItem.expanded, array[2],
+ "The testScope should " + (array[2] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(loadScopeItem.expanded, array[3],
+ "The loadScope should " + (array[3] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(globalScopeItem.expanded, array[4],
+ "The globalScope should " + (array[4] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ executeSoon(function() {
+ for (let i = 1; i <= Object.keys(f).length; i++) {
+ f["test" + i]();
+ }
+ closeDebuggerAndFinish();
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the inner object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js
new file mode 100644
index 000000000..7668512fd
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js
@@ -0,0 +1,324 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly filters nodes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+var gSearchBox = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.delayedSearch = false;
+ prepareVariables(testVariablesFiltering);
+ });
+}
+
+function testVariablesFiltering()
+{
+ let f = {
+ test1: function(aCallback)
+ {
+ assertExpansion(1, [true, false, false, false, false]);
+ write("*arguments");
+ aCallback();
+ },
+ test2: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, false,
+ "The arguments pseudoarray in the testScope should not be expanded");
+ is(loadScopeItem.get("arguments").expanded, false,
+ "The arguments pseudoarray in the testScope should not be expanded");
+
+ assertExpansion(1, [true, true, true, true, true]);
+ EventUtils.sendKey("RETURN", gDebugger);
+ aCallback();
+ },
+ test3: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(2, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the testScope should have 4 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the loadScope should have 4 visible properties");
+
+ assertExpansion(2, [true, true, true, true, true]);
+ backspace(1);
+ aCallback();
+ });
+ },
+ test4: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(0, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the testScope should have 4 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the loadScope should have 4 visible properties");
+
+ assertExpansion(3, [true, true, true, true, true]);
+ backspace(8);
+ aCallback();
+ });
+ },
+ test5: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(0, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the testScope should have 4 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the loadScope should have 4 visible properties");
+
+ assertExpansion(4, [true, true, true, true, true]);
+ backspace(1);
+ aCallback();
+ });
+ },
+ test6: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(0, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the testScope should have 4 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the loadScope should have 4 visible properties");
+
+ assertExpansion(5, [true, true, true, true, true]);
+ write("*");
+ aCallback();
+ });
+ },
+ test7: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(0, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the testScope should have 4 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 4,
+ "The arguments in the loadScope should have 4 visible properties");
+
+ assertExpansion(5, [true, true, true, true, true]);
+ append("arguments");
+ aCallback();
+ });
+ },
+ test8: function(aCallback)
+ {
+ is(testScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+ is(loadScopeItem.get("arguments").expanded, true,
+ "The arguments pseudoarray in the testScope should now be expanded");
+
+ waitForFetchedProperties(0, function() {
+ is(testScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "The arguments in the testScope should have 0 visible properties");
+ is(loadScopeItem.get("arguments").target.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
+ "The arguments in the loadScope should have 0 visible properties");
+
+ assertExpansion(5, [true, true, true, true, true]);
+ aCallback();
+ });
+ },
+ };
+
+ function assertExpansion(n, array) {
+ is(innerScopeItem.expanded, array[0],
+ "The innerScope should " + (array[0] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(mathScopeItem.expanded, array[1],
+ "The mathScope should " + (array[1] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(testScopeItem.expanded, array[2],
+ "The testScope should " + (array[2] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(loadScopeItem.expanded, array[3],
+ "The loadScope should " + (array[3] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+
+ is(globalScopeItem.expanded, array[4],
+ "The globalScope should " + (array[4] ? "" : "not ") +
+ "be expanded at this point (" + n + ")");
+ }
+
+ function waitForFetchedProperties(n, aCallback) {
+ if (n == 0) {
+ aCallback();
+ return;
+ }
+
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test() {
+ // We expect n Debugger:FetchedProperties events.
+ if (++count < n) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+ executeSoon(aCallback);
+ }}, 0);
+ }, false);
+ }
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ executeSoon(function() {
+ f.test1(function() {
+ f.test2(function() {
+ f.test3(function() {
+ f.test4(function() {
+ f.test5(function() {
+ f.test6(function() {
+ f.test7(function() {
+ f.test8(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
+
+function prepareVariables(aCallback)
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 2 Debugger:FetchedVariables events, one from the inner object
+ // scope and the regular one.
+ if (++count < 2) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+ executeSoon(function() {
+ aCallback();
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-reexpand.js b/browser/devtools/debugger/test/browser_dbg_propertyview-reexpand.js
new file mode 100644
index 000000000..7cb9e10d4
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-reexpand.js
@@ -0,0 +1,394 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view correctly re-expands nodes after pauses.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gDebuggee = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebuggee = aDebuggee;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown() {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+ addBreakpoint();
+ });
+ });
+}
+
+function addBreakpoint()
+{
+ gDebugger.DebuggerController.Breakpoints.addBreakpoint({
+ url: gDebugger.DebuggerView.Sources.selectedValue,
+ line: 16
+ }, function(aBreakpointClient, aResponseError) {
+ ok(!aResponseError, "There shouldn't be an error.");
+ // Wait for the resume...
+ gDebugger.gClient.addOneTimeListener("resumed", function() {
+ gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
+ gDebugger.DebuggerView.Variables.nonEnumVisible = false;
+ gDebugger.DebuggerView.Variables.commitHierarchyIgnoredItems = Object.create(null);
+ testVariablesExpand();
+ });
+ });
+}
+
+function testVariablesExpand()
+{
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+ // We expect 4 Debugger:FetchedVariables events, one from the global object
+ // scope, two from the |with| scopes and the regular one.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedVariables events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var frames = gDebugger.DebuggerView.StackFrames.widget._list,
+ scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
+ mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
+ testScope = scopes.querySelectorAll(".variables-view-scope")[2],
+ loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
+ globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
+
+ let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ innerScope.querySelector(".name").getAttribute("value"));
+ let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ mathScope.querySelector(".name").getAttribute("value"));
+ let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ testScope.querySelector(".name").getAttribute("value"));
+ let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ loadScope.querySelector(".name").getAttribute("value"));
+ let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
+ globalScope.querySelector(".name").getAttribute("value"));
+
+ is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The innerScope arrow should initially be expanded");
+ is(mathScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The mathScope arrow should initially be expanded");
+ is(testScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The testScope arrow should initially be expanded");
+ is(loadScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The loadScope arrow should initially be expanded");
+ is(globalScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalScope arrow should initially be expanded");
+
+ is(innerScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The innerScope enumerables should initially be expanded");
+ is(mathScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The mathScope enumerables should initially be expanded");
+ is(testScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The testScope enumerables should initially be expanded");
+ is(loadScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The loadScope enumerables should initially be expanded");
+ is(globalScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalScope enumerables should initially be expanded");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ mathScopeItem.collapse();
+ testScopeItem.collapse();
+ loadScopeItem.collapse();
+ globalScopeItem.collapse();
+
+ is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The innerScope arrow should initially be expanded");
+ is(mathScope.querySelector(".arrow").hasAttribute("open"), false,
+ "The mathScope arrow should initially not be expanded");
+ is(testScope.querySelector(".arrow").hasAttribute("open"), false,
+ "The testScope arrow should initially not be expanded");
+ is(loadScope.querySelector(".arrow").hasAttribute("open"), false,
+ "The loadScope arrow should initially not be expanded");
+ is(globalScope.querySelector(".arrow").hasAttribute("open"), false,
+ "The globalScope arrow should initially not be expanded");
+
+ is(innerScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The innerScope enumerables should initially be expanded");
+ is(mathScope.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The mathScope enumerables should initially not be expanded");
+ is(testScope.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The testScope enumerables should initially not be expanded");
+ is(loadScope.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The loadScope enumerables should initially not be expanded");
+ is(globalScope.querySelector(".variables-view-element-details").hasAttribute("open"), false,
+ "The globalScope enumerables should initially not be expanded");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, false,
+ "The mathScope expanded getter should return false");
+ is(testScopeItem.expanded, false,
+ "The testScope expanded getter should return false");
+ is(loadScopeItem.expanded, false,
+ "The loadScope expanded getter should return false");
+ is(globalScopeItem.expanded, false,
+ "The globalScope expanded getter should return false");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebugger);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebugger);
+
+
+ is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The innerScope arrow should now be expanded");
+ is(mathScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The mathScope arrow should now be expanded");
+ is(testScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The testScope arrow should now be expanded");
+ is(loadScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The loadScope arrow should now be expanded");
+ is(globalScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalScope arrow should now be expanded");
+
+ is(innerScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The innerScope enumerables should now be expanded");
+ is(mathScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The mathScope enumerables should now be expanded");
+ is(testScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The testScope enumerables should now be expanded");
+ is(loadScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The loadScope enumerables should now be expanded");
+ is(globalScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalScope enumerables should now be expanded");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+
+ let thisItem = innerScopeItem.get("this");
+ is(thisItem.expanded, false,
+ "The local scope 'this' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test2() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test2, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let windowItem = thisItem.get("window");
+ is(windowItem.expanded, false,
+ "The local scope 'this.window' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test3() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test3, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let documentItem = windowItem.get("document");
+ is(documentItem.expanded, false,
+ "The local scope 'this.window.document' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test4() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test4, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let locationItem = documentItem.get("location");
+ is(locationItem.expanded, false,
+ "The local scope 'this.window.document.location' should not be expanded yet");
+
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test5() {
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test5, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(thisItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The thisItem arrow should still be expanded (1)");
+ is(windowItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The windowItem arrow should still be expanded (1)");
+ is(documentItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The documentItem arrow should still be expanded (1)");
+ is(locationItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The locationItem arrow should still be expanded (1)");
+
+ is(thisItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The thisItem enumerables should still be expanded (1)");
+ is(windowItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The windowItem enumerables should still be expanded (1)");
+ is(documentItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The documentItem enumerables should still be expanded (1)");
+ is(locationItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The locationItem enumerables should still be expanded (1)");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should still be expanded (1)");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should still be expanded (1)");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should still be expanded (1)");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should still be expanded (1)");
+
+
+ let count = 0;
+ gDebugger.addEventListener("Debugger:FetchedProperties", function test6() {
+ // We expect 4 Debugger:FetchedProperties events, one from the this
+ // reference, one for window, one for document and one for location.
+ if (++count < 4) {
+ info("Number of received Debugger:FetchedProperties events: " + count);
+ return;
+ }
+ gDebugger.removeEventListener("Debugger:FetchedProperties", test6, false);
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The innerScope arrow should still be expanded");
+ is(mathScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The mathScope arrow should still be expanded");
+ is(testScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The testScope arrow should still be expanded");
+ is(loadScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The loadScope arrow should still be expanded");
+ is(globalScope.querySelector(".arrow").hasAttribute("open"), true,
+ "The globalScope arrow should still be expanded");
+
+ is(innerScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The innerScope enumerables should still be expanded");
+ is(mathScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The mathScope enumerables should still be expanded");
+ is(testScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The testScope enumerables should still be expanded");
+ is(loadScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The loadScope enumerables should still be expanded");
+ is(globalScope.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The globalScope enumerables should still be expanded");
+
+ is(innerScopeItem.expanded, true,
+ "The innerScope expanded getter should return true");
+ is(mathScopeItem.expanded, true,
+ "The mathScope expanded getter should return true");
+ is(testScopeItem.expanded, true,
+ "The testScope expanded getter should return true");
+ is(loadScopeItem.expanded, true,
+ "The loadScope expanded getter should return true");
+ is(globalScopeItem.expanded, true,
+ "The globalScope expanded getter should return true");
+
+ is(thisItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The thisItem arrow should still be expanded (2)");
+ is(windowItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The windowItem arrow should still be expanded (2)");
+ is(documentItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The documentItem arrow should still be expanded (2)");
+ is(locationItem.target.querySelector(".arrow").hasAttribute("open"), true,
+ "The locationItem arrow should still be expanded (2)");
+
+ is(thisItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The thisItem enumerables should still be expanded (2)");
+ is(windowItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The windowItem enumerables should still be expanded (2)");
+ is(documentItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The documentItem enumerables should still be expanded (2)");
+ is(locationItem.target.querySelector(".variables-view-element-details").hasAttribute("open"), true,
+ "The locationItem enumerables should still be expanded (2)");
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should still be expanded (2)");
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should still be expanded (2)");
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should still be expanded (2)");
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should still be expanded (2)");
+
+ executeSoon(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.querySelector("#step-in"),
+ gDebugger);
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ locationItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(locationItem.expanded, true,
+ "The local scope 'this.window.document.location' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ documentItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(documentItem.expanded, true,
+ "The local scope 'this.window.document' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ windowItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(windowItem.expanded, true,
+ "The local scope 'this.window' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ executeSoon(function() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ thisItem.target.querySelector(".arrow"),
+ gDebugger);
+
+ is(thisItem.expanded, true,
+ "The local scope 'this' should be expanded now");
+ });
+ }}, 0);
+ }, false);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gDebuggee.document.querySelector("button"),
+ gDebuggee.window);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+ gDebuggee = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js b/browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js
new file mode 100644
index 000000000..47846e46b
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the preferred script is shown when a page is loaded.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gView = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ let expectedScript = "test-script-switching-02.js";
+ let expectedScriptShown = false;
+ let scriptShownUrl = null;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gView = gDebugger.DebuggerView;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gView.Sources.preferredSource = EXAMPLE_URL + expectedScript;
+ startTest();
+ });
+
+ function onScriptShown(aEvent)
+ {
+ expectedScriptShown = aEvent.detail.url.indexOf(expectedScript) != -1;
+ scriptShownUrl = aEvent.detail.url;
+ startTest();
+ }
+
+ function startTest()
+ {
+ if (expectedScriptShown && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function performTest()
+ {
+ info("Currently preferred script: " + gView.Sources.preferredValue);
+ info("Currently selected script: " + gView.Sources.selectedValue);
+
+ isnot(gView.Sources.preferredValue.indexOf(expectedScript), -1,
+ "The preferred script url wasn't set correctly.");
+ isnot(gView.Sources.selectedValue.indexOf(expectedScript), -1,
+ "The selected script isn't the correct one.");
+ is(gView.Sources.selectedValue, scriptShownUrl,
+ "The shown script is not the the correct one.");
+
+ closeDebuggerAndFinish();
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gView = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_reload-same-script.js b/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
new file mode 100644
index 000000000..bee00530f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the same script is shown after a page is reloaded.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gView = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ let step = 0;
+ let expectedScript = "";
+ let expectedScriptShown = false;
+ let scriptShownUrl = null;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gView = gDebugger.DebuggerView;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ startTest();
+ });
+
+ function onScriptShown(aEvent)
+ {
+ expectedScriptShown = aEvent.detail.url.indexOf("-01.js") != -1;
+ scriptShownUrl = aEvent.detail.url;
+ startTest();
+ }
+
+ function onUlteriorScriptShown(aEvent)
+ {
+ ok(expectedScript,
+ "The expected script to show up should have been specified.");
+
+ info("The expected script for this ScriptShown event is: " + expectedScript);
+ info("The current script for this ScriptShown event is: " + aEvent.detail.url);
+
+ expectedScriptShown = aEvent.detail.url.indexOf(expectedScript) != -1;
+ scriptShownUrl = aEvent.detail.url;
+ testScriptShown();
+ }
+
+ function startTest()
+ {
+ if (expectedScriptShown && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ gDebugger.addEventListener("Debugger:SourceShown", onUlteriorScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: performTest }, 0);
+ }
+ }
+
+ function finishTest()
+ {
+ if (expectedScriptShown && resumed && testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onUlteriorScriptShown);
+ closeDebuggerAndFinish();
+ }
+ }
+
+ function performTest()
+ {
+ testCurrentScript("-01.js", step);
+ expectedScript = "-01.js";
+ performAction(reloadPage);
+ }
+
+ function testScriptShown()
+ {
+ if (!expectedScriptShown) {
+ return;
+ }
+ step++;
+
+ if (step === 1) {
+ testCurrentScript("-01.js", step, true);
+ expectedScript = "-01.js";
+ performAction(reloadPage);
+ }
+ else if (step === 2) {
+ testCurrentScript("-01.js", step, true);
+ expectedScript = "-02.js";
+ performAction(switchScript, 1);
+ }
+ else if (step === 3) {
+ testCurrentScript("-02.js", step);
+ expectedScript = "-02.js";
+ performAction(reloadPage);
+ }
+ else if (step === 4) {
+ testCurrentScript("-02.js", step, true);
+ expectedScript = "-01.js";
+ performAction(switchScript, 0);
+ }
+ else if (step === 5) {
+ testCurrentScript("-01.js", step);
+ expectedScript = "-01.js";
+ performAction(reloadPage);
+ }
+ else if (step === 6) {
+ testCurrentScript("-01.js", step, true);
+ expectedScript = "-01.js";
+ performAction(reloadPage);
+ }
+ else if (step === 7) {
+ testCurrentScript("-01.js", step, true);
+ expectedScript = "-01.js";
+ performAction(reloadPage);
+ }
+ else if (step === 8) {
+ testCurrentScript("-01.js", step, true);
+ expectedScript = "-02.js";
+ performAction(switchScript, 1);
+ }
+ else if (step === 9) {
+ testCurrentScript("-02.js", step);
+ expectedScript = "-02.js";
+ performAction(reloadPage);
+ }
+ else if (step === 10) {
+ testCurrentScript("-02.js", step, true);
+ expectedScript = "-02.js";
+ performAction(reloadPage);
+ }
+ else if (step === 11) {
+ testCurrentScript("-02.js", step, true);
+ expectedScript = "-02.js";
+ performAction(reloadPage);
+ }
+ else if (step === 12) {
+ testCurrentScript("-02.js", step, true);
+ expectedScript = "-01.js";
+ performAction(switchScript, 0);
+ }
+ else if (step === 13) {
+ testCurrentScript("-01.js", step);
+ finishTest();
+ }
+ }
+
+ function testCurrentScript(part, step, isAfterReload)
+ {
+ info("Currently preferred script: " + gView.Sources.preferredValue);
+ info("Currently selected script: " + gView.Sources.selectedValue);
+
+ if (step < 1) {
+ is(gView.Sources.preferredValue, null,
+ "The preferred script url should be initially null");
+ }
+ else if (isAfterReload) {
+ isnot(gView.Sources.preferredValue.indexOf(part), -1,
+ "The preferred script url wasn't set correctly. (" + step + ")");
+ }
+
+ isnot(gView.Sources.selectedValue.indexOf(part), -1,
+ "The selected script isn't the correct one. (" + step + ")");
+ is(gView.Sources.selectedValue, scriptShownUrl,
+ "The shown script is not the the correct one. (" + step + ")");
+ }
+
+ function performAction(callback, data)
+ {
+ // Poll every few milliseconds until the scripts are retrieved.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the scripts.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gView.Sources.values.length !== 2) {
+ return;
+ }
+ info("Available scripts: " + gView.Sources.values);
+
+ // We got all the scripts, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback(data);
+ }, 100);
+ }
+
+ function switchScript(index)
+ {
+ gView.Sources.selectedValue = gView.Sources.values[index];
+ }
+
+ function reloadPage()
+ {
+ gDebuggee.location.reload();
+ }
+
+ registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gView = null;
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_script-switching-02.html b/browser/devtools/debugger/test/browser_dbg_script-switching-02.html
new file mode 100644
index 000000000..42d960a15
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_script-switching-02.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Script Switching Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-script-switching-01.js"></script>
+ <script type="text/javascript" src="test-script-switching-02.js?foo=bar,baz|lol"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_script-switching.html b/browser/devtools/debugger/test/browser_dbg_script-switching.html
new file mode 100644
index 000000000..a09789d12
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_script-switching.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Script Switching Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-script-switching-01.js"></script>
+ <script type="text/javascript" src="test-script-switching-02.js"></script>
+ </head>
+ <body>
+ <button onclick="firstCall()">Click me!</button>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
new file mode 100644
index 000000000..a057c5aa4
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
@@ -0,0 +1,304 @@
+/* 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 basic functionality of scripts filtering (token search and line jump).
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchBox = null;
+
+function test()
+{
+ requestLongerTimeout(2);
+
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testScriptSearching();
+ });
+ });
+}
+
+function testScriptSearching() {
+ let noMatchingSources = gDebugger.L10N.getStr("noMatchingSourcesText");
+ let token = "";
+
+ Services.tm.currentThread.dispatch({ run: function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ write(":12");
+ ok(gEditor.getCaretPosition().line == 11 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+
+ EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
+ ok(gEditor.getCaretPosition().line == 12 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after Meta+G");
+
+ EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger);
+ ok(gEditor.getCaretPosition().line == 13 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after Ctrl+N");
+
+ EventUtils.synthesizeKey("G", { metaKey: true, shiftKey: true }, gDebugger);
+ ok(gEditor.getCaretPosition().line == 12 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after Meta+Shift+G");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true }, gDebugger);
+ ok(gEditor.getCaretPosition().line == 11 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after Ctrl+P");
+
+ for (let i = 0; i < 100; i++) {
+ EventUtils.sendKey("DOWN", gDebugger);
+ }
+ ok(gEditor.getCaretPosition().line == 32 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after multiple DOWN keys");
+
+ for (let i = 0; i < 100; i++) {
+ EventUtils.sendKey("UP", gDebugger);
+ }
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line after multiple UP keys");
+
+
+ token = "debugger";
+ write("#" + token);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor didn't jump to the correct token. (1)");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (2)");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 12 &&
+ gEditor.getCaretPosition().col == 8 + token.length,
+ "The editor didn't jump to the correct token. (3)");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 19 &&
+ gEditor.getCaretPosition().col == 4 + token.length,
+ "The editor didn't jump to the correct token. (4)");
+
+ EventUtils.sendKey("ENTER", gDebugger);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor didn't jump to the correct token. (5)");
+
+ EventUtils.sendKey("UP", gDebugger);
+ ok(gEditor.getCaretPosition().line == 19 &&
+ gEditor.getCaretPosition().col == 4 + token.length,
+ "The editor didn't jump to the correct token. (5.1)");
+
+
+ token = "debugger;";
+ write(":bogus#" + token);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (6)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write(":13#" + token);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (7)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write(":#" + token);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (8)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("::#" + token);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (9)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write(":::#" + token);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (10)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+
+ write("#" + token + ":bogus");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (6)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("#" + token + ":13");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (7)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("#" + token + ":");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (8)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("#" + token + "::");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (9)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("#" + token + ":::");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (10)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+
+ write(":i am not a number");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't remain at the correct token. (11)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ write("#__i do not exist__");
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't remain at the correct token. (12)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+
+ token = "debugger";
+ write("#" + token);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor didn't jump to the correct token. (12.1)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ clear();
+ EventUtils.sendKey("RETURN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor shouldn't jump to another token. (12.2)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ EventUtils.sendKey("ENTER", gDebugger);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor shouldn't jump to another token. (12.3)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+
+ write(":1:2:3:a:b:c:::12");
+ ok(gEditor.getCaretPosition().line == 11 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line. (13)");
+
+ write("#don't#find#me#instead#find#" + token);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor didn't jump to the correct token. (14)");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 8 &&
+ gEditor.getCaretPosition().col == 2 + token.length,
+ "The editor didn't jump to the correct token. (15)");
+
+ EventUtils.sendKey("DOWN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 12 &&
+ gEditor.getCaretPosition().col == 8 + token.length,
+ "The editor didn't jump to the correct token. (16)");
+
+ EventUtils.sendKey("RETURN", gDebugger);
+ ok(gEditor.getCaretPosition().line == 19 &&
+ gEditor.getCaretPosition().col == 4 + token.length,
+ "The editor didn't jump to the correct token. (17)");
+
+ EventUtils.sendKey("ENTER", gDebugger);
+ ok(gEditor.getCaretPosition().line == 2 &&
+ gEditor.getCaretPosition().col == 44 + token.length,
+ "The editor didn't jump to the correct token. (18)");
+
+ EventUtils.sendKey("UP", gDebugger);
+ ok(gEditor.getCaretPosition().line == 19 &&
+ gEditor.getCaretPosition().col == 4 + token.length,
+ "The editor didn't jump to the correct token. (18.1)");
+
+
+ clear();
+ ok(gEditor.getCaretPosition().line == 19 &&
+ gEditor.getCaretPosition().col == 4 + token.length,
+ "The editor didn't remain at the correct token. (19)");
+ is(gSources.visibleItems.length, 1,
+ "Not all the scripts are shown after the search. (20)");
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice that matches are found.");
+
+ closeDebuggerAndFinish();
+ }}, 0);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
new file mode 100644
index 000000000..c9b601334
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
@@ -0,0 +1,267 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests basic functionality of scripts filtering (file search).
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ firstSearch();
+ });
+}
+
+function firstSearch() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 4 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line. (1)");
+ is(gSources.visibleItems.length, 1,
+ "Not all the correct scripts are shown after the search. (1)");
+
+ secondSearch();
+ });
+ } else {
+ ok(false, "Get off my lawn.");
+ }
+ });
+ write(".*-01\.js:5");
+}
+
+function secondSearch() {
+ let token = "deb";
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ append("#" + token);
+
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length,
+ "The editor didn't jump to the correct line. (2)");
+ is(gSources.visibleItems.length, 1,
+ "Not all the correct scripts are shown after the search. (2)");
+
+ waitForFirstScript();
+ });
+ } else {
+ ok(false, "Get off my lawn.");
+ }
+ });
+ gSources.selectedIndex = 1;
+}
+
+function waitForFirstScript() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ thirdSearch();
+ });
+ }
+ });
+ gSources.selectedIndex = 0;
+}
+
+function thirdSearch() {
+ let token = "deb";
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length,
+ "The editor didn't jump to the correct line. (3)");
+ is(gSources.visibleItems.length, 1,
+ "Not all the correct scripts are shown after the search. (3)");
+
+ fourthSearch(0, "ugger;", token);
+ });
+ } else {
+ ok(false, "Get off my lawn.");
+ }
+ });
+ write(".*-02\.js#" + token);
+}
+
+function fourthSearch(i, string, token) {
+ info("Searchbox value: " + gSearchBox.value);
+
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length + i,
+ "The editor didn't remain at the correct token. (4)");
+
+ if (string[i]) {
+ EventUtils.sendChar(string[i], gDebugger);
+ fourthSearch(i + 1, string, token);
+ return;
+ }
+
+ clear();
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length + i,
+ "The editor didn't remain at the correct token. (5)");
+
+ executeSoon(function() {
+ let noMatchingSources = gDebugger.L10N.getStr("noMatchingSourcesText");
+
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the searchbox was emptied.");
+ is(gSources.selectedIndex, 1,
+ "The scripts container should have retained its selected index after the searchbox was emptied.");
+
+ write("BOGUS");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length + i,
+ "The editor didn't remain at the correct token. (6)");
+
+ is(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should display a notice that no scripts match the searched token.");
+ is(gSources.visibleItems.length, 0,
+ "No scripts should be displayed in the scripts container after a bogus search.");
+ is(gSources.selectedIndex, 1,
+ "The scripts container should retain its selected index after a bogus search.");
+
+ clear();
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length + i,
+ "The editor didn't remain at the correct token. (7)");
+
+ isnot(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should not display a notice after the searchbox was emptied.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the searchbox was emptied.");
+ is(gSources.selectedIndex, 1,
+ "The scripts container should have retained its selected index after the searchbox was emptied of a bogus search.");
+
+ noMatchingSourcesSingleCharCheck(token, i);
+ });
+}
+
+function noMatchingSourcesSingleCharCheck(token, i) {
+ let noMatchingSources = gDebugger.L10N.getStr("noMatchingSourcesText");
+
+ write("x");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 8 + token.length + i,
+ "The editor didn't remain at the correct token. (8)");
+
+ is(gSources.widget.getAttribute("label"), noMatchingSources,
+ "The scripts container should display a notice after no matches are found.");
+ is(gSources.visibleItems.length, 0,
+ "No scripts should be shown after no matches are found.");
+ is(gSources.selectedIndex, 1,
+ "The scripts container should have retained its selected index after no matches are found.");
+
+ closeDebuggerAndFinish();
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
new file mode 100644
index 000000000..6e7380ef5
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
@@ -0,0 +1,337 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests basic functionality of global search (lowercase + upper case, expected
+ * UI behavior, number of results found etc.)
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ firstSearch();
+ });
+}
+
+function firstSearch() {
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes 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.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor shouldn't have jumped to a matching line yet.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the global search.");
+
+ let scriptResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ is(scriptResults.length, 2,
+ "There should be matches found in two scripts.");
+
+ let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
+ is(item0.instance.expanded, true,
+ "The first script results should automatically be expanded.")
+ is(item1.instance.expanded, false,
+ "The second script results should not be automatically expanded.")
+
+ let searchResult0 = scriptResults[0].querySelectorAll(".dbg-search-result");
+ let searchResult1 = scriptResults[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 script doesn't have the correct line attached.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the first script 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 script 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 script 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 script 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 script 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 script 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 script 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 script doesn't have the correct line attached.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the second script 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 script 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 script 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 script 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 script 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 script 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 script 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 script doesn't have the correct line attached.");
+
+ is(secondLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The second result for the second script 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 script 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 script 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 script 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 script 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"), ' eval("',
+ "The second result for the second script 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 script doesn't have the correct non-matches in a line.");
+
+
+ secondSearch();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!de");
+ });
+}
+
+function secondSearch() {
+ isnot(gSearchView.widget._list.childNodes.length, 0,
+ "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.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor shouldn't have jumped to a matching line yet.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the global search.");
+
+ let scriptResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ is(scriptResults.length, 2,
+ "There should be matches found in two scripts.");
+
+ let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
+ is(item0.instance.expanded, true,
+ "The first script results should automatically be expanded.")
+ is(item1.instance.expanded, false,
+ "The first script results should not be automatically expanded.")
+
+ let searchResult0 = scriptResults[0].querySelectorAll(".dbg-search-result");
+ let searchResult1 = scriptResults[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 script doesn't have the correct line attached.");
+
+ is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the first script 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 script 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 script 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 script 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 script 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 script 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 script 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 script 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 script 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 script doesn't have the correct line attached.");
+
+ is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1,
+ "The first result for the second script 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 script 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 script 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 script 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 script 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 script 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 script 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 script 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 script doesn't have the correct non-matches in a line.");
+
+
+ testClearView();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ backspace(2);
+ append("ED");
+ });
+}
+
+function testClearView() {
+ gSearchView.clearView();
+
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes after clearView().");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clearView().");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clearView().");
+
+ closeDebuggerAndFinish();
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
new file mode 100644
index 000000000..55278f437
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
@@ -0,0 +1,285 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests if the global search results switch back and forth, and wrap around
+ * when switching between them.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ doSearch();
+ });
+}
+
+function doSearch() {
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes 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.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor shouldn't have jumped to a matching line yet.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the global search.");
+
+ isnot(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane should be visible now.");
+ isnot(gSearchView.widget._parent.hidden, true,
+ "The global search pane should be visible now.");
+ isnot(gSearchView._splitter.hidden, true,
+ "The global search pane splitter should be visible now.");
+
+ doFirstJump();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!eval");
+ });
+}
+
+function doFirstJump() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 4 &&
+ gEditor.getCaretPosition().col == 6,
+ "The editor didn't jump to the correct line. (1)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (1)");
+
+ doSecondJump();
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+ executeSoon(function() {
+ EventUtils.sendKey("DOWN", gDebugger);
+ });
+}
+
+function doSecondJump() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 6,
+ "The editor didn't jump to the correct line. (2)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (2)");
+
+ doWrapAroundJump();
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+ executeSoon(function() {
+ EventUtils.sendKey("RETURN", gDebugger);
+ });
+}
+
+function doWrapAroundJump() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 4 &&
+ gEditor.getCaretPosition().col == 6,
+ "The editor didn't jump to the correct line. (3)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (3)");
+
+ doBackwardsWrapAroundJump();
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+ executeSoon(function() {
+ EventUtils.sendKey("ENTER", gDebugger);
+ });
+}
+
+function doBackwardsWrapAroundJump() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 6,
+ "The editor didn't jump to the correct line. (4)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (4)");
+
+ testSearchTokenEmpty();
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+ executeSoon(function() {
+ EventUtils.sendKey("UP", gDebugger);
+ });
+}
+
+function testSearchTokenEmpty() {
+ gDebugger.addEventListener("Debugger:GlobalSearch:TokenEmpty", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 6,
+ "The editor didn't remain at the correct line. (5)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (5)");
+
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes after clear().");
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after clear().");
+ is(gSearchView._splitter.hidden, true,
+ "The global search pane splitter shouldn't be visible after clear().");
+
+ closeDebuggerAndFinish();
+ });
+ } else {
+ ok(false, "How did you get here? Go away!");
+ }
+ });
+ backspace(4);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function backspace(times) {
+ for (let i = 0; i < times; i++) {
+ EventUtils.sendKey("BACK_SPACE", gDebugger);
+ }
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
new file mode 100644
index 000000000..d8c33b425
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
@@ -0,0 +1,162 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests if the global search results are cleared on location changes, and
+ * the expected UI behaviors are triggered.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ requestLongerTimeout(3);
+
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ doSearch();
+ });
+}
+
+function doSearch() {
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes 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.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor shouldn't have jumped to a matching line yet.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the global search.");
+
+ isnot(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane should be visible now.");
+ isnot(gSearchView.widget._parent.hidden, true,
+ "The global search pane should be visible now.");
+ isnot(gSearchView._splitter.hidden, true,
+ "The global search pane splitter should be visible now.");
+
+ testLocationChange();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!eval");
+ });
+}
+
+function testLocationChange()
+{
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired after location change.");
+ info("Still attached to the tab.");
+
+ executeSoon(function() {
+ is(gSearchView.widget._list.childNodes.length, 0,
+ "The global search pane shouldn't have any child nodes 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.");
+
+ closeDebuggerAndFinish();
+ });
+ });
+
+ content.location = TAB1_URL;
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
new file mode 100644
index 000000000..46422d1f0
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
@@ -0,0 +1,150 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests if the global search results trigger MatchFound and NoMatchFound events
+ * properly, and triggers the expected UI behavior.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ doSearch();
+ });
+}
+
+function doSearch() {
+ gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor shouldn't have jumped to a matching line yet.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the scripts are shown after the global search.");
+
+ testSearchMatchNotFound();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!eval");
+ });
+}
+
+function testSearchMatchNotFound() {
+ gDebugger.addEventListener("Debugger:GlobalSearch:MatchNotFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't remain at the correct line.");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search.");
+
+ closeDebuggerAndFinish();
+ });
+ } else {
+ ok(false, "How did you get here? Go away!");
+ }
+ });
+ append("/punch him/");
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
new file mode 100644
index 000000000..e393db433
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
@@ -0,0 +1,262 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests if the global search results are expanded on scroll or click, and
+ * clicking matches makes the source editor shows the correct script and
+ * makes a selection based on the match.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ doSearch();
+}
+
+function doSearch() {
+ gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ continueTest();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!a");
+ });
+}
+
+function continueTest() {
+ let scriptResults = gDebugger.document.querySelectorAll(".dbg-source-results");
+ is(scriptResults.length, 2,
+ "There should be matches found in two scripts.");
+
+ testScrollToExpand(scriptResults);
+ testExpandCollapse(scriptResults);
+ testAdditionalScrollToExpand(scriptResults);
+ testClickLineToJump(scriptResults, [testClickMatchToJump, closeDebuggerAndFinish]);
+}
+
+function testScrollToExpand(scriptResults) {
+ let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
+
+ is(item0.instance.expanded, true,
+ "The first script results should automatically be expanded.");
+ is(item1.instance.expanded, false,
+ "The first script results should not be automatically expanded.");
+
+ gSearchView._forceExpandResults = true;
+ gSearchView._onScroll();
+
+ is(item0.instance.expanded, true,
+ "The first script results should be expanded after scrolling.");
+ is(item1.instance.expanded, true,
+ "The second script results should be expanded after scrolling.");
+}
+
+function testExpandCollapse(scriptResults) {
+ let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
+ let firstHeader = scriptResults[0].querySelector(".dbg-results-header");
+ let secondHeader = scriptResults[1].querySelector(".dbg-results-header");
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstHeader);
+ EventUtils.sendMouseEvent({ type: "click" }, secondHeader);
+
+ is(item0.instance.expanded, false,
+ "The first script results should be collapsed on click.")
+ is(item1.instance.expanded, false,
+ "The second script results should be collapsed on click.")
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstHeader);
+ EventUtils.sendMouseEvent({ type: "click" }, secondHeader);
+
+ is(item0.instance.expanded, true,
+ "The first script results should be expanded on an additional click.");
+ is(item1.instance.expanded, true,
+ "The second script results should be expanded on an additional click.");
+}
+
+function testAdditionalScrollToExpand(scriptResults) {
+ let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
+ let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
+ let firstHeader = scriptResults[0].querySelector(".dbg-results-header");
+ let secondHeader = scriptResults[1].querySelector(".dbg-results-header");
+
+ EventUtils.sendMouseEvent({ type: "click" }, firstHeader);
+ EventUtils.sendMouseEvent({ type: "click" }, secondHeader);
+
+ is(item0.instance.expanded, false,
+ "The first script results should be recollapsed on click.")
+ is(item1.instance.expanded, false,
+ "The second script results should be recollapsed on click.")
+
+ gSearchView._onScroll();
+
+ is(item0.instance.expanded, false,
+ "The first script results should not be automatically re-expanded on scroll after a user collapsed them.")
+ is(item1.instance.expanded, false,
+ "The second script results should not be automatically re-expanded on scroll after a user collapsed them.")
+}
+
+function testClickLineToJump(scriptResults, callbacks) {
+ let targetResults = scriptResults[0];
+ let firstHeader = targetResults.querySelector(".dbg-results-header");
+ let firstHeaderItem = gDebugger.SourceResults.getItemForElement(firstHeader);
+ firstHeaderItem.instance.expand()
+
+ is(firstHeaderItem.instance.expanded, true,
+ "The first script results should be expanded after direct function call.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 4,
+ "The editor didn't jump to the correct line. (1)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (1)");
+
+ callbacks[0](scriptResults, callbacks.slice(1));
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+
+ let firstLine = targetResults.querySelector(".dbg-results-line-contents");
+ EventUtils.sendMouseEvent({ type: "click" }, firstLine);
+}
+
+function testClickMatchToJump(scriptResults, callbacks) {
+ let targetResults = scriptResults[1];
+ let secondHeader = targetResults.querySelector(".dbg-results-header");
+ let secondHeaderItem = gDebugger.SourceResults.getItemForElement(secondHeader);
+ secondHeaderItem.instance.expand()
+
+ is(secondHeaderItem.instance.expanded, true,
+ "The second script results should be expanded after direct function call.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + aEvent.detail.url + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 5 &&
+ gEditor.getCaretPosition().col == 5,
+ "The editor didn't jump to the correct line. (1)");
+ is(gSources.visibleItems.length, 2,
+ "Not all the correct scripts are shown after the search. (1)");
+
+ callbacks[0]();
+ });
+ } else {
+ ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
+ }
+ });
+
+ let matches = targetResults.querySelectorAll(".dbg-results-line-contents-string[match=true]");
+ let lastMatch = matches[matches.length - 1];
+ EventUtils.sendMouseEvent({ type: "click" }, lastMatch);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js
new file mode 100644
index 000000000..70809fa69
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js
@@ -0,0 +1,208 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+/**
+ * Tests if the global search results are hidden when they're supposed to
+ * (after a focus lost, or when ESCAPE is pressed).
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchView = null;
+var gSearchBox = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gDebugger.SourceResults.prototype.alwaysExpand = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ }
+ }
+}
+
+function testScriptSearching() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchView = gDebugger.DebuggerView.GlobalSearch;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+ doSearch();
+}
+
+function doSearch() {
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible yet.");
+
+ gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ testFocusLost();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global search.");
+ }
+ });
+ executeSoon(function() {
+ write("!a");
+ });
+}
+
+function testFocusLost()
+{
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible after a search.");
+
+ gDebugger.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ reshowSearch();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after the global search stopped.");
+ }
+ });
+ executeSoon(function() {
+ gDebugger.DebuggerView.editor.focus();
+ });
+}
+
+function reshowSearch() {
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after the search was stopped.");
+
+ gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ testEscape();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after a global re-search.");
+ }
+ });
+ executeSoon(function() {
+ sendEnter();
+ });
+}
+
+function testEscape()
+{
+ is(gSearchView.widget._parent.hidden, false,
+ "The global search pane should be visible after a re-search.");
+
+ gDebugger.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("-02.js") != -1) {
+ executeSoon(function() {
+ finalCheck();
+ });
+ } else {
+ ok(false, "The current script shouldn't have changed after the global search escaped.");
+ }
+ });
+ executeSoon(function() {
+ sendEscape();
+ });
+}
+
+function finalCheck()
+{
+ is(gSearchView.widget._parent.hidden, true,
+ "The global search pane shouldn't be visible after the search was escaped.");
+
+ closeDebuggerAndFinish();
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function sendEnter() {
+ gSearchBox.focus();
+ EventUtils.sendKey("ENTER", gDebugger);
+}
+
+function sendEscape() {
+ gSearchBox.focus();
+ EventUtils.sendKey("ESCAPE", gDebugger);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchView = null;
+ gSearchBox = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
new file mode 100644
index 000000000..d688c6c1e
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
@@ -0,0 +1,693 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_update-editor-mode.html";
+
+/**
+ * Tests basic functionality of scripts filtering (file search) helper UI.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gSources = null;
+var gSearchBox = null;
+var gFilteredSources = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+ });
+ });
+}
+
+function testScriptSearching() {
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gFilteredSources = gDebugger.DebuggerView.FilteredSources;
+
+ firstSearch();
+}
+
+function firstSearch() {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ for (let i = 0; i < gFilteredSources.itemCount; i++) {
+ is(gFilteredSources.labels[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.labels[i]),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.values[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.values[i], 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].label,
+ gDebugger.SourceUtils.trimUrlLength(gSources.labels[i]),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].value,
+ gDebugger.SourceUtils.trimUrlLength(gSources.values[i], 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].attachment.fullLabel, gSources.labels[i],
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].attachment.fullValue, gSources.values[i],
+ "The filtered sources view should have the correct values.");
+ }
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("update-editor-mode.html") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ secondSearch();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ write(".");
+}
+
+function secondSearch() {
+ let sourceshown = false;
+ let popupshown = false;
+ let proceeded = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent1);
+ sourceshown = true;
+ executeSoon(proceed);
+ });
+ gDebugger.addEventListener("popupshown", function _onEvent2(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent2);
+ popupshown = true;
+ executeSoon(proceed);
+ });
+
+ function proceed() {
+ if (!sourceshown || !popupshown || proceeded) {
+ return;
+ }
+ proceeded = true;
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 1,
+ "The filtered sources view should have 1 items available.");
+ is(gFilteredSources.visibleItems.length, 1,
+ "The filtered sources view should have 1 items visible.");
+
+ for (let i = 0; i < gFilteredSources.itemCount; i++) {
+ is(gFilteredSources.labels[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].label),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.values[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].value, 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].label,
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].label),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].value,
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].value, 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].attachment.fullLabel, gSources.visibleItems[i].label,
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].attachment.fullValue, gSources.visibleItems[i].value,
+ "The filtered sources view should have the correct values.");
+ }
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 1,
+ "Not all the correct scripts are shown after the search.");
+
+ thirdSearch();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ }
+ write(".-0");
+}
+
+function thirdSearch() {
+ let sourceshown = false;
+ let popupshown = false;
+ let proceeded = false;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent1);
+ sourceshown = true;
+ executeSoon(proceed);
+ });
+ gDebugger.addEventListener("popupshown", function _onEvent2(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent2);
+ popupshown = true;
+ executeSoon(proceed);
+ });
+
+ function proceed() {
+ if (!sourceshown || !popupshown || proceeded) {
+ return;
+ }
+ proceeded = true;
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ for (let i = 0; i < gFilteredSources.itemCount; i++) {
+ is(gFilteredSources.labels[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].label),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.values[i],
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].value, 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].label,
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].label),
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].value,
+ gDebugger.SourceUtils.trimUrlLength(gSources.visibleItems[i].value, 0, "start"),
+ "The filtered sources view should have the correct values.");
+
+ is(gFilteredSources.visibleItems[i].attachment.fullLabel, gSources.visibleItems[i].label,
+ "The filtered sources view should have the correct labels.");
+ is(gFilteredSources.visibleItems[i].attachment.fullValue, gSources.visibleItems[i].value,
+ "The filtered sources view should have the correct values.");
+ }
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("update-editor-mode.html") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ goDown();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ }
+ write(".-");
+}
+
+function goDown() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-editor-mode") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ goDownAgain();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.sendKey("DOWN", gDebugger);
+}
+
+function goDownAgain() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ goDownAndWrap();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
+}
+
+function goDownAndWrap() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("update-editor-mode.html") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ goUpAndWrap();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger);
+}
+
+function goUpAndWrap() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ clickAndSwitch();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.sendKey("UP", gDebugger);
+}
+
+function clickAndSwitch() {
+ let sourceshown = false;
+ let popuphidden = false;
+ let popupshown = false;
+ let reopened = false;
+ let proceeded = false;
+
+ gDebugger.addEventListener("popuphidden", function _onEvent2(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent2);
+ info("Popup was hidden...");
+ popuphidden = true;
+
+ gDebugger.addEventListener("popupshown", function _onEvent3(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent3);
+ info("Popup was shown...");
+ popupshown = true;
+
+ proceed();
+ });
+
+ reopen();
+ });
+
+ function reopen() {
+ if (!sourceshown || !popuphidden || reopened) {
+ return;
+ }
+ info("Reopening popup...");
+ reopened = true;
+ append(".-");
+ }
+
+ function proceed() {
+ if (!sourceshown || !popuphidden || !popupshown || proceeded) {
+ return;
+ }
+ info("Proceeding with next test...");
+ proceeded = true;
+ executeSoon(clickAndSwitchAgain);
+ }
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("update-editor-mode.html") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ info("Source was shown and verified");
+ sourceshown = true;
+ reopen();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+
+ ok(gFilteredSources.widget._parent.querySelectorAll(".results-panel-item")[0]
+ .classList.contains("results-panel-item"),
+ "The first visible item target isn't the correct one.");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gFilteredSources.widget._parent.querySelectorAll(".results-panel-item")[0],
+ gDebugger);
+}
+
+function clickAndSwitchAgain() {
+ let sourceshown = false;
+ let popuphidden = false;
+ let popupshown = false;
+ let reopened = false;
+ let proceeded = false;
+
+ gDebugger.addEventListener("popuphidden", function _onEvent2(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent2);
+ info("Popup was hidden...");
+ popuphidden = true;
+
+ gDebugger.addEventListener("popupshown", function _onEvent3(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent3);
+ info("Popup was shown...");
+ popupshown = true;
+
+ proceed();
+ });
+
+ reopen();
+ });
+
+ function reopen() {
+ if (!sourceshown || !popuphidden || reopened) {
+ return;
+ }
+ info("Reopening popup...");
+ reopened = true;
+ append(".-");
+ }
+
+ function proceed() {
+ if (!sourceshown || !popuphidden || !popupshown || proceeded) {
+ return;
+ }
+ info("Proceeding with next test...");
+ proceeded = true;
+ executeSoon(switchFocusWithEscape);
+ }
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ is(gFilteredSources.itemCount, 3,
+ "The filtered sources view should have 3 items available.");
+ is(gFilteredSources.visibleItems.length, 3,
+ "The filtered sources view should have 3 items visible.");
+
+ is(gFilteredSources.selectedValue,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedValue, 0, "start"),
+ "The correct item should be selected in the filtered sources view");
+ is(gFilteredSources.selectedLabel,
+ gDebugger.SourceUtils.trimUrlLength(gSources.selectedLabel),
+ "The correct item should be selected in the filtered sources view");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ info("Source was shown and verified");
+ sourceshown = true;
+ reopen();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+
+ ok(gFilteredSources.widget._parent.querySelectorAll(".results-panel-item")[2]
+ .classList.contains("results-panel-item"),
+ "The first visible item target isn't the correct one.");
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ gFilteredSources.widget._parent.querySelectorAll(".results-panel-item")[2],
+ gDebugger);
+}
+
+function switchFocusWithEscape() {
+ gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("update-editor-mode.html") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ focusAgainAfterEscape();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.sendKey("ESCAPE", gDebugger);
+}
+
+function focusAgainAfterEscape() {
+ gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 1,
+ "Not all the correct scripts are shown after the search.");
+
+ switchFocusWithReturn();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ append("0");
+}
+
+function switchFocusWithReturn() {
+ gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+
+ info("Current script url:\n" + gSources.selectedValue + "\n");
+ info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+ let url = gSources.selectedValue;
+ if (url.indexOf("test-script-switching-01.js") != -1) {
+
+ executeSoon(function() {
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+ ok(gEditor.getCaretPosition().line == 0 &&
+ gEditor.getCaretPosition().col == 0,
+ "The editor didn't jump to the correct line.");
+ is(gSources.visibleItems.length, 3,
+ "Not all the correct scripts are shown after the search.");
+
+ closeDebuggerAndFinish();
+ });
+ } else {
+ ok(false, "How did you get here?");
+ }
+ });
+ EventUtils.sendKey("RETURN", gDebugger);
+}
+
+function clear() {
+ gSearchBox.focus();
+ gSearchBox.value = "";
+}
+
+function write(text) {
+ clear();
+ append(text);
+}
+
+function append(text) {
+ gSearchBox.focus();
+
+ for (let i = 0; i < text.length; i++) {
+ EventUtils.sendChar(text[i], gDebugger);
+ }
+ info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gSearchBox = null;
+ gFilteredSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-searching-popup.js b/browser/devtools/debugger/test/browser_dbg_scripts-searching-popup.js
new file mode 100644
index 000000000..0eddd95fc
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-popup.js
@@ -0,0 +1,57 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSearchBox = null;
+var gSearchBoxPanel = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ runTest();
+ });
+}
+
+function runTest() {
+ gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+ gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel;
+
+ focusSearchbox();
+}
+
+function focusSearchbox() {
+ is(gSearchBoxPanel.state, "closed",
+ "The search box panel shouldn't be visible yet.");
+
+ gSearchBoxPanel.addEventListener("popupshown", function _onEvent(aEvent) {
+ gSearchBoxPanel.removeEventListener(aEvent.type, _onEvent);
+
+ is(gSearchBoxPanel.state, "open",
+ "The search box panel should be visible after searching started.");
+
+ closeDebuggerAndFinish();
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, gSearchBox);
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSearchBox = null;
+ gSearchBoxPanel = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-sorting.js b/browser/devtools/debugger/test/browser_dbg_scripts-sorting.js
new file mode 100644
index 000000000..821d285cb
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-sorting.js
@@ -0,0 +1,124 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+ resumeAndFinish();
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+function resumeAndFinish() {
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ checkScriptsOrder();
+ addScriptAndCheckOrder(1, function() {
+ addScriptAndCheckOrder(2, function() {
+ addScriptAndCheckOrder(3, function() {
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+ });
+}
+
+function addScriptAndCheckOrder(method, callback) {
+ let sv = gDebugger.SourceUtils;
+ let vs = gDebugger.DebuggerView.Sources;
+ vs.empty();
+ vs.widget.removeEventListener("select", vs._onScriptsChange, false);
+
+ 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;
+ });
+
+ switch (method) {
+ case 1:
+ urls.forEach(function(url) {
+ let loc = url.href + url.leaf;
+ vs.push([sv.getSourceLabel(loc), { url: loc }], { staged: true });
+ });
+ vs.commit({ sorted: true });
+ break;
+
+ case 2:
+ urls.forEach(function(url) {
+ let loc = url.href + url.leaf;
+ vs.push([sv.getSourceLabel(loc), { url: loc }]);
+ });
+ break;
+
+ case 3:
+ let i = 0
+ for (; i < urls.length / 2; i++) {
+ let url = urls[i];
+ let loc = url.href + url.leaf;
+ vs.push([sv.getSourceLabel(loc), { url: loc }], { staged: true });
+ }
+ vs.commit({ sorted: true });
+
+ for (; i < urls.length; i++) {
+ let url = urls[i];
+ let loc = url.href + url.leaf;
+ vs.push([sv.getSourceLabel(loc), { url: loc }]);
+ }
+ break;
+ }
+
+ executeSoon(function() {
+ checkScriptsOrder(method);
+ callback();
+ });
+}
+
+function checkScriptsOrder(method) {
+ let labels = gDebugger.DebuggerView.Sources.labels;
+ let sorted = labels.reduce(function(prev, curr, index, array) {
+ return array[index - 1] < array[index];
+ });
+
+ ok(sorted,
+ "Using method " + method + ", " +
+ "the scripts weren't in the correct order: " + labels.toSource());
+
+ return sorted;
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-switching-02.js b/browser/devtools/debugger/test/browser_dbg_scripts-switching-02.js
new file mode 100644
index 000000000..b444ae100
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-switching-02.js
@@ -0,0 +1,175 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that switching the displayed script in the UI works as advertised
+ * when urls are escaped.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching-02.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSources = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: testScriptsDisplay }, 0);
+ }
+ }
+}
+
+function testScriptsDisplay() {
+ gSources = gDebugger.DebuggerView.Sources;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ for (let i = 0; i < gSources.itemCount; i++) {
+ info("label: " + i + " " + gSources.getItemAtIndex(i).target.getAttribute("label"));
+ }
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+ let params = "?foo=bar,baz|lol";
+
+ ok(gDebugger.DebuggerView.Sources.containsValue(EXAMPLE_URL +
+ label1), "First script url is incorrect.");
+ ok(gDebugger.DebuggerView.Sources.containsValue(EXAMPLE_URL +
+ label2 + params), "Second script url is incorrect.");
+
+ ok(gDebugger.DebuggerView.Sources.containsLabel(
+ label1), "First script label is incorrect.");
+ ok(gDebugger.DebuggerView.Sources.containsLabel(
+ label2), "Second script label is incorrect.");
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label2, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label2 + params, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The correct script was loaded initially.");
+
+ is(gDebugger.editor.getDebugLocation(), 5,
+ "Editor debugger location is correct.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitch1();
+ }
+ });
+
+ gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL + label1;
+}
+
+function testSwitch1() {
+ dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n");
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+ let params = "?foo=bar,baz|lol";
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label1, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label1, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) == -1,
+ "The second script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) != -1,
+ "The first script is displayed.");
+
+ is(gDebugger.editor.getDebugLocation(), -1,
+ "Editor debugger location has been cleared.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitch2();
+ }
+ });
+
+ gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL + label2 + params;
+}
+
+function testSwitch2() {
+ dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n");
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+ let params = "?foo=bar,baz|lol";
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label2, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label2 + params, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The correct script was loaded initially.");
+
+ is(gDebugger.editor.getDebugLocation(), 5,
+ "Editor debugger location is correct.");
+
+ closeDebuggerAndFinish();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_scripts-switching.js b/browser/devtools/debugger/test/browser_dbg_scripts-switching.js
new file mode 100644
index 000000000..e291bf7c9
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-switching.js
@@ -0,0 +1,178 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that switching the displayed script in the UI works as advertised.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSources = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent)
+ {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: testScriptsDisplay }, 0);
+ }
+ }
+}
+
+function testScriptsDisplay() {
+ gSources = gDebugger.DebuggerView.Sources;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 2,
+ "Found the expected number of scripts.");
+
+ for (let i = 0; i < gSources.itemCount; i++) {
+ info("label: " + i + " " + gSources.getItemAtIndex(i).target.getAttribute("label"));
+ }
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+
+ ok(gDebugger.DebuggerView.Sources.containsValue(EXAMPLE_URL +
+ label1), "First script url is incorrect.");
+ ok(gDebugger.DebuggerView.Sources.containsValue(EXAMPLE_URL +
+ label2), "Second script url is incorrect.");
+
+ ok(gDebugger.DebuggerView.Sources.containsLabel(
+ label1), "First script label is incorrect.");
+ ok(gDebugger.DebuggerView.Sources.containsLabel(
+ label2), "Second script label is incorrect.");
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label2, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label2, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The correct script was loaded initially.");
+
+ is(gDebugger.editor.getDebugLocation(), 5,
+ "editor debugger location is correct.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitchPaused();
+ }
+ });
+
+ gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL + label1;
+}
+
+function testSwitchPaused()
+{
+ dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n");
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label1, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label1, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) == -1,
+ "The second script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) != -1,
+ "The first script is displayed.");
+
+ is(gDebugger.editor.getDebugLocation(), -1,
+ "Editor debugger location has been cleared.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitchRunning();
+ }
+ });
+
+ gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL + label2;
+ });
+}
+
+function testSwitchRunning()
+{
+ dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n");
+
+ let label1 = "test-script-switching-01.js";
+ let label2 = "test-script-switching-02.js";
+
+ ok(gDebugger.DebuggerView.Sources.selectedItem,
+ "There should be a selected item in the sources pane.");
+ is(gDebugger.DebuggerView.Sources.selectedLabel,
+ label2, "The selected label is the sources pane is incorrect.");
+ is(gDebugger.DebuggerView.Sources.selectedValue, EXAMPLE_URL +
+ label2, "The selected value is the sources pane is incorrect.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The second script is displayed again.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) == -1,
+ "The first script is no longer displayed.");
+
+ is(gDebugger.editor.getDebugLocation(), -1,
+ "Editor debugger location is still -1.");
+
+ closeDebuggerAndFinish();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_select-line.js b/browser/devtools/debugger/test/browser_dbg_select-line.js
new file mode 100644
index 000000000..bb18ea651
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_select-line.js
@@ -0,0 +1,122 @@
+/* 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 script in the editor
+ * pane and highlights the proper line.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSources = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let testStarted = false;
+ let resumed = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent) {
+ scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: testSelectLine }, 0);
+ }
+ }
+}
+
+function testSelectLine() {
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 2, "Found the expected number of scripts.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The correct script was loaded initially.");
+
+ // Yield control back to the event loop so that the debugger has a
+ // chance to highlight the proper line.
+ executeSoon(function() {
+ // getCaretPosition is 0-based.
+ is(gDebugger.editor.getCaretPosition().line, 5,
+ "The correct line is selected.");
+
+ gDebugger.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, function onChange() {
+ // Wait for the actual text to be shown.
+ if (gDebugger.editor.getText() == gDebugger.L10N.getStr("loadingText")) {
+ return;
+ }
+ // The requested source text has been shown, remove the event listener.
+ gDebugger.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onChange);
+
+ ok(gDebugger.editor.getText().search(/debugger/) == -1,
+ "The second script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) != -1,
+ "The first script is displayed.");
+
+ // Yield control back to the event loop so that the debugger has a
+ // chance to highlight the proper line.
+ executeSoon(function(){
+ // getCaretPosition is 0-based.
+ is(gDebugger.editor.getCaretPosition().line, 4,
+ "The correct line is selected.");
+
+ closeDebuggerAndFinish();
+ });
+ });
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 4,
+ "Should have two frames.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.querySelector("#stackframe-3"),
+ gDebugger);
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_source_maps-01.js b/browser/devtools/debugger/test/browser_dbg_source_maps-01.js
new file mode 100644
index 000000000..c8ff8aae9
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_source_maps-01.js
@@ -0,0 +1,156 @@
+/* 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 + "binary_search.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", true);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ resumed = true;
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+ ok(aEvent.detail.url.indexOf(".coffee") != -1,
+ "The debugger should show the source mapped coffee script file.");
+ ok(gDebugger.editor.getText().search(/isnt/) != -1,
+ "The debugger's editor should have the coffee script source displayed.");
+
+ testSetBreakpoint();
+ });
+ });
+}
+
+function testSetBreakpoint() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.interrupt(function (aResponse) {
+ activeThread.setBreakpoint({
+ url: EXAMPLE_URL + "binary_search.coffee",
+ line: 5
+ }, function (aResponse, bpClient) {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a coffee script file.");
+ testSetBreakpointBlankLine();
+ });
+ });
+}
+
+function testSetBreakpointBlankLine() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.setBreakpoint({
+ url: EXAMPLE_URL + "binary_search.coffee",
+ line: 3
+ }, function (aResponse, bpClient) {
+ ok(aResponse.actualLocation,
+ "Because 3 is empty, we should have an actualLocation");
+ is(aResponse.actualLocation.url, EXAMPLE_URL + "binary_search.coffee",
+ "actualLocation.url should be source mapped to the coffee file");
+ is(aResponse.actualLocation.line, 2,
+ "actualLocation.line should be source mapped back to 2");
+ testHitBreakpoint();
+ });
+}
+
+function testHitBreakpoint() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.resume(function (aResponse) {
+ ok(!aResponse.error, "Shouldn't get an error resuming");
+ is(aResponse.type, "resumed", "Type should be 'resumed'");
+
+ activeThread.addOneTimeListener("paused", function (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");
+
+ waitForCaretPos(4, testStepping);
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the paused
+ // state.
+ executeSoon(function() {
+ gDebuggee.binary_search([0, 2, 3, 5, 7, 10], 5);
+ });
+ });
+}
+
+function testStepping() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.stepIn(function (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.
+ activeThread.addOneTimeListener("paused", function (aEvent, aPacket) {
+
+ // 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' hasn't been assigned to yet");
+ is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined",
+ "'pivot' hasn't been assigned to yet");
+
+ waitForCaretPos(5, closeDebuggerAndFinish);
+ });
+ });
+}
+
+function waitForCaretPos(number, callback)
+{
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gDebugger.DebuggerView.editor.getCaretPosition().line != number) {
+ return;
+ }
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+registerCleanupFunction(function() {
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_source_maps-02.js b/browser/devtools/debugger/test/browser_dbg_source_maps-02.js
new file mode 100644
index 000000000..40a5fa0bb
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_source_maps-02.js
@@ -0,0 +1,205 @@
+/* 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 + "binary_search.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gPrevPref = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let resumed = false;
+ let testStarted = false;
+
+ gPrevPref = Services.prefs.getBoolPref(
+ "devtools.debugger.source-maps-enabled");
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", true);
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ resumed = true;
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+ // Show original sources should be already enabled.
+ is(gPrevPref, false,
+ "The source maps functionality should be disabled 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. ")
+
+ ok(aEvent.detail.url.indexOf(".coffee") != -1,
+ "The debugger should show the source mapped coffee script file.");
+ ok(aEvent.detail.url.indexOf(".js") == -1,
+ "The debugger should not show the generated js script file.");
+ ok(gDebugger.editor.getText().search(/isnt/) != -1,
+ "The debugger's editor should have the coffee script source displayed.");
+ ok(gDebugger.editor.getText().search(/function/) == -1,
+ "The debugger's editor should not have the JS source displayed.");
+
+ testToggleGeneratedSource();
+ });
+ });
+}
+
+function testToggleGeneratedSource() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+
+ is(gDebugger.Prefs.sourceMapsEnabled, false,
+ "The source maps pref should have been set to false.");
+ is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"),
+ "false", "Source maps should be enabled from startup. ")
+
+ ok(aEvent.detail.url.indexOf(".coffee") == -1,
+ "The debugger should not show the source mapped coffee script file.");
+ ok(aEvent.detail.url.indexOf(".js") != -1,
+ "The debugger should show the generated js script file.");
+ ok(gDebugger.editor.getText().search(/isnt/) == -1,
+ "The debugger's editor should have the coffee script source displayed.");
+ ok(gDebugger.editor.getText().search(/function/) != -1,
+ "The debugger's editor should not have the JS source displayed.");
+
+ testSetBreakpoint();
+ });
+
+ // Disable source maps.
+ gDebugger.DebuggerView.Options._showOriginalSourceItem.setAttribute("checked",
+ "false");
+ gDebugger.DebuggerView.Options._toggleShowOriginalSource();
+ gDebugger.DebuggerView.Options._onPopupHidden();
+}
+
+function testSetBreakpoint() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.setBreakpoint({
+ url: EXAMPLE_URL + "binary_search.js",
+ line: 7
+ }, function (aResponse, bpClient) {
+ ok(!aResponse.error,
+ "Should be able to set a breakpoint in a JavaScript file.");
+ testHitBreakpoint();
+ });
+}
+
+function testHitBreakpoint() {
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.resume(function (aResponse) {
+ ok(!aResponse.error, "Shouldn't get an error resuming");
+ is(aResponse.type, "resumed", "Type should be 'resumed'");
+
+ activeThread.addOneTimeListener("framesadded", function (aEvent, aPacket) {
+ // Make sure that we have JavaScript stack frames.
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Correct number of frames.");
+ ok(frames.querySelector("#stackframe-0 .dbg-stackframe-details")
+ .getAttribute("value").search(/js/),
+ "First frame should be a JS frame.");
+
+ waitForCaretPos(6, testToggleOnPause);
+ });
+
+ // This will cause the breakpoint to be hit, and put us back in the paused
+ // stated.
+ executeSoon(function() {
+ gDebuggee.binary_search([0, 2, 3, 5, 7, 10], 5);
+ });
+ });
+}
+
+function testToggleOnPause() {
+ gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+ gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+
+ is(gDebugger.Prefs.sourceMapsEnabled, true,
+ "The source maps pref should have been set to true.");
+ is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"),
+ "true", "Source maps should be enabled. ")
+
+ ok(aEvent.detail.url.indexOf(".coffee") != -1,
+ "The debugger should show the source mapped coffee script file.");
+ ok(aEvent.detail.url.indexOf(".js") == -1,
+ "The debugger should not show the generated js script file.");
+ ok(gDebugger.editor.getText().search(/isnt/) != -1,
+ "The debugger's editor should not have the coffee script source displayed.");
+ ok(gDebugger.editor.getText().search(/function/) == -1,
+ "The debugger's editor should have the JS source displayed.");
+
+ // Make sure that we have coffee script stack frames.
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Correct number of frames.");
+ ok(frames.querySelector("#stackframe-0 .dbg-stackframe-details")
+ .getAttribute("value").search(/coffee/),
+ "First frame should be a coffee script frame.");
+
+ waitForCaretPos(4, resumeAndFinish);
+ });
+
+ // Enable source maps.
+ gDebugger.DebuggerView.Options._showOriginalSourceItem.setAttribute("checked",
+ "true");
+ gDebugger.DebuggerView.Options._toggleShowOriginalSource();
+ gDebugger.DebuggerView.Options._onPopupHidden();
+}
+
+function resumeAndFinish()
+{
+ let { activeThread } = gDebugger.DebuggerController;
+ activeThread.resume(function (aResponse) {
+ ok(!aResponse.error, "Shouldn't get an error resuming");
+ is(aResponse.type, "resumed", "Type should be 'resumed'");
+
+ closeDebuggerAndFinish();
+ });
+}
+
+function waitForCaretPos(number, callback)
+{
+ // Poll every few milliseconds until the source editor line is active.
+ let count = 0;
+ let intervalID = window.setInterval(function() {
+ info("count: " + count + " ");
+ if (++count > 50) {
+ ok(false, "Timed out while polling for the line.");
+ window.clearInterval(intervalID);
+ return closeDebuggerAndFinish();
+ }
+ if (gDebugger.DebuggerView.editor.getCaretPosition().line != number) {
+ return;
+ }
+ is(gDebugger.DebuggerView.editor.getCaretPosition().line, number,
+ "The right line is focused.")
+ // We got the source editor at the expected line, it's safe to callback.
+ window.clearInterval(intervalID);
+ callback();
+ }, 100);
+}
+
+registerCleanupFunction(function() {
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gPrevPref = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_sources-cache.js b/browser/devtools/debugger/test/browser_dbg_sources-cache.js
new file mode 100644
index 000000000..b8533c25f
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_sources-cache.js
@@ -0,0 +1,167 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_function-search-01.html";
+
+/**
+ * Tests if the sources cache knows how to cache sources when prompted.
+ */
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gEditor = null;
+let gSources = null;
+let gControllerSources = null;
+let gPrevLabelsCache = null;
+let gPrevGroupsCache = null;
+const TOTAL_SOURCES = 3;
+
+requestLongerTimeout(2);
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ Services.tm.currentThread.dispatch({ run: testSourcesCache }, 0);
+ });
+ });
+}
+
+function testSourcesCache()
+{
+ gEditor = gDebugger.DebuggerView.editor;
+ gSources = gDebugger.DebuggerView.Sources;
+ gControllerSources = gDebugger.DebuggerController.SourceScripts;
+
+ ok(gEditor.getText().contains("First source!"),
+ "Editor text contents appears to be correct.");
+ is(gSources.selectedLabel, "test-function-search-01.js",
+ "The currently selected label in the sources container is correct.");
+ ok(gSources.selectedValue.contains("test-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.labels.length, TOTAL_SOURCES,
+ "There should be " + TOTAL_SOURCES + " labels 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.labels.toSource());
+ info("Source values: " + gSources.values.toSource());
+
+ is(gSources.labels.sort()[0], "test-function-search-01.js",
+ "The first source label is correct.");
+ ok(gSources.values.sort()[0].contains("test-function-search-01.js"),
+ "The first source value appears to be correct.");
+
+ is(gSources.labels.sort()[1], "test-function-search-02.js",
+ "The second source label is correct.");
+ ok(gSources.values.sort()[1].contains("test-function-search-02.js"),
+ "The second source value appears to be correct.");
+
+ is(gSources.labels.sort()[2], "test-function-search-03.js",
+ "The third source label is correct.");
+ ok(gSources.values.sort()[2].contains("test-function-search-03.js"),
+ "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");
+
+ gPrevLabelsCache = gDebugger.SourceUtils._labelsCache;
+ gPrevGroupsCache = gDebugger.SourceUtils._groupsCache;
+
+ fetchSources(function() {
+ performReload(function() {
+ closeDebuggerAndFinish();
+ });
+ });
+}
+
+function fetchSources(callback) {
+ gControllerSources.getTextForSources(gSources.values).then((aSources) => {
+ testCacheIntegrity(aSources);
+ callback();
+ });
+}
+
+function performReload(callback) {
+ gDebugger.DebuggerController._target.once("will-navigate", testStateBeforeReload);
+ gDebugger.DebuggerController._target.once("navigate", function onTabNavigated(aEvent, aPacket) {
+ ok(true, "tabNavigated event was fired.");
+ info("Still attached to the tab.");
+
+ testStateAfterReload();
+ callback();
+ });
+
+ gDebuggee.location.reload();
+}
+
+function testStateBeforeReload() {
+ is(gSources.itemCount, 0,
+ "There should be no sources present in the sources list during reload.");
+ is(gControllerSources.getCache().length, 0,
+ "The sources cache should be empty 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.");
+}
+
+function testCacheIntegrity(aCache) {
+ for (let source of aCache) {
+ let [url, contents] = source;
+
+ // Sources of a debugee don't always finish fetching consecutively. D'uh.
+ let index = url.match(/test-function-search-0(\d)/).pop();
+
+ ok(index >= 1 && index <= TOTAL_SOURCES,
+ "Found a source url cached correctly (" + index + ")");
+ ok(contents.contains(
+ ["First source!", "Second source!", "Third source!"][index - 1]),
+ "Found a source's text contents cached correctly (" + index + ")");
+
+ info("Cached source url at " + index + ": " + url);
+ info("Cached source text at " + index + ": " + contents);
+ }
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gEditor = null;
+ gSources = null;
+ gControllerSources = null;
+ gPrevLabelsCache = null;
+ gPrevGroupsCache = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack-01.js b/browser/devtools/debugger/test/browser_dbg_stack-01.js
new file mode 100644
index 000000000..36b6aeb03
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack-01.js
@@ -0,0 +1,54 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testSimpleCall();
+ });
+}
+
+function testSimpleCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+ "Should have only one frame.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.simpleCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack-02.js b/browser/devtools/debugger/test/browser_dbg_stack-02.js
new file mode 100644
index 000000000..8bd6a2d0b
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack-02.js
@@ -0,0 +1,86 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testEvalCall();
+ });
+}
+
+function testEvalCall() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 2,
+ "Should have two frames.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ is(frames.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"),
+ "(eval)", "Frame name should be (eval)");
+
+ ok(frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should be selected by default.");
+
+ ok(!frames.querySelector("#stackframe-1").parentNode.hasAttribute("checked"),
+ "Second frame should not be selected.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.querySelector("#stackframe-1"),
+ gDebugger);
+
+ ok(!frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should not be selected after click.");
+
+ ok(frames.querySelector("#stackframe-1").parentNode.hasAttribute("checked"),
+ "Second frame should be selected after click.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.querySelector("#stackframe-0 .dbg-stackframe-title"),
+ gDebugger);
+
+ ok(frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should be selected after click inside the first frame.");
+
+ ok(!frames.querySelector("#stackframe-1").parentNode.hasAttribute("checked"),
+ "Second frame should not be selected after click inside the first frame.");
+
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+ }}, 0);
+ });
+
+ gDebuggee.evalCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack-03.js b/browser/devtools/debugger/test/browser_dbg_stack-03.js
new file mode 100644
index 000000000..1ef00d3a3
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack-03.js
@@ -0,0 +1,71 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testRecurse();
+ });
+}
+
+function testRecurse() {
+ gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let pageSize = gDebugger.gCallStackPageSize;
+ let recurseLimit = gDebuggee.gRecurseLimit;
+ let childNodes = frames.childNodes;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, pageSize,
+ "Should have the max limit of frames.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ is(frames.querySelectorAll(".dbg-stackframe").length, pageSize * 2,
+ "Should now have twice the max limit of frames.");
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ is(frames.querySelectorAll(".dbg-stackframe").length, recurseLimit,
+ "Should have reached the recurse limit.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ window.clearInterval(scrollingInterval);
+ closeDebuggerAndFinish();
+ });
+ });
+ });
+
+ let scrollingInterval = window.setInterval(function() {
+ frames.scrollByIndex(-1);
+ }, 100);
+ }}, 0);
+ });
+
+ gDebuggee.recurse();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack-04.js b/browser/devtools/debugger/test/browser_dbg_stack-04.js
new file mode 100644
index 000000000..e68aaea8c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack-04.js
@@ -0,0 +1,65 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testEvalCallResume();
+ });
+}
+
+function testEvalCallResume() {
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 2,
+ "Should have two frames.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+
+ gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
+ gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 0,
+ "Should have no frames after resume");
+
+ is(childNodes.length, 0,
+ "Should only have no children.");
+
+ closeDebuggerAndFinish();
+ }, true);
+
+ gDebugger.DebuggerController.activeThread.resume();
+ }}, 0);
+ });
+
+ gDebuggee.evalCall();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack-05.js b/browser/devtools/debugger/test/browser_dbg_stack-05.js
new file mode 100644
index 000000000..1fef044e0
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack-05.js
@@ -0,0 +1,114 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * 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 + "browser_dbg_script-switching.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test() {
+ let scriptShown = false;
+ let framesAdded = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("-02.js") != -1) {
+ scriptShown = true;
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ runTest();
+ }
+ });
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ runTest();
+ });
+
+ gDebuggee.firstCall();
+ });
+
+ function runTest()
+ {
+ if (scriptShown && framesAdded) {
+ Services.tm.currentThread.dispatch({ run: testRecurse }, 0);
+ }
+ }
+}
+
+function testRecurse()
+{
+ let frames = gDebugger.DebuggerView.StackFrames.widget._list;
+ let childNodes = frames.childNodes;
+
+ is(frames.querySelectorAll(".dbg-stackframe").length, 4,
+ "Correct number of frames.");
+
+ is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
+ "All children should be frames.");
+
+ ok(frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should be selected by default.");
+
+ ok(!frames.querySelector("#stackframe-2").parentNode.hasAttribute("checked"),
+ "Third frame should not be selected.");
+
+ is(gDebugger.editor.getDebugLocation(), 5,
+ "editor debugger location is correct.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.querySelector("#stackframe-2"),
+ gDebugger);
+
+ ok(!frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should not be selected after click.");
+
+ ok(frames.querySelector("#stackframe-2").parentNode.hasAttribute("checked"),
+ "Third frame should be selected after click.");
+
+ is(gDebugger.editor.getDebugLocation(), 4,
+ "editor debugger location is correct after click.");
+
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ frames.querySelector("#stackframe-0 .dbg-stackframe-title"),
+ gDebugger);
+
+ ok(frames.querySelector("#stackframe-0").parentNode.hasAttribute("checked"),
+ "First frame should be selected after click inside the first frame.");
+
+ ok(!frames.querySelector("#stackframe-2").parentNode.hasAttribute("checked"),
+ "Third frame should not be selected after click inside the first frame.");
+
+ is(gDebugger.editor.getDebugLocation(), 5,
+ "editor debugger location is correct (frame 0 again).");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ is(gDebugger.editor.getDebugLocation(), -1,
+ "editor debugger location is correct after resume.");
+
+ closeDebuggerAndFinish();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_stack.html b/browser/devtools/debugger/test/browser_dbg_stack.html
new file mode 100644
index 000000000..c3d083087
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_stack.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset='utf-8'/><title>Browser Debugger Test Tab</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<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>
+</head>
+
+<body></body>
+
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_step-out.js b/browser/devtools/debugger/test/browser_dbg_step-out.js
new file mode 100644
index 000000000..37df052b5
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_step-out.js
@@ -0,0 +1,132 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 + "test-step-out.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+
+function test()
+{
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+
+ testNormalReturn();
+ });
+}
+
+function testNormalReturn()
+{
+ gPane.panelWin.gClient.addOneTimeListener("paused", function() {
+ gDebugger.addEventListener("Debugger:SourceShown", function dbgstmt(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, dbgstmt);
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should be paused now.");
+
+ let count = 0;
+ gPane.panelWin.gClient.addOneTimeListener("paused", function() {
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should be paused again.");
+
+ gDebugger.addEventListener("Debugger:FetchedVariables", function stepout() {
+ ok(true, "Debugger:FetchedVariables event received.");
+ gDebugger.removeEventListener("Debugger:FetchedVariables", stepout, false);
+
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.firstChild,
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<return>",
+ "Should have the right property name for the return value.");
+
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), 10,
+ "Should have the right property value for the return value.");
+
+ testReturnWithException();
+ }}, 0);
+ }, false);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("step-out"),
+ gDebugger);
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.getElementById("return"),
+ content.window);
+}
+
+function testReturnWithException()
+{
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ gPane.panelWin.gClient.addOneTimeListener("paused", function() {
+ gDebugger.addEventListener("Debugger:FetchedVariables", function dbgstmt(aEvent) {
+ gDebugger.removeEventListener(aEvent.type, dbgstmt, false);
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should be paused now.");
+
+ let count = 0;
+ gPane.panelWin.gClient.addOneTimeListener("paused", function() {
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should be paused again.");
+
+ gDebugger.addEventListener("Debugger:FetchedVariables", function stepout() {
+ ok(true, "Debugger:FetchedVariables event received.");
+ gDebugger.removeEventListener("Debugger:FetchedVariables", stepout, false);
+
+ Services.tm.currentThread.dispatch({ run: function() {
+
+ var scopes = gDebugger.DebuggerView.Variables._list,
+ innerScope = scopes.firstChild,
+ innerNodes = innerScope.querySelector(".variables-view-element-details").childNodes;
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+ "Should have the right property name for the exception value.");
+
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), '"boom"',
+ "Should have the right property value for the exception value.");
+
+ resumeAndFinish();
+
+ }}, 0);
+ }, false);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("step-out"),
+ gDebugger);
+ }, false);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ content.document.getElementById("throw"),
+ content.window);
+ });
+}
+
+function resumeAndFinish() {
+ gPane.panelWin.gClient.addOneTimeListener("resumed", function() {
+ Services.tm.currentThread.dispatch({ run: closeDebuggerAndFinish }, 0);
+ });
+
+ gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebugger = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_tab1.html b/browser/devtools/debugger/test/browser_dbg_tab1.html
new file mode 100644
index 000000000..67d81f915
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tab1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Test Tab</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_tab2.html b/browser/devtools/debugger/test/browser_dbg_tab2.html
new file mode 100644
index 000000000..30fcccf74
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tab2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Test Tab 2</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_tabactor-01.js b/browser/devtools/debugger/test/browser_dbg_tabactor-01.js
new file mode 100644
index 000000000..8175d364c
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tabactor-01.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check extension-added tab actor lifetimes.
+ */
+
+var gTab1 = null;
+var gTab1Actor = null;
+
+var gClient = null;
+
+function test()
+{
+ DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function (aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ get_tab();
+ });
+}
+
+function get_tab()
+{
+ gTab1 = addTab(TAB1_URL, function() {
+ attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
+ gTab1Actor = aGrip.actor;
+ ok(aGrip.testTabActor1, "Found the test tab actor.")
+ ok(aGrip.testTabActor1.indexOf("testone") >= 0,
+ "testTabActor's actorPrefix should be used.");
+ gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
+ is(aResponse.pong, "pong", "Actor should respond to requests.");
+ finish_test();
+ });
+ });
+ });
+}
+
+function finish_test()
+{
+ gClient.close(function() {
+ removeTab(gTab1);
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_tabactor-02.js b/browser/devtools/debugger/test/browser_dbg_tabactor-02.js
new file mode 100644
index 000000000..492f47395
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tabactor-02.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check extension-added tab actor lifetimes.
+ */
+
+var gTab1 = null;
+var gTab1Actor = null;
+
+var gClient = null;
+
+function test()
+{
+ DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
+
+ let transport = DebuggerServer.connectPipe();
+ gClient = new DebuggerClient(transport);
+ gClient.connect(function (aType, aTraits) {
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+ get_tab();
+ });
+}
+
+function get_tab()
+{
+ gTab1 = addTab(TAB1_URL, function() {
+ attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
+ gTab1Actor = aGrip.actor;
+ ok(aGrip.testTabActor1, "Found the test tab actor.")
+ ok(aGrip.testTabActor1.indexOf("testone") >= 0,
+ "testTabActor's actorPrefix should be used.");
+ gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
+ is(aResponse.pong, "pong", "Actor should respond to requests.");
+ close_tab(aResponse.actor);
+ });
+ });
+ });
+}
+
+function close_tab(aTestActor)
+{
+ removeTab(gTab1);
+ try {
+ gClient.request({ to: aTestActor, type: "ping" }, function (aResponse) {
+ is(aResponse, undefined, "testTabActor1 didn't go away with the tab.");
+ finish_test();
+ });
+ } catch (e) {
+ is(e.message, "'ping' request packet has no destination.",
+ "testTabActor1 should have gone away with the tab.");
+ finish_test();
+ }
+}
+
+function finish_test()
+{
+ gClient.close(function () {
+ finish();
+ });
+}
diff --git a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.html b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.html
new file mode 100644
index 000000000..2cb5d3f50
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Update Editor Mode Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript" src="test-script-switching-01.js?q=a"></script>
+ <script type="text/javascript" src="test-editor-mode?a=b"></script>
+ <script type="text/javascript">
+ function banana() { }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
new file mode 100644
index 000000000..785994ee7
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
@@ -0,0 +1,145 @@
+/* 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 script URIs with extra query parameters also get the right engine.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_update-editor-mode.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gSources = null;
+
+function test()
+{
+ let scriptShown = false;
+ let framesAdded = false;
+ let testStarted = false;
+ let resumed = false;
+
+ debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+ gTab = aTab;
+ gDebuggee = aDebuggee;
+ gPane = aPane;
+ gDebugger = gPane.panelWin;
+ gSources = gDebugger.DebuggerView.Sources;
+ resumed = true;
+
+ gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
+ gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+ framesAdded = true;
+ executeSoon(startTest);
+ });
+
+ executeSoon(function() {
+ gDebuggee.firstCall();
+ });
+ });
+
+ function onScriptShown(aEvent) {
+ scriptShown = aEvent.detail.url.indexOf("test-editor-mode") != -1;
+ executeSoon(startTest);
+ }
+
+ function startTest()
+ {
+ if (scriptShown && framesAdded && resumed && !testStarted) {
+ gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+ testStarted = true;
+ Services.tm.currentThread.dispatch({ run: testScriptsDisplay }, 0);
+ }
+ }
+}
+
+function testScriptsDisplay() {
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 3,
+ "Found the expected number of scripts.");
+
+ is(gDebugger.editor.getMode(), SourceEditor.MODES.TEXT,
+ "Found the expected editor mode.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) != -1,
+ "The correct script was loaded initially.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("switching-01.js") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitchPaused1();
+ }
+ });
+
+ let url = gDebuggee.document.querySelector("script").src;
+ gDebugger.DebuggerView.Sources.selectedValue = url;
+}
+
+function testSwitchPaused1()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 3,
+ "Found the expected number of scripts.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) == -1,
+ "The second script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) != -1,
+ "The first script is displayed.");
+
+ is(gDebugger.editor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+ "Found the expected editor mode.");
+
+ gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+ let url = aEvent.detail.url;
+ if (url.indexOf("update-editor-mode") != -1) {
+ gDebugger.removeEventListener(aEvent.type, _onEvent);
+ testSwitchPaused2();
+ }
+ });
+
+ let label = "browser_dbg_update-editor-mode.html";
+ gDebugger.DebuggerView.Sources.selectedLabel = label;
+}
+
+function testSwitchPaused2()
+{
+ is(gDebugger.DebuggerController.activeThread.state, "paused",
+ "Should only be getting stack frames while paused.");
+
+ is(gSources.itemCount, 3,
+ "Found the expected number of scripts.");
+
+ ok(gDebugger.editor.getText().search(/firstCall/) == -1,
+ "The first script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/debugger/) == -1,
+ "The second script is no longer displayed.");
+
+ ok(gDebugger.editor.getText().search(/banana/) != -1,
+ "The third script is displayed.");
+
+ is(gDebugger.editor.getMode(), SourceEditor.MODES.HTML,
+ "Found the expected editor mode.");
+
+ gDebugger.DebuggerController.activeThread.resume(function() {
+ closeDebuggerAndFinish();
+ });
+}
+
+registerCleanupFunction(function() {
+ removeTab(gTab);
+ gPane = null;
+ gTab = null;
+ gDebuggee = null;
+ gDebugger = null;
+ gSources = null;
+});
diff --git a/browser/devtools/debugger/test/browser_dbg_watch-expressions.html b/browser/devtools/debugger/test/browser_dbg_watch-expressions.html
new file mode 100644
index 000000000..842ac7d48
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_watch-expressions.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Browser Debugger Watch Expressions Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <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>
+
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/browser_dbg_with-frame.html b/browser/devtools/debugger/test/browser_dbg_with-frame.html
new file mode 100644
index 000000000..59a2397c3
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_with-frame.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Function Call Parameter Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ function test(aNumber) {
+ var a, obj = { one: 1, two: 2 };
+ var r = aNumber;
+ with (Math) {
+ a = PI * r * r;
+ with (obj) {
+ var foo = two * PI;
+ debugger;
+ }
+ }
+ };
+ function load() {
+ test(10);
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", load, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js
new file mode 100644
index 000000000..3c4cf47af
--- /dev/null
+++ b/browser/devtools/debugger/test/head.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+let tempScope = {};
+Cu.import("resource://gre/modules/Services.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+let Services = tempScope.Services;
+let SourceEditor = tempScope.SourceEditor;
+let DebuggerServer = tempScope.DebuggerServer;
+let DebuggerTransport = tempScope.DebuggerTransport;
+let DebuggerClient = tempScope.DebuggerClient;
+let gDevTools = tempScope.gDevTools;
+let devtools = tempScope.devtools;
+let TargetFactory = devtools.TargetFactory;
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/";
+const TAB1_URL = EXAMPLE_URL + "browser_dbg_tab1.html";
+const TAB2_URL = EXAMPLE_URL + "browser_dbg_tab2.html";
+const STACK_URL = EXAMPLE_URL + "browser_dbg_stack.html";
+
+// Enable logging and remote debugging for the relevant tests.
+let gEnableRemote = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+registerCleanupFunction(function() {
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote);
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+
+ // Properly shut down the server to avoid memory leaks.
+ DebuggerServer.destroy();
+});
+
+if (!DebuggerServer.initialized) {
+ DebuggerServer.init(function() true);
+ DebuggerServer.addBrowserActors();
+}
+
+waitForExplicitFinish();
+
+function addWindow() {
+ let windowReference = window.open();
+ let chromeWindow = windowReference
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+
+ return chromeWindow;
+}
+
+function addTab(aURL, aOnload, aWindow) {
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ targetBrowser.selectedTab = targetBrowser.addTab(aURL);
+
+ let tab = targetBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ let win = browser.contentWindow;
+ let expectedReadyState = aURL == "about:blank" ? ["interactive", "complete"] : ["complete"];
+
+ if (aOnload) {
+ let handler = function() {
+ if (browser.currentURI.spec != aURL ||
+ expectedReadyState.indexOf((win.document || {}).readyState) == -1) {
+ return;
+ }
+ browser.removeEventListener("load", handler, true);
+ executeSoon(aOnload);
+ }
+ browser.addEventListener("load", handler, true);
+ }
+
+ return tab;
+}
+
+function removeTab(aTab, aWindow) {
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetBrowser.removeTab(aTab);
+}
+
+function closeDebuggerAndFinish(aRemoteFlag, aCallback, aWindow) {
+ let debuggerClosed = false;
+ let debuggerDisconnected = false;
+
+ ok(gTab, "There is a gTab to use for getting a toolbox reference");
+ let target = TargetFactory.forTab(gTab);
+
+ window.addEventListener("Debugger:Shutdown", function cleanup() {
+ window.removeEventListener("Debugger:Shutdown", cleanup, false);
+ debuggerDisconnected = true;
+ maybeFinish();
+ }, false);
+
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.destroy().then(function() {
+ debuggerClosed = true;
+ maybeFinish();
+ });
+
+ function maybeFinish() {
+ if (debuggerClosed && debuggerDisconnected) {
+ (finish || aCallback)();
+ }
+ }
+}
+
+function get_tab_actor_for_url(aClient, aURL, aCallback) {
+ aClient.listTabs(function(aResponse) {
+ for each (let tab in aResponse.tabs) {
+ if (tab.url == aURL) {
+ aCallback(tab);
+ return;
+ }
+ }
+ });
+}
+
+function attach_tab_actor_for_url(aClient, aURL, aCallback) {
+ get_tab_actor_for_url(aClient, aURL, function(actor) {
+ aClient.attachTab(actor.actor, function(aResponse) {
+ aCallback(actor, aResponse);
+ });
+ });
+}
+
+function attach_thread_actor_for_url(aClient, aURL, aCallback) {
+ attach_tab_actor_for_url(aClient, aURL, function(aTabActor, aResponse) {
+ aClient.attachThread(actor.threadActor, function(aResponse, aThreadClient) {
+ // We don't care about the pause right now (use
+ // get_actor_for_url() if you do), so resume it.
+ aThreadClient.resume(function(aResponse) {
+ aCallback(actor);
+ });
+ });
+ });
+}
+
+function wait_for_connect_and_resume(aOnDebugging, aTab) {
+ let target = TargetFactory.forTab(aTab);
+
+ gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+
+ // Wait for the initial resume...
+ dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
+ aOnDebugging();
+ });
+ });
+}
+
+function debug_tab_pane(aURL, aOnDebugging, aBeforeTabAdded) {
+ // Make any necessary preparations (start the debugger server etc.)
+ if (aBeforeTabAdded) {
+ aBeforeTabAdded();
+ }
+
+ let tab = addTab(aURL, function() {
+ let debuggee = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ info("Opening Debugger");
+ gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+
+ // Wait for the initial resume...
+ dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
+ info("Debugger has started");
+ dbg._view.Variables.lazyEmpty = false;
+ dbg._view.Variables.lazyAppend = false;
+ dbg._view.Variables.lazyExpand = false;
+ aOnDebugging(tab, debuggee, dbg);
+ });
+ });
+ });
+}
+
+function debug_chrome(aURL, aOnClosing, aOnDebugging) {
+ let tab = addTab(aURL, function() {
+ let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
+
+ info("Opening Browser Debugger");
+ let win = BrowserDebuggerProcess.init(aOnClosing, function(process) {
+
+ // The remote debugging process has started...
+ info("Browser Debugger has started");
+ aOnDebugging(tab, debuggee, process);
+ });
+ });
+}
diff --git a/browser/devtools/debugger/test/moz.build b/browser/devtools/debugger/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/debugger/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/debugger/test/test-editor-mode b/browser/devtools/debugger/test/test-editor-mode
new file mode 100644
index 000000000..a86191c98
--- /dev/null
+++ b/browser/devtools/debugger/test/test-editor-mode
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function secondCall() {
+ eval("debugger;");
+}
diff --git a/browser/devtools/debugger/test/test-function-search-01.js b/browser/devtools/debugger/test/test-function-search-01.js
new file mode 100644
index 000000000..b5d647cfe
--- /dev/null
+++ b/browser/devtools/debugger/test/test-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/browser/devtools/debugger/test/test-function-search-02.js b/browser/devtools/debugger/test/test-function-search-02.js
new file mode 100644
index 000000000..10b48518f
--- /dev/null
+++ b/browser/devtools/debugger/test/test-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/browser/devtools/debugger/test/test-function-search-03.js b/browser/devtools/debugger/test/test-function-search-03.js
new file mode 100644
index 000000000..e64292a92
--- /dev/null
+++ b/browser/devtools/debugger/test/test-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/browser/devtools/debugger/test/test-location-changes-bp.html b/browser/devtools/debugger/test/test-location-changes-bp.html
new file mode 100644
index 000000000..0e1831894
--- /dev/null
+++ b/browser/devtools/debugger/test/test-location-changes-bp.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset='utf-8'/>
+<script type="text/javascript" src="test-location-changes-bp.js"></script>
+<script type="text/javascript">
+function runDebuggerStatement() {
+ debugger;
+}
+</script>
+</head>
+<body>
+
+<button type="button" onclick="myFunction()">Run</button>
+
+</body>
+</html>
diff --git a/browser/devtools/debugger/test/test-location-changes-bp.js b/browser/devtools/debugger/test/test-location-changes-bp.js
new file mode 100644
index 000000000..d164b8bdf
--- /dev/null
+++ b/browser/devtools/debugger/test/test-location-changes-bp.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/browser/devtools/debugger/test/test-script-switching-01.js b/browser/devtools/debugger/test/test-script-switching-01.js
new file mode 100644
index 000000000..a4c078032
--- /dev/null
+++ b/browser/devtools/debugger/test/test-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() {
+ eval("secondCall();");
+}
diff --git a/browser/devtools/debugger/test/test-script-switching-02.js b/browser/devtools/debugger/test/test-script-switching-02.js
new file mode 100644
index 000000000..6606bd366
--- /dev/null
+++ b/browser/devtools/debugger/test/test-script-switching-02.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function secondCall() {
+ // This comment is useful for browser_dbg_select-line.js. ☺
+ eval("debugger;");
+ function foo() {}
+ if (true) {
+ foo();
+ }
+}
diff --git a/browser/devtools/debugger/test/test-step-out.html b/browser/devtools/debugger/test/test-step-out.html
new file mode 100644
index 000000000..c19236772
--- /dev/null
+++ b/browser/devtools/debugger/test/test-step-out.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Debugger Step Out Return Value Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <button id="return">return</button>
+ <button id="throw">throw</button>
+ </body>
+ <script type="text/javascript">
+ window.addEventListener("load", function() {
+ 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 button = document.getElementById("return");
+ button.addEventListener("click", normal, false);
+ button = document.getElementById("throw");
+ button.addEventListener("click", error, false);
+ });
+ </script>
+</html>
diff --git a/browser/devtools/debugger/test/testactors.js b/browser/devtools/debugger/test/testactors.js
new file mode 100644
index 000000000..8c97b3e91
--- /dev/null
+++ b/browser/devtools/debugger/test/testactors.js
@@ -0,0 +1,31 @@
+/* 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: "testone",
+
+ 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/browser/devtools/fontinspector/font-inspector.css b/browser/devtools/fontinspector/font-inspector.css
new file mode 100644
index 000000000..3ad5e5437
--- /dev/null
+++ b/browser/devtools/fontinspector/font-inspector.css
@@ -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/. */
+
+.dim > #root,
+.font:not(.has-code) .font-css-code,
+.font-is-local,
+.font-is-remote,
+.font.is-local .font-format-url {
+ display: none;
+}
+
+.font.is-remote .font-is-remote,
+.font.is-local .font-is-local {
+ display: inline;
+}
diff --git a/browser/devtools/fontinspector/font-inspector.js b/browser/devtools/fontinspector/font-inspector.js
new file mode 100644
index 000000000..9983db21f
--- /dev/null
+++ b/browser/devtools/fontinspector/font-inspector.js
@@ -0,0 +1,231 @@
+/* -*- Mode: C++; tab-width: 8; 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+function FontInspector(inspector, window)
+{
+ this.inspector = inspector;
+ this.chromeDoc = window.document;
+ this.init();
+}
+
+FontInspector.prototype = {
+ init: function FI_init() {
+ this.update = this.update.bind(this);
+ this.onNewNode = this.onNewNode.bind(this);
+ this.onHighlighterLocked = this.onHighlighterLocked.bind(this);
+ this.inspector.selection.on("new-node", this.onNewNode);
+ this.inspector.sidebar.on("fontinspector-selected", this.onNewNode);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.on("locked", this.onHighlighterLocked);
+ }
+ this.update();
+ },
+
+ /**
+ * Is the fontinspector visible in the sidebar?
+ */
+ isActive: function FI_isActive() {
+ return this.inspector.sidebar &&
+ this.inspector.sidebar.getCurrentTabID() == "fontinspector";
+ },
+
+ /**
+ * Remove listeners.
+ */
+ destroy: function FI_destroy() {
+ this.chromeDoc = null;
+ this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
+ this.inspector.selection.off("new-node", this.onNewNode);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.off("locked", this.onHighlighterLocked);
+ }
+ },
+
+ /**
+ * Selection 'new-node' event handler.
+ */
+ onNewNode: function FI_onNewNode() {
+ if (this.isActive() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode() &&
+ this.inspector.selection.reason != "highlighter") {
+ this.undim();
+ this.update();
+ } else {
+ this.dim();
+ }
+ },
+
+ /**
+ * Highlighter 'locked' event handler
+ */
+ onHighlighterLocked: function FI_onHighlighterLocked() {
+ this.undim();
+ this.update();
+ },
+
+ /**
+ * Hide the font list. No node are selected.
+ */
+ dim: function FI_dim() {
+ this.chromeDoc.body.classList.add("dim");
+ this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
+ },
+
+ /**
+ * Show the font list. A node is selected.
+ */
+ undim: function FI_undim() {
+ this.chromeDoc.body.classList.remove("dim");
+ },
+
+ /**
+ * Retrieve all the font related info we have for the selected
+ * node and display them.
+ */
+ update: function FI_update() {
+ if (!this.isActive() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode() ||
+ this.chromeDoc.body.classList.contains("dim")) {
+ return;
+ }
+
+ let node = this.inspector.selection.node;
+ let contentDocument = node.ownerDocument;
+
+ // We don't get fonts for a node, but for a range
+ let rng = contentDocument.createRange();
+ rng.selectNode(node);
+ let fonts = DOMUtils.getUsedFontFaces(rng);
+ let fontsArray = [];
+ for (let i = 0; i < fonts.length; i++) {
+ fontsArray.push(fonts.item(i));
+ }
+ fontsArray = fontsArray.sort(function(a, b) {
+ return a.srcIndex < b.srcIndex;
+ });
+ this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
+ for (let f of fontsArray) {
+ this.render(f, contentDocument);
+ }
+ },
+
+ /**
+ * Display the information of one font.
+ */
+ render: function FI_render(font, document) {
+ let s = this.chromeDoc.querySelector("#template > section");
+ s = s.cloneNode(true);
+
+ s.querySelector(".font-name").textContent = font.name;
+ s.querySelector(".font-css-name").textContent = font.CSSFamilyName;
+ s.querySelector(".font-format").textContent = font.format;
+
+ if (font.srcIndex == -1) {
+ s.classList.add("is-local");
+ } else {
+ s.classList.add("is-remote");
+ }
+
+ s.querySelector(".font-url").value = font.URI;
+
+ let iframe = s.querySelector(".font-preview");
+ if (font.rule) {
+ // This is the @font-face{…} code.
+ let cssText = font.rule.style.parentRule.cssText;
+
+ s.classList.add("has-code");
+ s.querySelector(".font-css-code").textContent = cssText;
+
+ // We guess the base URL of the stylesheet to make
+ // sure the font will be accessible in the preview.
+ // If the font-face is in an inline <style>, we get
+ // the location of the page.
+ let origin = font.rule.style.parentRule.parentStyleSheet.href;
+ if (!origin) { // Inline stylesheet
+ origin = document.location.href;
+ }
+ // We remove the last part of the URL to get a correct base.
+ let base = origin.replace(/\/[^\/]*$/,"/")
+
+ // From all this information, we build a preview.
+ this.buildPreview(iframe, font.CSSFamilyName, cssText, base);
+ } else {
+ this.buildPreview(iframe, font.CSSFamilyName, "", "");
+ }
+
+ this.chromeDoc.querySelector("#all-fonts").appendChild(s);
+ },
+
+ /**
+ * Show a preview of the font in an iframe.
+ */
+ buildPreview: function FI_buildPreview(iframe, name, cssCode, base) {
+ /* The HTML code of the preview is:
+ * <!DOCTYPE HTML>
+ * <head>
+ * <base href="{base}"></base>
+ * </head>
+ * <style>
+ * p {font-family: {name};}
+ * * {font-size: 40px;line-height:60px;padding:0 10px;margin:0};
+ * </style>
+ * <p contenteditable>Abc</p>
+ */
+ let extraCSS = "* {padding:0;margin:0}";
+ extraCSS += ".theme-dark {color: white}";
+ extraCSS += "p {font-size: 40px;line-height:60px;padding:0 10px;margin:0;}";
+ cssCode += extraCSS;
+ let src = "data:text/html;charset=utf-8,<!DOCTYPE HTML><head><base></base></head><style></style><p contenteditable>Abc</p>";
+ iframe.addEventListener("load", function onload() {
+ iframe.removeEventListener("load", onload, true);
+ let doc = iframe.contentWindow.document;
+ // We could have done that earlier, but we want to avoid any URL-encoding
+ // nightmare.
+ doc.querySelector("base").href = base;
+ doc.querySelector("style").textContent = cssCode;
+ doc.querySelector("p").style.fontFamily = name;
+ // Forward theme
+ doc.documentElement.className = document.documentElement.className;
+ }, true);
+ iframe.src = src;
+ },
+
+ /**
+ * Select the <body> to show all the fonts included in the document.
+ */
+ showAll: function FI_showAll() {
+ if (!this.isActive() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ return;
+ }
+ let node = this.inspector.selection.node;
+ let contentDocument = node.ownerDocument;
+ let root = contentDocument.documentElement;
+ if (contentDocument.body) {
+ root = contentDocument.body;
+ }
+ this.inspector.selection.setNode(root, "fontinspector");
+ },
+}
+
+
+window.setPanel = function(panel) {
+ window.fontInspector = new FontInspector(panel, window);
+}
+
+window.onunload = function() {
+ if (window.fontInspector) {
+ window.fontInspector.destroy();
+ }
+}
diff --git a/browser/devtools/fontinspector/font-inspector.xhtml b/browser/devtools/fontinspector/font-inspector.xhtml
new file mode 100644
index 000000000..19dd2061c
--- /dev/null
+++ b/browser/devtools/fontinspector/font-inspector.xhtml
@@ -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/. -->
+
+<!DOCTYPE html [
+<!ENTITY % fontinspectorDTD SYSTEM "chrome://browser/locale/devtools/font-inspector.dtd" >
+ %fontinspectorDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&title;</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="font-inspector.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/font-inspector.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
+ </head>
+ <body class="theme-body devtools-monospace" role="application">
+ <script type="application/javascript;version=1.8" src="font-inspector.js"></script>
+ <div id="root">
+ <ul id="all-fonts"></ul>
+ <button id="showall" onclick="fontInspector.showAll()">&showAllFonts;</button>
+ </div>
+ <div id="template" style="display:none">
+ <section class="font">
+ <iframe sandbox="" class="font-preview"></iframe>
+ <div class="font-info">
+ <h1 class="font-name"></h1>
+ <span class="font-is-local">&system;</span>
+ <span class="font-is-remote">&remote;</span>
+ <p class="font-format-url">
+ <input readonly="readonly" class="font-url"></input>
+ (<span class="font-format"></span>)
+ </p>
+ <p class="font-css">&usedAs; "<span class="font-css-name"></span>"</p>
+ <pre class="font-css-code"></pre>
+ </div>
+ </section>
+ </div>
+ </body>
+</html>
diff --git a/browser/devtools/fontinspector/moz.build b/browser/devtools/fontinspector/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/fontinspector/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/fontinspector/test/Makefile.in b/browser/devtools/fontinspector/test/Makefile.in
new file mode 100644
index 000000000..4e1e45775
--- /dev/null
+++ b/browser/devtools/fontinspector/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_fontinspector.js \
+ browser_fontinspector.html \
+ browser_font.woff \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/fontinspector/test/browser_font.woff b/browser/devtools/fontinspector/test/browser_font.woff
new file mode 100644
index 000000000..e8440843b
--- /dev/null
+++ b/browser/devtools/fontinspector/test/browser_font.woff
Binary files differ
diff --git a/browser/devtools/fontinspector/test/browser_fontinspector.html b/browser/devtools/fontinspector/test/browser_fontinspector.html
new file mode 100644
index 000000000..13d7a2e4b
--- /dev/null
+++ b/browser/devtools/fontinspector/test/browser_fontinspector.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+
+<style>
+ @font-face {
+ font-family: bar;
+ src: url(bad/font/name.ttf), url(browser_font.woff) format("woff");
+ }
+ body{
+ font-family:Arial;
+ }
+ div {
+ font-family:Arial;
+ font-family:bar;
+ }
+</style>
+
+<body>
+ BODY
+ <div>DIV</div>
+</body>
diff --git a/browser/devtools/fontinspector/test/browser_fontinspector.js b/browser/devtools/fontinspector/test/browser_fontinspector.js
new file mode 100644
index 000000000..e57fe6093
--- /dev/null
+++ b/browser/devtools/fontinspector/test/browser_fontinspector.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+function test() {
+ waitForExplicitFinish();
+
+ let doc;
+ let node;
+ let view;
+ let inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/fontinspector/test/browser_fontinspector.html";
+
+ function setupTest() {
+ let rng = doc.createRange();
+ rng.selectNode(doc.body);
+ let fonts = DOMUtils.getUsedFontFaces(rng);
+ if (fonts.length != 2) {
+ // Fonts are not loaded yet.
+ // Let try again in a couple of milliseconds (hacky, but
+ // there's not better way to do it. See bug 835247).
+ setTimeout(setupTest, 500);
+ } else {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ openFontInspector(toolbox.getCurrentPanel());
+ });
+ }
+ }
+
+ function openFontInspector(aInspector) {
+ inspector = aInspector;
+
+ info("Inspector open");
+
+ inspector.selection.setNode(doc.body);
+ inspector.sidebar.select("fontinspector");
+ inspector.sidebar.once("fontinspector-ready", viewReady);
+ }
+
+ function viewReady() {
+ info("Font Inspector ready");
+
+ view = inspector.sidebar.getWindowForTab("fontinspector");
+
+ ok(!!view.fontInspector, "Font inspector document is alive.");
+
+ let d = view.document;
+
+ let s = d.querySelectorAll("#all-fonts > section");
+ is(s.length, 2, "Found 2 fonts");
+
+ is(s[0].querySelector(".font-name").textContent,
+ "DeLarge Bold", "font 0: Right font name");
+ ok(s[0].classList.contains("is-remote"),
+ "font 0: is remote");
+ is(s[0].querySelector(".font-url").value,
+ "http://mochi.test:8888/browser/browser/devtools/fontinspector/test/browser_font.woff",
+ "font 0: right url");
+ is(s[0].querySelector(".font-format").textContent,
+ "woff", "font 0: right font format");
+ is(s[0].querySelector(".font-css-name").textContent,
+ "bar", "font 0: right css name");
+
+
+ let font1Name = s[1].querySelector(".font-name").textContent;
+ let font1CssName = s[1].querySelector(".font-css-name").textContent;
+
+ // On Linux test machines, the Arial font doesn't exist.
+ // The fallback is "Liberation Sans"
+
+ ok((font1Name == "Arial") || (font1Name == "Liberation Sans"),
+ "font 1: Right font name");
+ ok(s[1].classList.contains("is-local"), "font 1: is local");
+ ok((font1CssName == "Arial") || (font1CssName == "Liberation Sans"),
+ "Arial", "font 1: right css name");
+
+ executeSoon(function() {
+ gDevTools.once("toolbox-destroyed", finishUp);
+ inspector._toolbox.destroy();
+ });
+ }
+
+ function finishUp() {
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/fontinspector/test/moz.build b/browser/devtools/fontinspector/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/fontinspector/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/framework/Makefile.in b/browser/devtools/framework/Makefile.in
new file mode 100644
index 000000000..5ea7a0c96
--- /dev/null
+++ b/browser/devtools/framework/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/framework
diff --git a/browser/devtools/framework/connect/connect.css b/browser/devtools/framework/connect/connect.css
new file mode 100644
index 000000000..93dfbd8cd
--- /dev/null
+++ b/browser/devtools/framework/connect/connect.css
@@ -0,0 +1,109 @@
+:root {
+ font: caption;
+}
+
+html {
+ background-color: #111;
+ background-image: url("chrome://browser/skin/newtab/noise.png");
+}
+
+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);
+}
+
+label {
+ display: block;
+ margin: 10px;
+}
+
+label > span {
+ display: inline-block;
+ min-width: 150px;
+ text-align: right;
+ margin-right: 10px;
+}
+
+#submit {
+ margin-left: 160px;
+}
+
+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/browser/devtools/framework/connect/connect.js b/browser/devtools/framework/connect/connect.js
new file mode 100644
index 000000000..30e1ee05b
--- /dev/null
+++ b/browser/devtools/framework/connect/connect.js
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; 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 Cu = Components.utils;
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+let gClient;
+let gConnectionTimeout;
+
+XPCOMUtils.defineLazyGetter(window, 'l10n', function () {
+ return Services.strings.createBundle('chrome://browser/locale/devtools/connection-screen.properties');
+});
+
+/**
+ * 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;
+ }
+
+}, true);
+
+/**
+ * Called when the "connect" button is clicked.
+ */
+function submit() {
+ // Show the "connecting" screen
+ document.body.classList.add("connecting");
+
+ // Save the host/port values
+ let host = document.getElementById("host").value;
+ Services.prefs.setCharPref("devtools.debugger.remote-host", host);
+
+ let port = document.getElementById("port").value;
+ Services.prefs.setIntPref("devtools.debugger.remote-port", port);
+
+ // Initiate the connection
+ let transport = debuggerSocketConnect(host, port);
+ gClient = new DebuggerClient(transport);
+ let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
+ gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
+ gClient.connect(onConnectionReady);
+}
+
+/**
+ * Connection is ready. List actors and build buttons.
+ */
+function onConnectionReady(aType, aTraits) {
+ clearTimeout(gConnectionTimeout);
+ gClient.listTabs(function(aResponse) {
+ document.body.classList.remove("connecting");
+ document.body.classList.add("actors-mode");
+
+ let parent = document.getElementById("tabActors");
+
+ // Add Global Process debugging...
+ let globals = JSON.parse(JSON.stringify(aResponse));
+ 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 < aResponse.tabs.length; i++) {
+ buildLink(aResponse.tabs[i], parent, i == aResponse.selected);
+ }
+
+ let gParent = document.getElementById("globalActors");
+
+ // Build the Remote Process button
+ if (Object.keys(globals).length > 1) {
+ let a = document.createElement("a");
+ a.onclick = function() {
+ openToolbox(globals, true);
+
+ }
+ a.title = a.textContent = window.l10n.GetStringFromName("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);
+ }
+
+ // Ensure the first link is focused
+ let firstLink = parent.querySelector("a:first-of-type");
+ if (firstLink) {
+ firstLink.focus();
+ }
+
+ });
+}
+
+/**
+ * Build one button for an actor.
+ */
+function buildLink(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) {
+ let options = {
+ form: form,
+ client: gClient,
+ chrome: chrome
+ };
+ devtools.TargetFactory.forRemoteTab(options).then((target) => {
+ gDevTools.showToolbox(target, "webconsole", devtools.Toolbox.HostType.WINDOW);
+ window.close();
+ });
+}
diff --git a/browser/devtools/framework/connect/connect.xhtml b/browser/devtools/framework/connect/connect.xhtml
new file mode 100644
index 000000000..25ffabed6
--- /dev/null
+++ b/browser/devtools/framework/connect/connect.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 % connectionDTD SYSTEM "chrome://browser/locale/devtools/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://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/content/devtools/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" onsubmit="window.submit()" 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" 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>&availableProcesses;</p>
+ <ul class="actors" id="globalActors"></ul>
+ </section>
+ <section id="connecting">
+ <p><img src="chrome://browser/skin/tabbrowser/loading.png"></img> &connecting;</p>
+ </section>
+ <footer>&help;</footer>
+ </body>
+</html>
diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm
new file mode 100644
index 000000000..1de3dbaf3
--- /dev/null
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -0,0 +1,748 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If 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 = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource:///modules/devtools/ProfilerController.jsm");
+
+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._toolboxes = new Map(); // Map<target, toolbox>
+
+ // destroy() is an observer's handler so we need to preserve context.
+ this.destroy = this.destroy.bind(this);
+ this._teardown = this._teardown.bind(this);
+
+ EventEmitter.decorate(this);
+
+ Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
+ Services.obs.addObserver(this.destroy, "quit-application", false);
+}
+
+DevTools.prototype = {
+ /**
+ * 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.
+ * - icon: URL pointing to a graphic which will be used as the src for an
+ * 16x16 img tag (string|required)
+ * - 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)
+ * - 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");
+ }
+
+ toolDefinition.visibilityswitch = 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 devtools.defaultTools.sort(this.ordinalSort);
+ },
+
+ getAdditionalTools: function DT_getAdditionalTools() {
+ let tools = [];
+ for (let [key, value] of this._tools) {
+ if (devtools.defaultTools.indexOf(value) == -1) {
+ tools.push(value);
+ }
+ }
+ return tools.sort(this.ordinalSort);
+ },
+
+ /**
+ * 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 [key, value] of this._tools) {
+ let enabled;
+
+ try {
+ enabled = Services.prefs.getBoolPref(value.visibilityswitch);
+ } catch(e) {
+ enabled = true;
+ }
+
+ if (enabled || value.id == "options") {
+ tools.set(key, value);
+ }
+ }
+ 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.getToolDefinitionMap()) {
+ 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)
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that was opened
+ */
+ showToolbox: function(target, toolId, hostType) {
+ let deferred = Promise.defer();
+
+ let toolbox = this._toolboxes.get(target);
+ if (toolbox) {
+
+ let promise = (hostType != null && toolbox.hostType != hostType) ?
+ toolbox.switchHost(hostType) :
+ Promise.resolve(null);
+
+ if (toolId != null && toolbox.currentToolId != toolId) {
+ promise = promise.then(function() {
+ return toolbox.selectTool(toolId);
+ });
+ }
+
+ return promise.then(function() {
+ toolbox.raise();
+ return toolbox;
+ });
+ }
+ else {
+ // No toolbox for target, create one
+ toolbox = new devtools.Toolbox(target, toolId, hostType);
+
+ this._toolboxes.set(target, toolbox);
+
+ toolbox.once("destroyed", function() {
+ this._toolboxes.delete(target);
+ this.emit("toolbox-destroyed", target);
+ }.bind(this));
+
+ // If we were asked for a specific tool then we need to wait for the
+ // tool to be ready, otherwise we can just wait for toolbox open
+ if (toolId != null) {
+ toolbox.once(toolId + "-ready", function(event, panel) {
+ this.emit("toolbox-ready", toolbox);
+ deferred.resolve(toolbox);
+ }.bind(this));
+ toolbox.open();
+ }
+ else {
+ toolbox.open().then(function() {
+ deferred.resolve(toolbox);
+ this.emit("toolbox-ready", toolbox);
+ }.bind(this));
+ }
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Return the toolbox for a given target.
+ *
+ * @param {object} target
+ * Target value e.g. the target that owns this toolbox
+ *
+ * @return {Toolbox} toolbox
+ * The toobox that is debugging the given target
+ */
+ getToolbox: function DT_getToolbox(target) {
+ return this._toolboxes.get(target);
+ },
+
+ /**
+ * Close the toolbox for a given target
+ */
+ closeToolbox: function DT_closeToolbox(target) {
+ let toolbox = this._toolboxes.get(target);
+ if (toolbox == null) {
+ return;
+ }
+ return toolbox.destroy();
+ },
+
+ /**
+ * Called to tear down a tools provider.
+ */
+ _teardown: function DT_teardown() {
+ for (let [target, toolbox] of this._toolboxes) {
+ toolbox.destroy();
+ }
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function() {
+ Services.obs.removeObserver(this.destroy, "quit-application");
+ Services.obs.removeObserver(this._teardown, "devtools-unloaded");
+
+ for (let [key, tool] of this.getToolDefinitionMap()) {
+ this.unregisterTool(key, true);
+ }
+
+ // Cleaning down the toolboxes: i.e.
+ // for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
+ // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
+ },
+};
+
+/**
+ * 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 gDevTools = new DevTools();
+this.gDevTools = gDevTools;
+
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+let gDevToolsBrowser = {
+ /**
+ * A record of the windows whose menus we altered, so we can undo the changes
+ * as the window is closed
+ */
+ _trackedBrowserWindows: new Set(),
+
+ /**
+ * This function is for the benefit of Tools:DevToolbox in
+ * browser/base/content/browser-sets.inc and should not be used outside
+ * of there
+ */
+ toggleToolboxCommand: function(gBrowser) {
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ toolbox ? toolbox.destroy() : gDevTools.showToolbox(target);
+ },
+
+ toggleBrowserToolboxCommand: function(gBrowser) {
+ let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView);
+ let toolbox = gDevTools.getToolbox(target);
+
+ toolbox ? toolbox.destroy()
+ : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW);
+ },
+
+ /**
+ * 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 targetted tool is not selected,
+ * we select it
+ * - if the toolbox is open, and the targetted tool is selected,
+ * and the host is NOT a window, we close the toolbox
+ * - if the toolbox is open, and the targetted tool is selected,
+ * and the host is a window, we raise the toolbox window
+ */
+ selectToolCommand: function(gBrowser, toolId) {
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ if (toolbox && toolbox.currentToolId == toolId) {
+ if (toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+ toolbox.raise();
+ } else {
+ toolbox.destroy();
+ }
+ } else {
+ gDevTools.showToolbox(target, toolId);
+ }
+ },
+
+ /**
+ * Open a tab to allow connects to a remote browser
+ */
+ openConnectScreen: function(gBrowser) {
+ gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml");
+ },
+
+ /**
+ * Add this DevTools's presence to a browser window's document
+ *
+ * @param {XULDocument} doc
+ * The document to which menuitems and handlers are to be added
+ */
+ registerBrowserWindow: function DT_registerBrowserWindow(win) {
+ gDevToolsBrowser._trackedBrowserWindows.add(win);
+ gDevToolsBrowser._addAllToolsToMenu(win.document);
+
+ let tabContainer = win.document.getElementById("tabbrowser-tabs")
+ tabContainer.addEventListener("TabSelect",
+ gDevToolsBrowser._updateMenuCheckbox, false);
+ },
+
+ /**
+ * 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
+ */
+ attachKeybindingsToBrowser: function DT_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 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.id == "options") {
+ return;
+ }
+
+ // Skip if the tool is disabled.
+ try {
+ if (!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.id == "options") {
+ continue;
+ }
+ if (def === toolDefinition) {
+ break;
+ }
+ prevDef = def;
+ }
+
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ let doc = win.document;
+ let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
+
+ doc.getElementById("mainCommandSet").appendChild(elements.cmd);
+
+ if (elements.key) {
+ this.attachKeybindingsToBrowser(doc, elements.key);
+ }
+
+ doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
+
+ let amp = doc.getElementById("appmenu_webDeveloper_popup");
+ if (amp) {
+ let ref;
+
+ if (prevDef != null) {
+ let menuitem = doc.getElementById("appmenuitem_" + prevDef.id);
+ ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
+ } else {
+ ref = doc.getElementById("appmenu_devtools_separator");
+ }
+
+ if (ref) {
+ amp.insertBefore(elements.appmenuitem, ref);
+ }
+ }
+
+ let mp = doc.getElementById("menuWebDeveloperPopup");
+ if (mp) {
+ let ref;
+
+ if (prevDef != null) {
+ let menuitem = doc.getElementById("menuitem_" + prevDef.id);
+ ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
+ } else {
+ ref = doc.getElementById("menu_devtools_separator");
+ }
+
+ if (ref) {
+ mp.insertBefore(elements.menuitem, ref);
+ }
+ }
+ }
+ },
+
+ /**
+ * 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.
+ */
+ _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
+ let fragCommands = doc.createDocumentFragment();
+ let fragKeys = doc.createDocumentFragment();
+ let fragBroadcasters = doc.createDocumentFragment();
+ let fragAppMenuItems = doc.createDocumentFragment();
+ let fragMenuItems = doc.createDocumentFragment();
+
+ for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
+ if (toolDefinition.id == "options") {
+ continue;
+ }
+
+ // Skip if the tool is disabled.
+ try {
+ if (!Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
+ continue;
+ }
+ } catch(e) {}
+
+ let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
+
+ if (!elements) {
+ return;
+ }
+
+ fragCommands.appendChild(elements.cmd);
+ if (elements.key) {
+ fragKeys.appendChild(elements.key);
+ }
+ fragBroadcasters.appendChild(elements.bc);
+ fragAppMenuItems.appendChild(elements.appmenuitem);
+ fragMenuItems.appendChild(elements.menuitem);
+ }
+
+ let mcs = doc.getElementById("mainCommandSet");
+ mcs.appendChild(fragCommands);
+
+ this.attachKeybindingsToBrowser(doc, fragKeys);
+
+ let mbs = doc.getElementById("mainBroadcasterSet");
+ mbs.appendChild(fragBroadcasters);
+
+ let amp = doc.getElementById("appmenu_webDeveloper_popup");
+ if (amp) {
+ let amps = doc.getElementById("appmenu_devtools_separator");
+ amp.insertBefore(fragAppMenuItems, amps);
+ }
+
+ let mp = doc.getElementById("menuWebDeveloperPopup");
+ let mps = doc.getElementById("menu_devtools_separator");
+ mp.insertBefore(fragMenuItems, mps);
+ },
+
+ /**
+ * Add a menu entry for a tool definition
+ *
+ * @param {string} 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.
+ */
+ _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) {
+ let id = toolDefinition.id;
+
+ // Prevent multiple entries for the same tool.
+ if (doc.getElementById("Tools:" + id)) {
+ return;
+ }
+
+ let cmd = doc.createElement("command");
+ cmd.id = "Tools:" + id;
+ cmd.setAttribute("oncommand",
+ 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");');
+
+ let key = null;
+ if (toolDefinition.key) {
+ key = doc.createElement("key");
+ key.id = "key_" + id;
+
+ if (toolDefinition.key.startsWith("VK_")) {
+ key.setAttribute("keycode", toolDefinition.key);
+ } else {
+ key.setAttribute("key", toolDefinition.key);
+ }
+
+ key.setAttribute("command", cmd.id);
+ key.setAttribute("modifiers", toolDefinition.modifiers);
+ }
+
+ let bc = doc.createElement("broadcaster");
+ bc.id = "devtoolsMenuBroadcaster_" + id;
+ bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
+ bc.setAttribute("command", cmd.id);
+
+ if (key) {
+ bc.setAttribute("key", "key_" + id);
+ }
+
+ let appmenuitem = doc.createElement("menuitem");
+ appmenuitem.id = "appmenuitem_" + id;
+ appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
+
+ let menuitem = doc.createElement("menuitem");
+ menuitem.id = "menuitem_" + id;
+ menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
+
+ if (toolDefinition.accesskey) {
+ menuitem.setAttribute("accesskey", toolDefinition.accesskey);
+ }
+
+ return {
+ cmd: cmd,
+ key: key,
+ bc: bc,
+ appmenuitem: appmenuitem,
+ menuitem: menuitem
+ };
+ },
+
+ /**
+ * 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 = false;
+ if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
+ let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
+ if (gDevTools._toolboxes.has(target)) {
+ hasToolbox = true;
+ }
+ }
+
+ let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ if (hasToolbox) {
+ broadcaster.setAttribute("checked", "true");
+ } else {
+ broadcaster.removeAttribute("checked");
+ }
+ }
+ },
+
+ /**
+ * Connects to the SPS profiler when the developer tools are open.
+ */
+ _connectToProfiler: function DT_connectToProfiler() {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
+ let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
+ if (gDevTools._toolboxes.has(target)) {
+ target.makeRemote().then(() => {
+ let profiler = new ProfilerController(target);
+ profiler.connect();
+ }).then(null, Cu.reportError);
+
+ return;
+ }
+ }
+ }
+ },
+
+ /**
+ * 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) {
+ gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
+ }
+ },
+
+ /**
+ * 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
+ */
+ _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) {
+ let command = doc.getElementById("Tools:" + toolId);
+ if (command) {
+ command.parentNode.removeChild(command);
+ }
+
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.parentNode.removeChild(key);
+ }
+
+ let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
+ if (bc) {
+ bc.parentNode.removeChild(bc);
+ }
+
+ let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
+ if (appmenuitem) {
+ appmenuitem.parentNode.removeChild(appmenuitem);
+ }
+
+ let menuitem = doc.getElementById("menuitem_" + toolId);
+ if (menuitem) {
+ menuitem.parentNode.removeChild(menuitem);
+ }
+ },
+
+ /**
+ * 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 DT_forgetBrowserWindow(win) {
+ gDevToolsBrowser._trackedBrowserWindows.delete(win);
+
+ // Destroy toolboxes for closed window
+ for (let [target, toolbox] of gDevTools._toolboxes) {
+ if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) {
+ toolbox.destroy();
+ }
+ }
+
+ let tabContainer = win.document.getElementById("tabbrowser-tabs")
+ tabContainer.removeEventListener("TabSelect",
+ gDevToolsBrowser._updateMenuCheckbox, false);
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function() {
+ gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
+ Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
+ },
+}
+this.gDevToolsBrowser = gDevToolsBrowser;
+
+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-ready", gDevToolsBrowser._connectToProfiler);
+gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
+
+Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
+
+// Load the browser devtools main module as the loader's main module.
+devtools.main("main");
diff --git a/browser/devtools/framework/moz.build b/browser/devtools/framework/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/framework/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/framework/sidebar.js b/browser/devtools/framework/sidebar.js
new file mode 100644
index 000000000..e312db451
--- /dev/null
+++ b/browser/devtools/framework/sidebar.js
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cu} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var Promise = require("sdk/core/promise");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/shared/telemetry");
+
+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.
+ *
+ * @param {Node} tabbox
+ * <tabbox> node;
+ * @param {ToolPanel} panel
+ * Related ToolPanel instance;
+ * @param {String} uid
+ * Unique ID
+ * @param {Boolean} showTabstripe
+ * Show the tabs.
+ */
+function ToolSidebar(tabbox, panel, uid, showTabstripe=true)
+{
+ EventEmitter.decorate(this);
+
+ this._tabbox = tabbox;
+ this._uid = uid;
+ this._panelDoc = this._tabbox.ownerDocument;
+ this._toolPanel = panel;
+
+ try {
+ this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
+ } catch(e) {}
+
+ this._telemetry = new Telemetry();
+
+ this._tabbox.tabpanels.addEventListener("select", this, true);
+
+ this._tabs = new Map();
+
+ if (!showTabstripe) {
+ this._tabbox.setAttribute("hidetabs", "true");
+ }
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+ /**
+ * 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} tab uniq id
+ * @param {string} url
+ */
+ addTab: function ToolSidebar_addTab(id, url, selected=false) {
+ let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
+ iframe.className = "iframe-" + id;
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("src", url);
+ iframe.tooltip = "aHTMLTooltip";
+
+ let tab = this._tabbox.tabs.appendItem();
+ tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading
+
+ let onIFrameLoaded = function() {
+ tab.setAttribute("label", iframe.contentDocument.title);
+ iframe.removeEventListener("load", onIFrameLoaded, true);
+ if ("setPanel" in iframe.contentWindow) {
+ iframe.contentWindow.setPanel(this._toolPanel, iframe);
+ }
+ this.emit(id + "-ready");
+ }.bind(this);
+
+ iframe.addEventListener("load", onIFrameLoaded, true);
+
+ let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
+ tabpanel.setAttribute("id", "sidebar-panel-" + id);
+ tabpanel.appendChild(iframe);
+ 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 = "sidebar-panel-" + id;
+
+ // We store the index of this tab.
+ this._tabs.set(id, tab);
+
+ if (selected) {
+ // For some reason I don't understand, if we call this.select in this
+ // event loop (after inserting the tab), the tab will never get the
+ // the "selected" attribute set to true.
+ this._panelDoc.defaultView.setTimeout(function() {
+ this.select(id);
+ }.bind(this), 10);
+ }
+
+ this.emit("new-tab-registered", id);
+ },
+
+ /**
+ * Select a specific tab.
+ */
+ select: function ToolSidebar_select(id) {
+ let tab = this._tabs.get(id);
+ if (tab) {
+ this._tabbox.selectedTab = tab;
+ }
+ },
+
+ /**
+ * Return the id of the selected tab.
+ */
+ getCurrentTabID: function ToolSidebar_getCurrentTabID() {
+ 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 based on the id.
+ *
+ * @param String id
+ * unique id of the requested tab.
+ */
+ getTab: function ToolSidebar_getTab(id) {
+ return this._tabbox.tabpanels.querySelector("#sidebar-panel-" + id);
+ },
+
+ /**
+ * Event handler.
+ */
+ handleEvent: function ToolSidebar_eventHandler(event) {
+ if (event.type == "select") {
+ let previousTool = this._currentTool;
+ this._currentTool = this.getCurrentTabID();
+ if (previousTool) {
+ this._telemetry.toolClosed(previousTool);
+ this.emit(previousTool + "-unselected");
+ }
+
+ this._telemetry.toolOpened(this._currentTool);
+ this.emit(this._currentTool + "-selected");
+ this.emit("select", this._currentTool);
+ }
+ },
+
+ /**
+ * Toggle sidebar's visibility state.
+ */
+ toggle: function ToolSidebar_toggle() {
+ if (this._tabbox.hasAttribute("hidden")) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ show: function ToolSidebar_show() {
+ if (this._width) {
+ this._tabbox.width = this._width;
+ }
+ this._tabbox.removeAttribute("hidden");
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ hide: function ToolSidebar_hide() {
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+ this._tabbox.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Return the window containing the tab content.
+ */
+ getWindowForTab: function ToolSidebar_getWindowForTab(id) {
+ if (!this._tabs.has(id)) {
+ return null;
+ }
+
+ let panel = this._panelDoc.getElementById(this._tabs.get(id).linkedPanel);
+ return panel.firstChild.contentWindow;
+ },
+
+ /**
+ * Clean-up.
+ */
+ destroy: function ToolSidebar_destroy() {
+ if (this._destroyed) {
+ return Promise.resolve(null);
+ }
+ this._destroyed = true;
+
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+
+ this._tabbox.tabpanels.removeEventListener("select", this, true);
+
+ while (this._tabbox.tabpanels.hasChildNodes()) {
+ this._tabbox.tabpanels.removeChild(this._tabbox.tabpanels.firstChild);
+ }
+
+ while (this._tabbox.tabs.hasChildNodes()) {
+ this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
+ }
+
+ if (this._currentTool) {
+ this._telemetry.toolClosed(this._currentTool);
+ }
+
+ this._tabs = null;
+ this._tabbox = null;
+ this._panelDoc = null;
+ this._toolPanel = null;
+
+ return Promise.resolve(null);
+ },
+}
diff --git a/browser/devtools/framework/target.js b/browser/devtools/framework/target.js
new file mode 100644
index 000000000..d76ec7f98
--- /dev/null
+++ b/browser/devtools/framework/target.js
@@ -0,0 +1,601 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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 Promise = require("sdk/core/promise");
+var EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
+ "resource://gre/modules/devtools/dbg-server.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
+ "resource://gre/modules/devtools/dbg-client.jsm");
+
+const targets = new WeakMap();
+const 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 TF_forTab(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,
+ * chrome: true if the remote target is the whole process
+ * }
+ *
+ * @return A promise of a target object
+ */
+ forRemoteTab: function TF_forRemoteTab(options) {
+ let promise = promiseTargets.get(options);
+ if (promise == null) {
+ let target = new TabTarget(options);
+ promise = target.makeRemote().then(() => target);
+ promiseTargets.set(options, promise);
+ }
+ return promise;
+ },
+
+ /**
+ * 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 TF_isKnownTab(tab) {
+ return targets.has(tab);
+ },
+
+ /**
+ * Construct a Target
+ * @param {nsIDOMWindow} window
+ * The chromeWindow to use in creating a new target
+ * @return A target object
+ */
+ forWindow: function TF_forWindow(window) {
+ let target = targets.get(window);
+ if (target == null) {
+ target = new WindowTarget(window);
+ targets.set(window, target);
+ }
+ return target;
+ },
+
+ /**
+ * Get all of the targets known to the local browser instance
+ * @return An array of target objects
+ */
+ allTargets: function TF_allTargets() {
+ let windows = [];
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ let en = wm.getXULWindowEnumerator(null);
+ while (en.hasMoreElements()) {
+ windows.push(en.getNext());
+ }
+
+ return windows.map(function(window) {
+ return TargetFactory.forWindow(window);
+ });
+ },
+};
+
+/**
+ * The 'version' property allows the developer tools equivalent of browser
+ * detection. Browser detection is evil, however while we don't know what we
+ * will need to detect in the future, it is an easy way to postpone work.
+ * We should be looking to use 'supports()' in place of version where
+ * possible.
+ */
+function getVersion() {
+ // FIXME: return something better
+ return 20;
+}
+
+/**
+ * A better way to support feature detection, but we're not yet at a place
+ * where we have the features well enough defined for this to make lots of
+ * sense.
+ */
+function supports(feature) {
+ // FIXME: return something better
+ return false;
+};
+
+/**
+ * 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)
+ *
+ * Target also supports 2 functions to help allow 2 different versions of
+ * Firefox debug each other. The 'version' property is the equivalent of
+ * browser detection - simple and easy to implement but gets fragile when things
+ * are not quite what they seem. The 'supports' property is the equivalent of
+ * feature detection - harder to setup, but more robust long-term.
+ *
+ * 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)'.
+ */
+function Target() {
+ throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'");
+}
+
+Object.defineProperty(Target.prototype, "version", {
+ get: getVersion,
+ enumerable: true
+});
+
+
+/**
+ * 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);
+ // 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;
+ }
+}
+
+TabTarget.prototype = {
+ _webProgressListener: null,
+
+ supports: supports,
+ get version() { return getVersion(); },
+
+ get tab() {
+ return this._tab;
+ },
+
+ get form() {
+ return this._form;
+ },
+
+ get root() {
+ return this._root;
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ get chrome() {
+ return this._chrome;
+ },
+
+ get window() {
+ // 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() {
+ return this._tab ? this._tab.linkedBrowser.contentDocument.title :
+ this._form.title;
+ },
+
+ get url() {
+ return this._tab ? this._tab.linkedBrowser.contentDocument.location.href :
+ this._form.url;
+ },
+
+ get isRemote() {
+ return !this.isLocalTab;
+ },
+
+ get isLocalTab() {
+ return !!this._tab;
+ },
+
+ 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 TabTarget_makeRemote() {
+ 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();
+
+ let attachTab = () => {
+ this._client.attachTab(this._form.actor, (aResponse, aTabClient) => {
+ if (!aTabClient) {
+ this._remote.reject("Unable to attach to the tab");
+ return;
+ }
+ this.threadActor = aResponse.threadActor;
+ this._remote.resolve(null);
+ });
+ };
+
+ if (this.isLocalTab) {
+ this._client.connect((aType, aTraits) => {
+ this._client.listTabs(aResponse => {
+ this._root = aResponse;
+ this._form = aResponse.tabs[aResponse.selected];
+ attachTab();
+ });
+ });
+ } else if (!this.chrome) {
+ // In the remote debugging case, the protocol connection will have been
+ // already initialized in the connection screen code.
+ attachTab();
+ } else {
+ // Remote chrome debugging doesn't need anything at this point.
+ this._remote.resolve(null);
+ }
+
+ return this._remote.promise;
+ },
+
+ /**
+ * Listen to the different events.
+ */
+ _setupListeners: function TabTarget__setupListeners() {
+ 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);
+ },
+
+ /**
+ * Setup listeners for remote debugging, updating existing ones as necessary.
+ */
+ _setupRemoteListeners: function TabTarget__setupRemoteListeners() {
+ this.client.addListener("tabDetached", this.destroy);
+
+ this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) {
+ let event = Object.create(null);
+ event.url = aPacket.url;
+ event.title = aPacket.title;
+ event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
+ // 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;
+ }
+ }.bind(this);
+ this.client.addListener("tabNavigated", this._onTabNavigated);
+ },
+
+ /**
+ * 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) {
+ if (this._webProgressListener) {
+ this._webProgressListener.destroy();
+ }
+
+ this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+ this._tab.removeEventListener("TabClose", this);
+ this._tab.parentNode.removeEventListener("TabSelect", this);
+ }
+
+ // If this target was not remoted, the promise will be resolved before the
+ // function returns.
+ if (this._tab && !this._client) {
+ targets.delete(this._tab);
+ this._tab = null;
+ this._client = null;
+ this._form = null;
+ this._remote = null;
+
+ this._destroyer.resolve(null);
+ } 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.client.removeListener("tabNavigated", this._onTabNavigated);
+ this.client.removeListener("tabDetached", this.destroy);
+
+ this._client.close(function onClosed() {
+ if (this._tab) {
+ targets.delete(this._tab);
+ } else {
+ promiseTargets.delete(this._form);
+ }
+ this._client = null;
+ this._tab = null;
+ this._form = null;
+ this._remote = null;
+
+ this._destroyer.resolve(null);
+ }.bind(this));
+ }
+
+ return this._destroyer.promise;
+ },
+
+ toString: function() {
+ return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor));
+ },
+};
+
+
+/**
+ * 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 TWPL_onStateChange(progress, request, flag, status) {
+ 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 (this.target && this.target.window == progress.DOMWindow) {
+ // 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 TWPL_onLocationChange(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 TWPL_destroy() {
+ if (this.target.tab) {
+ this.target.tab.linkedBrowser.removeProgressListener(this);
+ }
+ this.target._webProgressListener = null;
+ this.target._navRequest = null;
+ this.target._navWindow = null;
+ this.target = null;
+ }
+};
+
+
+/**
+ * A WindowTarget represents a page living in a xul window or panel. Generally
+ * these will have a chrome: URL
+ */
+function WindowTarget(window) {
+ EventEmitter.decorate(this);
+ this._window = window;
+ this._setupListeners();
+}
+
+WindowTarget.prototype = {
+ supports: supports,
+ get version() { return getVersion(); },
+
+ get window() {
+ return this._window;
+ },
+
+ get name() {
+ return this._window.document.title;
+ },
+
+ get url() {
+ return this._window.document.location.href;
+ },
+
+ get isRemote() {
+ return false;
+ },
+
+ get isLocalTab() {
+ return false;
+ },
+
+ get isThreadPaused() {
+ return !!this._isThreadPaused;
+ },
+
+ /**
+ * Listen to the different events.
+ */
+ _setupListeners: function() {
+ this._handleThreadState = this._handleThreadState.bind(this);
+ this.on("thread-paused", this._handleThreadState);
+ this.on("thread-resumed", this._handleThreadState);
+ },
+
+ _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 (!this._destroyed) {
+ this._destroyed = true;
+
+ this.off("thread-paused", this._handleThreadState);
+ this.off("thread-resumed", this._handleThreadState);
+ this.emit("close");
+
+ targets.delete(this._window);
+ this._window = null;
+ }
+
+ return Promise.resolve(null);
+ },
+
+ toString: function() {
+ return 'WindowTarget:' + this.window;
+ },
+};
diff --git a/browser/devtools/framework/test/Makefile.in b/browser/devtools/framework/test/Makefile.in
new file mode 100644
index 000000000..2fe8c2a52
--- /dev/null
+++ b/browser/devtools/framework/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ head.js \
+ browser_devtools_api.js \
+ browser_new_activation_workflow.js \
+ browser_toolbox_dynamic_registration.js \
+ browser_toolbox_hosts.js \
+ browser_toolbox_ready.js \
+ browser_toolbox_select_event.js \
+ browser_target_events.js \
+ browser_toolbox_tool_ready.js \
+ browser_toolbox_sidebar.js \
+ browser_toolbox_window_shortcuts.js \
+ browser_toolbox_window_title_changes.js \
+ browser_toolbox_options.js \
+ browser_toolbox_options_disablejs.js \
+ browser_toolbox_options_disablejs.html \
+ browser_toolbox_options_disablejs_iframe.html \
+ browser_toolbox_highlight.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/framework/test/browser_devtools_api.js b/browser/devtools/framework/test/browser_devtools_api.js
new file mode 100644
index 000000000..6a79d337d
--- /dev/null
+++ b/browser/devtools/framework/test/browser_devtools_api.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+const Cu = Components.utils;
+const toolId = "test-tool";
+
+let tempScope = {};
+Cu.import("resource:///modules/devtools/shared/event-emitter.js", tempScope);
+let EventEmitter = tempScope.EventEmitter;
+
+function test() {
+ addTab("about:blank", function(aBrowser, aTab) {
+ runTests(aTab);
+ });
+}
+
+function runTests(aTab) {
+ let toolDefinition = {
+ id: toolId,
+ isTargetSupported: function() 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");
+ is(gDevTools.getToolDefinitionMap().has(toolId), false,
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ is(gDevTools.getToolDefinitionMap().has(toolId), true,
+ "The tool is registered");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, toolId).then(function(toolbox) {
+ is(toolbox.target, target, "toolbox target is correct");
+ is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct");
+ continueTests(toolbox);
+ }).then(null, console.error);
+}
+
+function continueTests(toolbox, panel) {
+ ok(toolbox.getCurrentPanel(), "panel value is correct");
+ is(toolbox.currentToolId, toolId, "toolbox _currentToolId is correct");
+
+ let toolDefinitions = gDevTools.getToolDefinitionMap();
+ is(toolDefinitions.has(toolId), true, "The tool is in gDevTools");
+
+ let toolDefinition = toolDefinitions.get(toolId);
+ is(toolDefinition.id, toolId, "toolDefinition id is correct");
+
+ gDevTools.unregisterTool(toolId);
+ is(gDevTools.getToolDefinitionMap().has(toolId), false,
+ "The tool is no longer registered");
+
+ 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();
+ }).then(null, console.error);
+}
+
+function finishUp() {
+ tempScope = null;
+ 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 = Promise.defer();
+
+ executeSoon(function() {
+ this._isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+ }.bind(this));
+
+ return deferred.promise;
+ },
+
+ get target() this._toolbox.target,
+
+ get toolbox() this._toolbox,
+
+ get isReady() this._isReady,
+
+ _isReady: false,
+
+ destroy: function DTI_destroy() {
+ return Promise.defer(null);
+ },
+};
diff --git a/browser/devtools/framework/test/browser_new_activation_workflow.js b/browser/devtools/framework/test/browser_new_activation_workflow.js
new file mode 100644
index 000000000..85b73e7c7
--- /dev/null
+++ b/browser/devtools/framework/test/browser_new_activation_workflow.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+const Cu = Components.utils;
+
+let toolbox, target;
+
+let tempScope = {};
+
+function test() {
+ addTab("about:blank", function(aBrowser, aTab) {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ loadWebConsole(aTab).then(function() {
+ console.log('loaded');
+ }, console.error);
+ });
+}
+
+function loadWebConsole(aTab) {
+ ok(gDevTools, "gDevTools exists");
+
+ return gDevTools.showToolbox(target, "webconsole").then(function(aToolbox) {
+ toolbox = aToolbox;
+ checkToolLoading();
+ }, console.error);
+}
+
+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();
+ });
+ }, console.error);
+}
+
+function selectAndCheckById(id) {
+ let doc = toolbox.frame.contentDocument;
+
+ return toolbox.selectTool(id).then(function() {
+ let tab = doc.getElementById("toolbox-tab-" + id);
+ is(tab.selected, true, "The " + id + " tab is selected");
+ });
+}
+
+function testToggle() {
+ toolbox.once("destroyed", function() {
+ // 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();
+ });
+ }.bind(this));
+
+ toolbox.destroy();
+}
+
+function finishUp() {
+ toolbox.destroy().then(function() {
+ toolbox = null;
+ target = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/browser/devtools/framework/test/browser_target_events.js b/browser/devtools/framework/test/browser_target_events.js
new file mode 100644
index 000000000..a9e386f4b
--- /dev/null
+++ b/browser/devtools/framework/test/browser_target_events.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/ */
+
+var target;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+
+ 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);
+ gBrowser.contentWindow.location = "data:text/html,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/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js b/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js
new file mode 100644
index 000000000..ae30f036a
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js
@@ -0,0 +1,107 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toolbox;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ gDevTools.showToolbox(target).then(testRegister);
+ }, true);
+
+ content.location = "data:text/html,test for dynamically registering and unregistering tools";
+}
+
+function testRegister(aToolbox)
+{
+ toolbox = aToolbox
+ gDevTools.once("tool-registered", toolRegistered);
+
+ gDevTools.registerTool({
+ id: "test-tool",
+ label: "Test Tool",
+ isTargetSupported: function() true,
+ build: function() {}
+ });
+}
+
+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.frame.contentDocument;
+ 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 command = win.document.getElementById("Tools:" + toolId);
+ ok(command, "command 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.frame.contentDocument;
+ 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 command = win.document.getElementById("Tools:" + toolId);
+ ok(!command, "command 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/browser/devtools/framework/test/browser_toolbox_highlight.js b/browser/devtools/framework/test/browser_toolbox_highlight.js
new file mode 100644
index 000000000..3ba74a4b7
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_highlight.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Toolbox = devtools.Toolbox;
+let temp = {};
+Cu.import("resource://gre/modules/Services.jsm", temp);
+let Services = temp.Services;
+temp = null;
+let toolbox = null;
+
+function test() {
+ waitForExplicitFinish();
+
+ 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, () => {
+ 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.classList.contains("highlighted"), "The highlighted class 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.classList.contains("highlighted"), "The highlighted class 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.classList.contains("highlighted"),
+ "The highlighted class is not present");
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_hosts.js b/browser/devtools/framework/test/browser_toolbox_hosts.js
new file mode 100644
index 000000000..5d451f01c
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_hosts.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/ */
+
+let temp = {}
+Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
+let DevTools = temp.DevTools;
+
+Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
+let devtools = temp.devtools;
+
+let Toolbox = devtools.Toolbox;
+
+let toolbox, target;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ gDevTools.showToolbox(target)
+ .then(testBottomHost, console.error)
+ .then(null, console.error);
+ }, true);
+
+ content.location = "data:text/html,test for opening toolbox in different hosts";
+}
+
+function testBottomHost(aToolbox)
+{
+ toolbox = aToolbox;
+
+ checkHostType(Toolbox.HostType.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);
+
+ toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
+}
+
+function testSidebarHost()
+{
+ checkHostType(Toolbox.HostType.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);
+
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost);
+}
+
+function testWindowHost()
+{
+ checkHostType(Toolbox.HostType.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);
+
+ testToolSelect();
+}
+
+function testToolSelect()
+{
+ // make sure we can load a tool after switching hosts
+ toolbox.selectTool("inspector").then(testDestroy);
+}
+
+function testDestroy()
+{
+ toolbox.destroy().then(function() {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target).then(testRememberHost);
+ });
+}
+
+function testRememberHost(aToolbox)
+{
+ toolbox = aToolbox;
+ // last host was the window - make sure it's the same when re-opening
+ is(toolbox.hostType, Toolbox.HostType.WINDOW, "host remembered");
+
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ ok(win, "toolbox separate window exists");
+
+ cleanup();
+}
+
+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 checkToolboxLoaded(iframe)
+{
+ let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
+ ok(tabs, "toolbox UI has been loaded into iframe");
+}
+
+function cleanup()
+{
+ Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+
+ toolbox.destroy().then(function() {
+ DevTools = Toolbox = toolbox = target = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }
diff --git a/browser/devtools/framework/test/browser_toolbox_options.js b/browser/devtools/framework/test/browser_toolbox_options.js
new file mode 100644
index 000000000..f24e03353
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_options.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc = null, toolbox = null, panelWin = null, index = 0, prefValues = [], prefNodes = [];
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ gDevTools.showToolbox(target).then(testSelectTool);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test for dynamically registering and unregistering tools";
+}
+
+function testSelectTool(aToolbox) {
+ toolbox = aToolbox;
+ doc = toolbox.doc;
+ toolbox.once("options-selected", testOptionsShortcut);
+ toolbox.selectTool("options");
+}
+
+function testOptionsShortcut() {
+ ok(true, "Toolbox selected via selectTool method");
+ toolbox.once("options-selected", testOptions);
+ toolbox.selectTool("webconsole")
+ .then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc));
+}
+
+function testOptions(event, tool) {
+ ok(true, "Toolbox selected via button click");
+ panelWin = tool.panelWin;
+ // Testing pref changes
+ let prefCheckboxes = tool.panelDoc.querySelectorAll("checkbox[data-pref]");
+ for (let checkbox of prefCheckboxes) {
+ prefNodes.push(checkbox);
+ prefValues.push(Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")));
+ }
+ // Do again with opposite values to reset prefs
+ for (let checkbox of prefCheckboxes) {
+ prefNodes.push(checkbox);
+ prefValues.push(!Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")));
+ }
+ testMouseClicks();
+}
+
+function testMouseClicks() {
+ if (index == prefValues.length) {
+ checkTools();
+ return;
+ }
+ gDevTools.once("pref-changed", prefChanged);
+ info("Click event synthesized for index " + index);
+ prefNodes[index].scrollIntoView();
+
+ // We use executeSoon here to ensure that the element is in view and
+ // clickable.
+ executeSoon(function() {
+ EventUtils.synthesizeMouseAtCenter(prefNodes[index], {}, panelWin);
+ });
+}
+
+function prefChanged(event, data) {
+ if (data.pref == prefNodes[index].getAttribute("data-pref")) {
+ ok(true, "Correct pref was changed");
+ is(data.oldValue, prefValues[index], "Previous value is correct");
+ is(data.newValue, !prefValues[index], "New value is correct");
+ index++;
+ testMouseClicks();
+ return;
+ }
+ ok(false, "Pref was not changed correctly");
+ cleanup();
+}
+
+function checkTools() {
+ let toolsPref = panelWin.document.querySelectorAll("#default-tools-box > checkbox");
+ prefNodes = [];
+ index = 0;
+ for (let tool of toolsPref) {
+ prefNodes.push(tool);
+ }
+ // Randomize the order in which we remove the tool and then add them back so
+ // that we get to know if the tabs are correctly placed as per their ordinals.
+ prefNodes = prefNodes.sort(() => Math.random() > 0.5 ? 1: -1);
+
+ // Wait for the next turn of the event loop to avoid stack overflow errors.
+ executeSoon(toggleTools);
+}
+
+function toggleTools() {
+ if (index < prefNodes.length) {
+ gDevTools.once("tool-unregistered", checkUnregistered);
+ let node = prefNodes[index];
+ node.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+ }
+ else if (index < 2*prefNodes.length) {
+ gDevTools.once("tool-registered", checkRegistered);
+ let node = prefNodes[index - prefNodes.length];
+ node.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+ }
+ else {
+ cleanup();
+ }
+}
+
+function checkUnregistered(event, data) {
+ if (data.id == prefNodes[index].getAttribute("id")) {
+ ok(true, "Correct tool removed");
+ // checking tab on the toolbox
+ ok(!doc.getElementById("toolbox-tab-" + data.id), "Tab removed for " +
+ data.id);
+ index++;
+ // Wait for the next turn of the event loop to avoid stack overflow errors.
+ executeSoon(toggleTools);
+ return;
+ }
+ ok(false, "Something went wrong, " + data.id + " was not unregistered");
+ cleanup();
+}
+
+function checkRegistered(event, data) {
+ if (data == prefNodes[index - prefNodes.length].getAttribute("id")) {
+ ok(true, "Correct tool added back");
+ // checking tab on the toolbox
+ let radio = doc.getElementById("toolbox-tab-" + data);
+ ok(radio, "Tab added back for " + data);
+ 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"));
+ }
+ index++;
+ // Wait for the next turn of the event loop to avoid stack overflow errors.
+ executeSoon(toggleTools);
+ return;
+ }
+ ok(false, "Something went wrong, " + data + " was not registered back");
+ cleanup();
+}
+
+function cleanup() {
+ toolbox.destroy().then(function() {
+ gBrowser.removeCurrentTab();
+ toolbox = doc = prefNodes = prefValues = panelWin = null;
+ finish();
+ });
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_options_disablejs.html b/browser/devtools/framework/test/browser_toolbox_options_disablejs.html
new file mode 100644
index 000000000..4e9a65685
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_options_disablejs.html
@@ -0,0 +1,45 @@
+<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_disablejs_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/browser/devtools/framework/test/browser_toolbox_options_disablejs.js b/browser/devtools/framework/test/browser_toolbox_options_disablejs.js
new file mode 100644
index 000000000..36c77f2ab
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_options_disablejs.js
@@ -0,0 +1,128 @@
+/* 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 = "http://example.com/browser/browser/devtools/framework/" +
+ "test/browser_toolbox_options_disablejs.html";
+
+let doc;
+let toolbox;
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ gDevTools.showToolbox(target).then(testSelectTool);
+ }, true);
+
+ content.location = TEST_URI;
+}
+
+function testSelectTool(aToolbox) {
+ toolbox = aToolbox;
+ toolbox.once("options-selected", testJSEnabled);
+ toolbox.selectTool("options");
+}
+
+function testJSEnabled(event, tool, secondPass) {
+ ok(true, "Toolbox selected via selectTool method");
+ info("Testing that JS is enabled");
+
+ let logJSEnabled = doc.getElementById("logJSEnabled");
+ let output = doc.getElementById("output");
+
+ // We use executeSoon here because switching docSehll.allowJavascript to true
+ // takes a while to become live.
+ executeSoon(function() {
+ EventUtils.synthesizeMouseAtCenter(logJSEnabled, {}, doc.defaultView);
+ is(output.textContent, "JavaScript Enabled", 'Output is "JavaScript Enabled"');
+ testJSEnabledIframe(secondPass);
+ });
+}
+
+function testJSEnabledIframe(secondPass) {
+ info("Testing that JS is enabled in the iframe");
+
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let logJSEnabled = iframeDoc.getElementById("logJSEnabled");
+ let output = iframeDoc.getElementById("output");
+
+ EventUtils.synthesizeMouseAtCenter(logJSEnabled, {}, iframe.contentWindow);
+ is(output.textContent, "JavaScript Enabled",
+ 'Output is "JavaScript Enabled" in iframe');
+ if (secondPass) {
+ finishUp();
+ } else {
+ toggleJS().then(testJSDisabled);
+ }
+}
+
+function toggleJS() {
+ let deferred = Promise.defer();
+ let panel = toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById("devtools-disable-javascript");
+
+ cbx.scrollIntoView();
+
+ if (cbx.checked) {
+ info("Clearing checkbox to re-enable JS");
+ } else {
+ info("Checking checkbox to disable JS");
+ }
+
+ // After uising scrollIntoView() we need to use executeSoon() to wait for the
+ // browser to scroll.
+ executeSoon(function() {
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+
+ deferred.resolve();
+ }, true);
+
+ EventUtils.synthesizeMouseAtCenter(cbx, {}, panel.panelWin);
+ });
+
+ return deferred.promise;
+}
+
+function testJSDisabled() {
+ info("Testing that JS is disabled");
+
+ let logJSDisabled = doc.getElementById("logJSDisabled");
+ let output = doc.getElementById("output");
+
+ EventUtils.synthesizeMouseAtCenter(logJSDisabled, {}, doc.defaultView);
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled"');
+
+ testJSDisabledIframe();
+}
+
+function testJSDisabledIframe() {
+ info("Testing that JS is disabled in the iframe");
+
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let logJSDisabled = iframeDoc.getElementById("logJSDisabled");
+ let output = iframeDoc.getElementById("output");
+
+ EventUtils.synthesizeMouseAtCenter(logJSDisabled, {}, iframe.contentWindow);
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled" in iframe');
+ toggleJS().then(function() {
+ testJSEnabled(null, null, true);
+ });
+}
+
+function finishUp() {
+ doc = toolbox = null;
+ finish();
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_options_disablejs_iframe.html b/browser/devtools/framework/test/browser_toolbox_options_disablejs_iframe.html
new file mode 100644
index 000000000..777bf86bf
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_options_disablejs_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/browser/devtools/framework/test/browser_toolbox_ready.js b/browser/devtools/framework/test/browser_toolbox_ready.js
new file mode 100644
index 000000000..29336406b
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_ready.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/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ gDevTools.showToolbox(target).then(testReady);
+ }, true);
+
+ content.location = "data:text/html,test for dynamically registering and unregistering tools";
+}
+
+function testReady(toolbox)
+{
+ ok(toolbox.isReady, "toolbox isReady is set");
+ testDouble(toolbox);
+}
+
+function testDouble(toolbox)
+{
+ let target = toolbox.target;
+ let toolId = toolbox.currentToolId;
+
+ gDevTools.showToolbox(target, toolId).then(function(toolbox2) {
+ is(toolbox2, toolbox, "same toolbox");
+ cleanup(toolbox);
+ });
+}
+
+function cleanup(toolbox)
+{
+ toolbox.destroy().then(function() {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_select_event.js b/browser/devtools/framework/test/browser_toolbox_select_event.js
new file mode 100644
index 000000000..58465b94a
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_select_event.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toolbox;
+
+function test() {
+ addTab("about:blank", function() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then(testSelect);
+ });
+}
+
+let called = {
+ inspector: false,
+ webconsole: false,
+ styleeditor: false,
+ //jsdebugger: false,
+}
+
+function testSelect(aToolbox) {
+ toolbox = aToolbox;
+
+ info("Toolbox fired a `ready` event");
+
+ toolbox.on("select", selectCB);
+
+ toolbox.selectTool("inspector");
+ toolbox.selectTool("webconsole");
+ toolbox.selectTool("styleeditor");
+ //toolbox.selectTool("jsdebugger");
+}
+
+function selectCB(event, id) {
+ called[id] = true;
+ info("toolbox-select event from " + id);
+
+ for (let tool in called) {
+ if (!called[tool]) {
+ return;
+ }
+ }
+
+ ok(true, "All the tools fired a 'select event'");
+ toolbox.off("select", selectCB);
+
+ reselect();
+}
+
+function reselect() {
+ for (let tool in called) {
+ called[tool] = false;
+ }
+
+ toolbox.once("inspector-selected", function() {
+ tidyUpIfAllCalled("inspector");
+ });
+
+ toolbox.once("webconsole-selected", function() {
+ tidyUpIfAllCalled("webconsole");
+ });
+
+ /*
+ toolbox.once("jsdebugger-selected", function() {
+ tidyUpIfAllCalled("jsdebugger");
+ });
+ */
+
+ toolbox.once("styleeditor-selected", function() {
+ tidyUpIfAllCalled("styleeditor");
+ });
+
+ toolbox.selectTool("inspector");
+ toolbox.selectTool("webconsole");
+ toolbox.selectTool("styleeditor");
+ //toolbox.selectTool("jsdebugger");
+}
+
+function tidyUpIfAllCalled(id) {
+ called[id] = true;
+ info("select event from " + id);
+
+ for (let tool in called) {
+ if (!called[tool]) {
+ return;
+ }
+ }
+
+ ok(true, "All the tools fired a {id}-selected event");
+ tidyUp();
+}
+
+function tidyUp() {
+ toolbox.destroy();
+ gBrowser.removeCurrentTab();
+
+ toolbox = null;
+ finish();
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_sidebar.js b/browser/devtools/framework/test/browser_toolbox_sidebar.js
new file mode 100644
index 000000000..63bc6ab0d
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ const Cu = Components.utils;
+ let {ToolSidebar} = devtools.require("devtools/framework/sidebar");
+
+ const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css' type='text/css'?>" +
+ "<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: function() true,
+ build: function(iframeWindow, toolbox) {
+ let deferred = Promise.defer();
+ executeSoon(function() {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function(){},
+ panelDoc: iframeWindow.document,
+ });
+ }.bind(this));
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ addTab("about:blank", function(aBrowser, aTab) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function(toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ 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, 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");
+ ok(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");
+ panel.sidebar.hide();
+ is(panel.sidebar._tabbox.getAttribute("hidden"), "true", "Sidebar hidden");
+ is(panel.sidebar.getWindowForTab("tab1").location.href, tab1URL, "Window is accessible");
+ testWidth(panel);
+ });
+ });
+
+ panel.sidebar.select("tab2");
+ }
+
+ 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();
+ gDevTools.unregisterTool(toolDefinition.id);
+
+ executeSoon(function() {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_tool_ready.js b/browser/devtools/framework/test/browser_toolbox_tool_ready.js
new file mode 100644
index 000000000..061815518
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_tool_ready.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ addTab().then(function(data) {
+ let toolIds = gDevTools.getToolDefinitionArray().map((def) => def.id);
+
+ let open = function(index) {
+ let toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ gDevTools.showToolbox(data.target, toolId).then(function(toolbox) {
+ 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");
+
+ let nextIndex = index + 1;
+ if (nextIndex >= toolIds.length) {
+ toolbox.destroy();
+ finish();
+ }
+ else {
+ open(nextIndex);
+ }
+ }, console.error);
+ };
+
+ open(0);
+ }).then(null, console.error);
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js b/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js
new file mode 100644
index 000000000..be40d5591
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Toolbox = devtools.Toolbox;
+
+let toolbox, toolIDs, idIndex;
+
+function test() {
+ waitForExplicitFinish();
+
+ if (window.navigator.userAgent.indexOf("Mac OS X 10.8") != -1 ||
+ window.navigator.userAgent.indexOf("Windows NT 5.1") != -1) {
+ info("Skipping Mac OSX 10.8 and Windows xp, see bug 838069");
+ finish();
+ return;
+ }
+ addTab("about:blank", function() {
+ toolIDs = [];
+ for (let [id, definition] of gDevTools._tools) {
+ // Skipping Profiler due to bug 838069. Re-enable when bug 845752 is fixed
+ if (definition.key && id != "jsprofiler") {
+ toolIDs.push(id);
+ }
+ }
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ idIndex = 0;
+ gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.WINDOW)
+ .then(testShortcuts);
+ });
+}
+
+function testShortcuts(aToolbox, aIndex) {
+ if (aIndex == toolIDs.length) {
+ tidyUp();
+ return;
+ }
+
+ toolbox = aToolbox;
+ info("Toolbox fired a `ready` event");
+
+ toolbox.once("select", selectCB);
+
+ if (aIndex != null) {
+ // This if block is to allow the call of selectCB without shortcut press for
+ // the first time. That happens because on opening of toolbox, one tool gets
+ // selected atleast.
+
+ let key = gDevTools._tools.get(toolIDs[aIndex]).key;
+ let toolModifiers = gDevTools._tools.get(toolIDs[aIndex]).modifiers;
+ let modifiers = {
+ accelKey: toolModifiers.contains("accel"),
+ altKey: toolModifiers.contains("alt"),
+ shiftKey: toolModifiers.contains("shift"),
+ };
+ idIndex = aIndex;
+ info("Testing shortcut for tool " + aIndex + ":" + toolIDs[aIndex] +
+ " using key " + key);
+ EventUtils.synthesizeKey(key, modifiers, toolbox.doc.defaultView.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();
+
+ toolbox = toolIDs = idIndex = Toolbox = null;
+ finish();
+ });
+}
diff --git a/browser/devtools/framework/test/browser_toolbox_window_title_changes.js b/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
new file mode 100644
index 000000000..6cf238eeb
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Toolbox = devtools.Toolbox;
+let temp = {};
+Cu.import("resource://gre/modules/Services.jsm", temp);
+let Services = temp.Services;
+temp = null;
+
+function test() {
+ waitForExplicitFinish();
+
+ const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+ const URL_2 = "data:text/plain;charset=UTF-8,12345";
+
+ const TOOL_ID_1 = "webconsole";
+ const TOOL_ID_2 = "jsdebugger";
+
+ const LABEL_1 = "Console";
+ const LABEL_2 = "Debugger";
+
+ let toolbox;
+
+ addTab(URL_1, function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
+ .then(function (aToolbox) { toolbox = aToolbox; })
+ .then(function () toolbox.selectTool(TOOL_ID_1))
+
+ // undock toolbox and check title
+ .then(function () toolbox.switchHost(Toolbox.HostType.WINDOW))
+ .then(checkTitle.bind(null, LABEL_1, URL_1, "toolbox undocked"))
+
+ // switch to different tool and check title
+ .then(function () toolbox.selectTool(TOOL_ID_2))
+ .then(checkTitle.bind(null, LABEL_2, URL_1, "tool changed"))
+
+ // navigate to different url and check title
+ .then(function () {
+ let deferred = Promise.defer();
+ target.once("navigate", function () deferred.resolve());
+ gBrowser.loadURI(URL_2);
+ return deferred.promise;
+ })
+ .then(checkTitle.bind(null, LABEL_2, URL_2, "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(function () toolbox.selectTool(TOOL_ID_1))
+ .then(checkTitle.bind(null, LABEL_1, URL_2,
+ "toolbox destroyed and recreated"))
+
+ // clean up
+ .then(function () 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(toolLabel, url, context) {
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ let definitions = gDevTools.getToolDefinitionMap();
+ let expectedTitle = toolLabel + " - " + url;
+ is(win.document.title, expectedTitle, context);
+}
diff --git a/browser/devtools/framework/test/head.js b/browser/devtools/framework/test/head.js
new file mode 100644
index 000000000..84a144a74
--- /dev/null
+++ b/browser/devtools/framework/test/head.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/. */
+
+let TargetFactory = gDevTools.TargetFactory;
+
+let tempScope = {};
+Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+let console = tempScope.console;
+Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope);
+let Promise = tempScope.Promise;
+
+let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback)
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ if (aURL != null) {
+ content.location = aURL;
+ }
+
+ let deferred = Promise.defer();
+
+ let tab = gBrowser.selectedTab;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ function onTabLoad() {
+ browser.removeEventListener("load", onTabLoad, true);
+
+ if (aCallback != null) {
+ aCallback(browser, tab, browser.contentDocument);
+ }
+
+ deferred.resolve({ browser: browser, tab: tab, target: target });
+ }
+
+ browser.addEventListener("load", onTabLoad, true);
+ return deferred.promise;
+}
+
+registerCleanupFunction(function tearDown() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function synthesizeKeyFromKeyTag(aKeyId, document) {
+ let key = document.getElementById(aKeyId);
+ isnot(key, null, "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("ctrl"),
+ altKey: modifiersAttr.match("alt"),
+ metaKey: modifiersAttr.match("meta"),
+ accelKey: modifiersAttr.match("accel")
+ }
+
+ EventUtils.synthesizeKey(name, modifiers);
+}
diff --git a/browser/devtools/framework/test/moz.build b/browser/devtools/framework/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/framework/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/framework/toolbox-hosts.js b/browser/devtools/framework/toolbox-hosts.js
new file mode 100644
index 000000000..c3c3556c8
--- /dev/null
+++ b/browser/devtools/framework/toolbox-hosts.js
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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");
+
+let Promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * 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
+}
+
+/**
+ * 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 BH_create() {
+ let deferred = Promise.defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+
+ this._splitter = ownerDocument.createElement("splitter");
+ this._splitter.setAttribute("class", "devtools-horizontal-splitter");
+
+ this.frame = ownerDocument.createElement("iframe");
+ this.frame.className = "devtools-toolbox-bottom-iframe";
+ this.frame.height = Services.prefs.getIntPref(this.heightPref);
+
+ this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser);
+ this._nbox.appendChild(this._splitter);
+ this._nbox.appendChild(this.frame);
+
+ let frameLoad = function() {
+ this.frame.removeEventListener("DOMContentLoaded", frameLoad, true);
+ this.emit("ready", this.frame);
+
+ deferred.resolve(this.frame);
+ }.bind(this);
+
+ this.frame.tooltip = "aHTMLTooltip";
+ this.frame.addEventListener("DOMContentLoaded", frameLoad, true);
+
+ // we have to load something so we can switch documents if we have to
+ this.frame.setAttribute("src", "about:blank");
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function BH_raise() {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function BH_setTitle(title) {
+ // Nothing to do for this host type.
+ },
+
+ /**
+ * Destroy the bottom dock.
+ */
+ destroy: function BH_destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.heightPref, this.frame.height);
+ this._nbox.removeChild(this._splitter);
+ this._nbox.removeChild(this.frame);
+ }
+
+ 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 SH_create() {
+ let deferred = Promise.defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+
+ 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 = Services.prefs.getIntPref(this.widthPref);
+
+ this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser);
+ this._sidebar.appendChild(this._splitter);
+ this._sidebar.appendChild(this.frame);
+
+ let frameLoad = function() {
+ this.frame.removeEventListener("DOMContentLoaded", frameLoad, true);
+ this.emit("ready", this.frame);
+
+ deferred.resolve(this.frame);
+ }.bind(this);
+
+ this.frame.addEventListener("DOMContentLoaded", frameLoad, true);
+ this.frame.tooltip = "aHTMLTooltip";
+ this.frame.setAttribute("src", "about:blank");
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function SH_raise() {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function SH_setTitle(title) {
+ // Nothing to do for this host type.
+ },
+
+ /**
+ * Destroy the sidebar.
+ */
+ destroy: function SH_destroy() {
+ 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://browser/content/devtools/framework/toolbox-window.xul",
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function WH_create() {
+ let deferred = Promise.defer();
+
+ let flags = "chrome,centerscreen,resizable,dialog=no";
+ let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank",
+ flags, null);
+
+ let frameLoad = function(event) {
+ win.removeEventListener("load", frameLoad, true);
+ this.frame = win.document.getElementById("toolbox-iframe");
+ this.emit("ready", this.frame);
+
+ deferred.resolve(this.frame);
+ }.bind(this);
+
+ win.addEventListener("load", frameLoad, true);
+ win.addEventListener("unload", this._boundUnload);
+
+ win.focus();
+
+ 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 RH_raise() {
+ this._window.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function WH_setTitle(title) {
+ this._window.document.title = title;
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function WH_destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._window.removeEventListener("unload", this._boundUnload);
+ this._window.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/browser/devtools/framework/toolbox-options.js b/browser/devtools/framework/toolbox-options.js
new file mode 100644
index 000000000..845c33d01
--- /dev/null
+++ b/browser/devtools/framework/toolbox-options.js
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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");
+
+let Promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+exports.OptionsPanel = OptionsPanel;
+
+XPCOMUtils.defineLazyGetter(this, "l10n", function() {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
+ let l10n = function(aName, ...aArgs) {
+ try {
+ if (aArgs.length == 0) {
+ return bundle.GetStringFromName(aName);
+ } else {
+ return bundle.formatStringFromName(aName, aArgs, aArgs.length);
+ }
+ } catch (ex) {
+ Services.console.logStringMessage("Error reading '" + aName + "'");
+ }
+ };
+ return l10n;
+});
+
+/**
+ * Represents the Options Panel in the Toolbox.
+ */
+function OptionsPanel(iframeWindow, toolbox) {
+ this.panelDoc = iframeWindow.document;
+ this.panelWin = iframeWindow;
+ this.toolbox = toolbox;
+ this.isReady = false;
+
+ // Make restart method available from xul
+ this.panelWin.restart = this.restart;
+
+ EventEmitter.decorate(this);
+};
+
+OptionsPanel.prototype = {
+
+ get target() {
+ return this.toolbox.target;
+ },
+
+ open: function() {
+ let deferred = Promise.defer();
+
+ this.setupToolsList();
+ this.populatePreferences();
+ this.prepareRestartPreferences();
+
+ this._disableJSClicked = this._disableJSClicked.bind(this);
+
+ let disableJSNode = this.panelDoc.getElementById("devtools-disable-javascript");
+ disableJSNode.addEventListener("click", this._disableJSClicked, false);
+
+ this.isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+ return deferred.promise;
+ },
+
+ 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;
+
+ defaultToolsBox.textContent = "";
+ additionalToolsBox.textContent = "";
+
+ let pref = function(key) {
+ try {
+ return Services.prefs.getBoolPref(key);
+ }
+ catch (ex) {
+ return true;
+ }
+ };
+
+ 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 checkbox = this.panelDoc.createElement("checkbox");
+ checkbox.setAttribute("id", tool.id);
+ checkbox.setAttribute("tooltiptext", tool.tooltip || "");
+ if (tool.isTargetSupported(this.target)) {
+ checkbox.setAttribute("label", tool.label);
+ }
+ else {
+ atleastOneToolNotSupported = true;
+ checkbox.setAttribute("label",
+ l10n("options.toolNotSupportedMarker", tool.label));
+ }
+ checkbox.setAttribute("checked", pref(tool.visibilityswitch));
+ checkbox.addEventListener("command", onCheckboxClick.bind(checkbox, tool.id));
+ return checkbox;
+ };
+
+ // Populating the default tools lists
+ for (let tool of gDevTools.getDefaultTools()) {
+ if (tool.id == "options") {
+ continue;
+ }
+ 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";
+ additionalToolsBox.previousSibling.style.display = "none";
+ }
+
+ if (!atleastOneToolNotSupported) {
+ toolsNotSupportedLabel.style.display = "none";
+ }
+
+ this.panelWin.focus();
+ },
+
+ populatePreferences: function() {
+ let prefCheckboxes = this.panelDoc.querySelectorAll("checkbox[data-pref]");
+ for (let checkbox of prefCheckboxes) {
+ checkbox.checked = Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"));
+ checkbox.addEventListener("command", function() {
+ let data = {
+ pref: this.getAttribute("data-pref"),
+ newValue: this.checked
+ };
+ data.oldValue = Services.prefs.getBoolPref(data.pref);
+ Services.prefs.setBoolPref(data.pref, data.newValue);
+ gDevTools.emit("pref-changed", data);
+ }.bind(checkbox));
+ }
+ let prefRadiogroups = this.panelDoc.querySelectorAll("radiogroup[data-pref]");
+ for (let radiogroup of prefRadiogroups) {
+ let selectedValue = Services.prefs.getCharPref(radiogroup.getAttribute("data-pref"));
+ for (let radio of radiogroup.childNodes) {
+ radiogroup.selectedIndex = -1;
+ if (radio.getAttribute("value") == selectedValue) {
+ radiogroup.selectedItem = radio;
+ break;
+ }
+ }
+ radiogroup.addEventListener("select", function() {
+ let data = {
+ pref: this.getAttribute("data-pref"),
+ newValue: this.selectedItem.getAttribute("value")
+ };
+ data.oldValue = Services.prefs.getCharPref(data.pref);
+ Services.prefs.setCharPref(data.pref, data.newValue);
+ gDevTools.emit("pref-changed", data);
+ }.bind(radiogroup));
+ }
+ },
+
+ /**
+ * Hides any label in a box with class "hidden-labels-box" at page load. The
+ * labels are shown again when the user click on the checkbox in the box.
+ */
+ prepareRestartPreferences: function() {
+ let labels = this.panelDoc.querySelectorAll(".hidden-labels-box > label");
+ for (let label of labels) {
+ label.style.display = "none";
+ }
+ let checkboxes = this.panelDoc.querySelectorAll(".hidden-labels-box > checkbox");
+ for (let checkbox of checkboxes) {
+ checkbox.addEventListener("command", function(target) {
+ target.nextSibling.style.display = "";
+ target.nextSibling.nextSibling.style.display = "";
+ }.bind(null, checkbox));
+ }
+ },
+
+ restart: function() {
+ let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
+ if (canceled.data) {
+ return;
+ }
+
+ // restart
+ Cc['@mozilla.org/toolkit/app-startup;1']
+ .getService(Ci.nsIAppStartup)
+ .quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+ },
+
+ /**
+ * 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 linkedBrowser = this.toolbox._host.hostTab.linkedBrowser;
+ let win = linkedBrowser.contentWindow;
+ let docShell = linkedBrowser.docShell;
+
+ if (typeof this.toolbox._origAllowJavascript == "undefined") {
+ this.toolbox._origAllowJavascript = docShell.allowJavascript;
+ }
+
+ docShell.allowJavascript = !checked;
+ win.location.reload();
+ },
+
+ destroy: function OP_destroy() {
+ let disableJSNode = this.panelDoc.getElementById("devtools-disable-javascript");
+ disableJSNode.removeEventListener("click", this._disableJSClicked, false);
+
+ this.panelWin = this.panelDoc = this.toolbox = this._disableJSClicked = null;
+ }
+};
diff --git a/browser/devtools/framework/toolbox-options.xul b/browser/devtools/framework/toolbox-options.xul
new file mode 100644
index 000000000..433669161
--- /dev/null
+++ b/browser/devtools/framework/toolbox-options.xul
@@ -0,0 +1,73 @@
+<?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://browser/locale/devtools/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet rel="stylesheet" href="chrome://browser/content/devtools/framework/toolbox.css" type="text/css"?>
+<?xml-stylesheet rel="stylesheet" href="chrome://browser/skin/devtools/toolbox.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox id="options-panel-container" flex="1">
+ <hbox id="options-panel" flex="1">
+ <vbox id="tools-box" class="options-vertical-pane" flex="1">
+ <label value="&options.selectDefaultTools.label;"/>
+ <vbox id="default-tools-box" class="options-groupbox" tabindex="0"/>
+ <label value="&options.selectAdditionalTools.label;"/>
+ <vbox id="additional-tools-box" class="options-groupbox"/>
+ <label id="tools-not-supported-label"
+ class="options-citation-label"
+ value="&options.toolNotSupported.label;"/>
+ </vbox>
+ <vbox class="options-vertical-pane" flex="1">
+ <label value="&options.selectDevToolsTheme.label;"/>
+ <radiogroup id="devtools-theme-box"
+ class="options-groupbox"
+ data-pref="devtools.theme"
+ orient="horizontal">
+ <radio value="light" label="&options.lightTheme.label;"/>
+ <radio value="dark" label="&options.darkTheme.label;"/>
+ </radiogroup>
+ <label value="&options.webconsole.label;"/>
+ <vbox id="webconsole-options" class="options-groupbox">
+ <checkbox label="&options.enablePersistentLogging.label;"
+ tooltiptext="&options.enablePersistentLogging.tooltip;"
+ data-pref="devtools.webconsole.persistlog"/>
+ </vbox>
+ <label value="&options.context.advancedSettings;"/>
+ <vbox id="context-options" class="options-groupbox">
+ <hbox>
+ <checkbox id="devtools-disable-javascript"
+ label="&options.disableJavaScript.label2;"
+ tooltiptext="&options.disableJavaScript.tooltip;"/>
+ <label class="options-citation-label"
+ value="(&options.context.triggersPageRefresh2;)"/>
+ </hbox>
+ <hbox class="hidden-labels-box">
+ <checkbox label="&options.enableChrome.label3;"
+ tooltiptext="&options.enableChrome.tooltip;"
+ data-pref="devtools.chrome.enabled"/>
+ <label class="options-citation-label"
+ value="&options.context.requiresRestart2;"/>
+ <label class="text-link"
+ onclick="restart()"
+ value="&options.restartButton.label;"/>
+ </hbox>
+ <hbox class="hidden-labels-box">
+ <checkbox label="&options.enableRemote.label3;"
+ tooltiptext="&options.enableRemote.tooltip;"
+ data-pref="devtools.debugger.remote-enabled"/>
+ <label class="options-citation-label"
+ value="&options.context.requiresRestart2;"/>
+ <label class="text-link"
+ onclick="restart()"
+ value="&options.restartButton.label;"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ </hbox>
+</window>
diff --git a/browser/devtools/framework/toolbox-window.xul b/browser/devtools/framework/toolbox-window.xul
new file mode 100644
index 000000000..880445e83
--- /dev/null
+++ b/browser/devtools/framework/toolbox-window.xul
@@ -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 window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<?xml-stylesheet href="chrome://browser/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"/>
+ </keyset>
+
+ <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
+</window>
diff --git a/browser/devtools/framework/toolbox.css b/browser/devtools/framework/toolbox.css
new file mode 100644
index 000000000..b1d68ec26
--- /dev/null
+++ b/browser/devtools/framework/toolbox.css
@@ -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/. */
+
+.devtools-tab > .radio-check,
+.devtools-tab > .radio-check-box1,
+.devtools-tab > .radio-spacer-box {
+ display: none;
+}
+
+#toolbox-controls > toolbarbutton > .toolbarbutton-text,
+#toolbox-dock-buttons > toolbarbutton > .toolbarbutton-text,
+.command-button > .toolbarbutton-text {
+ display: none;
+}
+
+#options-panel-container {
+ overflow: auto;
+}
+
+#options-panel {
+ overflow-y: auto;
+ display: block;
+}
+
+.options-vertical-pane {
+ display: inline;
+ float: left;
+}
+
+.options-vertical-pane > label {
+ display: block;
+}
diff --git a/browser/devtools/framework/toolbox.js b/browser/devtools/framework/toolbox.js
new file mode 100644
index 000000000..7288805a7
--- /dev/null
+++ b/browser/devtools/framework/toolbox.js
@@ -0,0 +1,785 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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 MAX_ORDINAL = 99;
+let Promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+let Telemetry = require("devtools/shared/telemetry");
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts);
+
+XPCOMUtils.defineLazyModuleGetter(this, "CommandUtils",
+ "resource:///modules/devtools/DeveloperToolbar.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function() {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
+ let l10n = function(aName, ...aArgs) {
+ try {
+ if (aArgs.length == 0) {
+ return bundle.GetStringFromName(aName);
+ } else {
+ return bundle.formatStringFromName(aName, aArgs, aArgs.length);
+ }
+ } catch (ex) {
+ Services.console.logStringMessage("Error reading '" + aName + "'");
+ }
+ };
+ return l10n;
+});
+
+XPCOMUtils.defineLazyGetter(this, "Requisition", function() {
+ let scope = {};
+ Cu.import("resource://gre/modules/devtools/Require.jsm", scope);
+ Cu.import("resource://gre/modules/devtools/gcli.jsm", {});
+
+ let req = scope.require;
+ return req('gcli/cli').Requisition;
+});
+
+/**
+ * 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)
+ */
+function Toolbox(target, selectedTool, hostType) {
+ this._target = target;
+ this._toolPanels = new Map();
+ this._telemetry = new Telemetry();
+
+ this._toolRegistered = this._toolRegistered.bind(this);
+ this._toolUnregistered = this._toolUnregistered.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this._target.on("close", this.destroy);
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
+ }
+ if (!selectedTool) {
+ selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
+ }
+ let definitions = gDevTools.getToolDefinitionMap();
+ if (!definitions.get(selectedTool) && selectedTool != "options") {
+ selectedTool = "webconsole";
+ }
+ this._defaultToolId = selectedTool;
+
+ this._host = this._createHost(hostType);
+
+ EventEmitter.decorate(this);
+
+ this._refreshHostTitle = this._refreshHostTitle.bind(this);
+ this._target.on("navigate", this._refreshHostTitle);
+ this.on("host-changed", this._refreshHostTitle);
+ this.on("select", this._refreshHostTitle);
+
+ gDevTools.on("tool-registered", this._toolRegistered);
+ gDevTools.on("tool-unregistered", this._toolUnregistered);
+}
+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"
+}
+
+Toolbox.prototype = {
+ _URL: "chrome://browser/content/devtools/framework/toolbox.xul",
+
+ _prefs: {
+ LAST_HOST: "devtools.toolbox.host",
+ LAST_TOOL: "devtools.toolbox.selectedTool",
+ SIDE_ENABLED: "devtools.toolbox.sideEnabled"
+ },
+
+ HostType: Toolbox.HostType,
+
+ /**
+ * Returns a *copy* of the _toolPanels collection.
+ *
+ * @return {Map} panels
+ * All the running panels in the toolbox
+ */
+ getToolPanels: function TB_getToolPanels() {
+ let panels = new Map();
+
+ for (let [key, value] of this._toolPanels) {
+ panels.set(key, value);
+ }
+ return panels;
+ },
+
+ /**
+ * Access the panel for a given tool
+ */
+ getPanel: function TBOX_getPanel(id) {
+ return this.getToolPanels().get(id);
+ },
+
+ /**
+ * 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 TBOX_getCurrentPanel() {
+ return this.getToolPanels().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/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._host.type;
+ },
+
+ /**
+ * Get/alter the currently displayed tool.
+ */
+ get currentToolId() {
+ return this._currentToolId;
+ },
+
+ set currentToolId(value) {
+ this._currentToolId = value;
+ },
+
+ /**
+ * Get the iframe containing the toolbox UI.
+ */
+ get frame() {
+ return this._host.frame;
+ },
+
+ /**
+ * Shortcut to the document containing the toolbox UI
+ */
+ get doc() {
+ return this.frame.contentDocument;
+ },
+
+ /**
+ * Open the toolbox
+ */
+ open: function TBOX_open() {
+ let deferred = Promise.defer();
+
+ this._host.create().then(iframe => {
+ let domReady = () => {
+ iframe.removeEventListener("DOMContentLoaded", domReady, true);
+
+ this.isReady = true;
+
+ let closeButton = this.doc.getElementById("toolbox-close");
+ closeButton.addEventListener("command", this.destroy, true);
+
+ this._buildDockButtons();
+ this._buildOptions();
+ this._buildTabs();
+ this._buildButtons();
+ this._addKeysToWindow();
+
+ this._telemetry.toolOpened("toolbox");
+
+ this.selectTool(this._defaultToolId).then(function(panel) {
+ this.emit("ready");
+ deferred.resolve();
+ }.bind(this));
+ };
+
+ iframe.addEventListener("DOMContentLoaded", domReady, true);
+ iframe.setAttribute("src", this._URL);
+ });
+
+ return deferred.promise;
+ },
+
+ _buildOptions: function TBOX__buildOptions() {
+ let key = this.doc.getElementById("toolbox-options-key");
+ key.addEventListener("command", function(toolId) {
+ this.selectTool(toolId);
+ }.bind(this, "options"), true);
+ },
+
+ /**
+ * Adds the keys and commands to the Toolbox Window in window mode.
+ */
+ _addKeysToWindow: function TBOX__addKeysToWindow() {
+ if (this.hostType != Toolbox.HostType.WINDOW) {
+ return;
+ }
+ let doc = this.doc.defaultView.parent.document;
+ for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
+ if (toolDefinition.key) {
+ // Prevent multiple entries for the same tool.
+ if (doc.getElementById("key_" + id)) {
+ continue;
+ }
+ let key = doc.createElement("key");
+ key.id = "key_" + id;
+
+ if (toolDefinition.key.startsWith("VK_")) {
+ key.setAttribute("keycode", toolDefinition.key);
+ } else {
+ key.setAttribute("key", toolDefinition.key);
+ }
+
+ key.setAttribute("modifiers", toolDefinition.modifiers);
+ key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
+ key.addEventListener("command", function(toolId) {
+ this.selectTool(toolId);
+ }.bind(this, id), true);
+ doc.getElementById("toolbox-keyset").appendChild(key);
+ }
+ }
+ },
+
+ /**
+ * Build the buttons for changing hosts. Called every time
+ * the host changes.
+ */
+ _buildDockButtons: function TBOX_createDockButtons() {
+ let dockBox = this.doc.getElementById("toolbox-dock-buttons");
+
+ while (dockBox.firstChild) {
+ dockBox.removeChild(dockBox.firstChild);
+ }
+
+ if (!this._target.isLocalTab) {
+ return;
+ }
+
+ let closeButton = this.doc.getElementById("toolbox-close");
+ if (this.hostType === this.HostType.WINDOW) {
+ closeButton.setAttribute("hidden", "true");
+ } else {
+ closeButton.removeAttribute("hidden");
+ }
+
+ let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+
+ for each (let position in this.HostType) {
+ if (position == this.hostType ||
+ (!sideEnabled && position == this.HostType.SIDE)) {
+ continue;
+ }
+
+ let button = this.doc.createElement("toolbarbutton");
+ button.id = "toolbox-dock-" + position;
+ button.className = "toolbox-dock-button";
+ button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." +
+ position + ".tooltip"));
+ button.addEventListener("command", function(position) {
+ this.switchHost(position);
+ }.bind(this, position));
+
+ dockBox.appendChild(button);
+ }
+ },
+
+ /**
+ * Add tabs to the toolbox UI for registered tools
+ */
+ _buildTabs: function TBOX_buildTabs() {
+ for (let definition of gDevTools.getToolDefinitionArray()) {
+ this._buildTabForTool(definition);
+ }
+ },
+
+ /**
+ * Add buttons to the UI as specified in the devtools.window.toolbarSpec pref
+ */
+ _buildButtons: function TBOX_buildButtons() {
+ if (!this.target.isLocalTab) {
+ return;
+ }
+
+ let toolbarSpec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
+ let env = CommandUtils.createEnvironment(this.target.tab.ownerDocument,
+ this.target.window.document);
+ let requisition = new Requisition(env);
+
+ let buttons = CommandUtils.createButtons(toolbarSpec, this._target, this.doc, requisition);
+
+ let container = this.doc.getElementById("toolbox-buttons");
+ buttons.forEach(container.appendChild.bind(container));
+ },
+
+ /**
+ * 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 TBOX_buildTabForTool(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;
+
+ let radio = this.doc.createElement("radio");
+ radio.className = "toolbox-tab devtools-tab";
+ radio.id = "toolbox-tab-" + id;
+ radio.setAttribute("toolid", id);
+ if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
+ toolDefinition.ordinal = MAX_ORDINAL;
+ }
+ radio.setAttribute("ordinal", toolDefinition.ordinal);
+ radio.setAttribute("tooltiptext", toolDefinition.tooltip);
+
+ radio.addEventListener("command", function(id) {
+ this.selectTool(id);
+ }.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) {
+ let label = this.doc.createElement("label");
+ label.setAttribute("value", toolDefinition.label)
+ label.setAttribute("crop", "end");
+ label.setAttribute("flex", "1");
+ radio.appendChild(label);
+ radio.setAttribute("flex", "1");
+ }
+
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "toolbox-panel";
+ vbox.id = "toolbox-panel-" + id;
+
+
+ // 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, iterate over all the tabs to get the correct location.
+ else {
+ Array.some(tabs.childNodes, (node, i) => {
+ if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
+ tabs.insertBefore(radio, node);
+ deck.insertBefore(vbox, deck.childNodes[i]);
+ return true;
+ }
+ });
+ }
+
+ this._addKeysToWindow();
+ },
+
+ /**
+ * Ensure the tool with the given id is loaded.
+ *
+ * @param {string} id
+ * The id of the tool to load.
+ */
+ loadTool: function TBOX_loadTool(id) {
+ let deferred = Promise.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", (panel) => {
+ deferred.resolve(panel);
+ });
+ }
+ return deferred.promise;
+ }
+
+ let definition = gDevTools.getToolDefinitionMap().get(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";
+
+ let vbox = this.doc.getElementById("toolbox-panel-" + id);
+ vbox.appendChild(iframe);
+
+ let onLoad = () => {
+ iframe.removeEventListener("DOMContentLoaded", onLoad, true);
+
+ let built = definition.build(iframe.contentWindow, this);
+ Promise.resolve(built).then((panel) => {
+ this._toolPanels.set(id, panel);
+ this.emit(id + "-ready", panel);
+ gDevTools.emit(id + "-ready", this, panel);
+ deferred.resolve(panel);
+ });
+ };
+
+ iframe.addEventListener("DOMContentLoaded", onLoad, true);
+ iframe.setAttribute("src", definition.url);
+ return deferred.promise;
+ },
+
+ /**
+ * Switch to the tool with the given id
+ *
+ * @param {string} id
+ * The id of the tool to switch to
+ */
+ selectTool: function TBOX_selectTool(id) {
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ if (selected) {
+ selected.removeAttribute("selected");
+ }
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab.setAttribute("selected", "true");
+
+ let prevToolId = this._currentToolId;
+
+ if (this._currentToolId == id) {
+ // Return the existing panel in order to have a consistent return value.
+ return 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 (prevToolId) {
+ this._telemetry.toolClosed(prevToolId);
+ }
+ 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
+ let index = 0;
+ let tabs = tabstrip.childNodes;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] === tab) {
+ index = i;
+ break;
+ }
+ }
+ tabstrip.selectedItem = tab;
+
+ // and select the right iframe
+ let deck = this.doc.getElementById("toolbox-deck");
+ deck.selectedIndex = index;
+
+ this._currentToolId = id;
+ if (id != "options") {
+ Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
+ }
+
+ return this.loadTool(id).then((panel) => {
+ this.emit("select", id);
+ this.emit(id + "-selected", panel);
+ return panel;
+ });
+ },
+
+ /**
+ * 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 TBOX_highlightTool(id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.classList.add("highlighted");
+ },
+
+ /**
+ * De-highlights the tool's tab.
+ *
+ * @param {string} id
+ * The id of the tool to unhighlight
+ */
+ unhighlightTool: function TBOX_unhighlightTool(id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.classList.remove("highlighted");
+ },
+
+ /**
+ * Raise the toolbox host.
+ */
+ raise: function TBOX_raise() {
+ this._host.raise();
+ },
+
+ /**
+ * Refresh the host's title.
+ */
+ _refreshHostTitle: function TBOX_refreshHostTitle() {
+ let toolName;
+ let toolId = this.currentToolId;
+ let toolDef = gDevTools.getToolDefinitionMap().get(toolId);
+ if (toolDef) {
+ toolName = toolDef.label;
+ } else {
+ // no tool is selected
+ toolName = toolboxStrings("toolbox.defaultTitle");
+ }
+ let title = toolboxStrings("toolbox.titleTemplate",
+ toolName, this.target.url);
+ this._host.setTitle(title);
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: some 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: function TBOX_createHost(hostType) {
+ if (!Hosts[hostType]) {
+ throw new Error('Unknown hostType: '+ hostType);
+ }
+ let newHost = new Hosts[hostType](this.target.tab);
+
+ // clean up the toolbox if its window is closed
+ newHost.on("window-closed", this.destroy);
+
+ return newHost;
+ },
+
+ /**
+ * Switch to a new host for the toolbox UI. E.g.
+ * bottom, sidebar, separate window.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ */
+ switchHost: function TBOX_switchHost(hostType) {
+ if (hostType == this._host.type) {
+ return;
+ }
+
+ if (!this._target.isLocalTab) {
+ return;
+ }
+
+ let newHost = this._createHost(hostType);
+ return newHost.create().then(function(iframe) {
+ // change toolbox document's parent to the new host
+ iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+ iframe.swapFrameLoaders(this.frame);
+
+ this._host.off("window-closed", this.destroy);
+ this._host.destroy();
+
+ this._host = newHost;
+
+ Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
+
+ this._buildDockButtons();
+ this._addKeysToWindow();
+
+ this.emit("host-changed");
+ }.bind(this));
+ },
+
+ /**
+ * 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 TBOX_toolRegistered(event, toolId) {
+ let defs = gDevTools.getToolDefinitionMap();
+ let tool = defs.get(toolId);
+
+ this._buildTabForTool(tool);
+ },
+
+ /**
+ * 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 TBOX_toolUnregistered(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.doc.defaultView.parent.document;
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.parentNode.removeChild(key);
+ }
+ }
+ },
+
+
+ /**
+ * Get the toolbox's notification box
+ *
+ * @return The notification box element.
+ */
+ getNotificationBox: function TBOX_getNotificationBox() {
+ return this.doc.getElementById("toolbox-notificationbox");
+ },
+
+ /**
+ * Remove all UI elements, detach from target and clear up
+ */
+ destroy: function TBOX_destroy() {
+ // 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;
+ }
+ // Assign the "_destroyer" property before calling the other
+ // destroyer methods to guarantee that the Toolbox's destroy
+ // method is only executed once.
+ let deferred = Promise.defer();
+ this._destroyer = deferred.promise;
+
+ this._target.off("navigate", this._refreshHostTitle);
+ this.off("select", this._refreshHostTitle);
+ this.off("host-changed", this._refreshHostTitle);
+
+ gDevTools.off("tool-registered", this._toolRegistered);
+ gDevTools.off("tool-unregistered", this._toolUnregistered);
+
+ // Revert docShell.allowJavascript back to it's original value if it was
+ // changed via the Disable JS option.
+ if (typeof this._origAllowJavascript != "undefined") {
+ let docShell = this._host.hostTab.linkedBrowser.docShell;
+ docShell.allowJavascript = this._origAllowJavascript;
+ delete this._origAllowJavascript;
+ }
+
+ let outstanding = [];
+
+ for (let [id, panel] of this._toolPanels) {
+ outstanding.push(panel.destroy());
+ }
+
+ let container = this.doc.getElementById("toolbox-buttons");
+ while(container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ outstanding.push(this._host.destroy());
+
+ this._telemetry.destroy();
+
+ // Targets need to be notified that the toolbox is being torn down, so that
+ // remote protocol connections can be gracefully terminated.
+ if (this._target) {
+ this._target.off("close", this.destroy);
+ outstanding.push(this._target.destroy());
+ }
+ this._target = null;
+
+ Promise.all(outstanding).then(function() {
+ 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;
+ deferred.resolve();
+ }.bind(this));
+
+ return this._destroyer;
+ }
+};
diff --git a/browser/devtools/framework/toolbox.xul b/browser/devtools/framework/toolbox.xul
new file mode 100644
index 000000000..e1cd2f7f8
--- /dev/null
+++ b/browser/devtools/framework/toolbox.xul
@@ -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 window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/framework/toolbox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/toolbox.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <commandset id="editMenuCommands"/>
+ <keyset id="editMenuKeys"/>
+ <keyset id="toolbox-keyset">
+ <key id="toolbox-options-key"
+ key="&toolboxOptionsButton.key;"
+ oncommand="void(0);"
+ modifiers="shift, accel"/>
+ </keyset>
+
+ <notificationbox id="toolbox-notificationbox" flex="1">
+ <toolbar class="devtools-tabbar">
+#ifdef XP_MACOSX
+ <hbox id="toolbox-controls">
+ <toolbarbutton id="toolbox-close"
+ class="devtools-closebutton"
+ tooltiptext="&toolboxCloseButton.tooltip;"/>
+ <hbox id="toolbox-dock-buttons"/>
+ </hbox>
+#endif
+ <hbox id="toolbox-tabs" flex="1">
+ </hbox>
+ <hbox id="toolbox-buttons" pack="end"/>
+#ifndef XP_MACOSX
+ <vbox id="toolbox-controls-separator"/>
+ <hbox id="toolbox-controls">
+ <hbox id="toolbox-dock-buttons"/>
+ <toolbarbutton id="toolbox-close"
+ class="devtools-closebutton"
+ tooltiptext="&toolboxCloseButton.tooltip;"/>
+ </hbox>
+#endif
+ </toolbar>
+ <deck id="toolbox-deck" flex="1">
+ </deck>
+ </notificationbox>
+</window>
diff --git a/browser/devtools/inspector/CmdInspect.jsm b/browser/devtools/inspector/CmdInspect.jsm
new file mode 100644
index 000000000..e1d2f28ee
--- /dev/null
+++ b/browser/devtools/inspector/CmdInspect.jsm
@@ -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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+this.EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+/**
+ * 'inspect' command
+ */
+gcli.addCommand({
+ name: "inspect",
+ description: gcli.lookup("inspectDesc"),
+ manual: gcli.lookup("inspectManual"),
+ params: [
+ {
+ name: "selector",
+ type: "node",
+ description: gcli.lookup("inspectNodeDesc"),
+ manual: gcli.lookup("inspectNodeManual")
+ }
+ ],
+ exec: function Command_inspect(args, context) {
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ return gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ toolbox.getCurrentPanel().selection.setNode(args.selector, "gcli");
+ }.bind(this));
+ }
+});
diff --git a/browser/devtools/inspector/Makefile.in b/browser/devtools/inspector/Makefile.in
new file mode 100644
index 000000000..12ae09fda
--- /dev/null
+++ b/browser/devtools/inspector/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools/
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/inspector
diff --git a/browser/devtools/inspector/breadcrumbs.js b/browser/devtools/inspector/breadcrumbs.js
new file mode 100644
index 000000000..820ff6b79
--- /dev/null
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -0,0 +1,597 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cc, Cu, Ci} = require("chrome");
+
+const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+const LOW_PRIORITY_ELEMENTS = {
+ "HEAD": true,
+ "BASE": true,
+ "BASEFONT": true,
+ "ISINDEX": true,
+ "LINK": true,
+ "META": true,
+ "SCRIPT": true,
+ "STYLE": true,
+ "TITLE": true,
+};
+
+///////////////////////////////////////////////////////////////////////////
+//// HTML Breadcrumbs
+
+/**
+ * Display the ancestors of the current node and its children.
+ * Only one "branch" of children are displayed (only one line).
+ *
+ * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector.
+ *
+ * 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).
+ */
+function HTMLBreadcrumbs(aInspector)
+{
+ this.inspector = aInspector;
+ this.selection = this.inspector.selection;
+ this.chromeWin = this.inspector.panelWin;
+ this.chromeDoc = this.inspector.panelDoc;
+ this.DOMHelpers = new DOMHelpers(this.chromeWin);
+ this._init();
+}
+
+exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
+
+HTMLBreadcrumbs.prototype = {
+ _init: function BC__init()
+ {
+ this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
+ this.container.addEventListener("mousedown", this, true);
+ this.container.addEventListener("keypress", this, true);
+
+ // We will save a list of already displayed nodes in this array.
+ this.nodeHierarchy = [];
+
+ // Last selected node in nodeHierarchy.
+ this.currentIndex = -1;
+
+ // By default, hide the arrows. We let the <scrollbox> show them
+ // in case of overflow.
+ this.container.removeAttribute("overflows");
+ this.container._scrollButtonUp.collapsed = true;
+ this.container._scrollButtonDown.collapsed = true;
+
+ this.onscrollboxreflow = function() {
+ if (this.container._scrollButtonDown.collapsed)
+ this.container.removeAttribute("overflows");
+ else
+ this.container.setAttribute("overflows", true);
+ }.bind(this);
+
+ this.container.addEventListener("underflow", this.onscrollboxreflow, false);
+ this.container.addEventListener("overflow", this.onscrollboxreflow, false);
+
+ this.update = this.update.bind(this);
+ this.updateSelectors = this.updateSelectors.bind(this);
+ this.selection.on("new-node", this.update);
+ this.selection.on("pseudoclass", this.updateSelectors);
+ this.selection.on("attribute-changed", this.updateSelectors);
+ this.update();
+ },
+
+ /**
+ * Build a string that represents the node: tagName#id.class1.class2.
+ *
+ * @param aNode The node to pretty-print
+ * @returns a string
+ */
+ prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
+ {
+ let text = aNode.tagName.toLowerCase();
+ if (aNode.id) {
+ text += "#" + aNode.id;
+ }
+ for (let i = 0; i < aNode.classList.length; i++) {
+ text += "." + aNode.classList[i];
+ }
+ for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
+ let pseudo = PSEUDO_CLASSES[i];
+ if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) {
+ text += pseudo;
+ }
+ }
+
+ return text;
+ },
+
+
+ /**
+ * Build <label>s that represent the node:
+ * <label class="breadcrumbs-widget-item-tag">tagName</label>
+ * <label class="breadcrumbs-widget-item-id">#id</label>
+ * <label class="breadcrumbs-widget-item-classes">.class1.class2</label>
+ *
+ * @param aNode The node to pretty-print
+ * @returns a document fragment.
+ */
+ prettyPrintNodeAsXUL: function BC_prettyPrintNodeXUL(aNode)
+ {
+ let fragment = this.chromeDoc.createDocumentFragment();
+
+ let tagLabel = this.chromeDoc.createElement("label");
+ tagLabel.className = "breadcrumbs-widget-item-tag plain";
+
+ let idLabel = this.chromeDoc.createElement("label");
+ idLabel.className = "breadcrumbs-widget-item-id plain";
+
+ let classesLabel = this.chromeDoc.createElement("label");
+ classesLabel.className = "breadcrumbs-widget-item-classes plain";
+
+ let pseudosLabel = this.chromeDoc.createElement("label");
+ pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
+
+ tagLabel.textContent = aNode.tagName.toLowerCase();
+ idLabel.textContent = aNode.id ? ("#" + aNode.id) : "";
+
+ let classesText = "";
+ for (let i = 0; i < aNode.classList.length; i++) {
+ classesText += "." + aNode.classList[i];
+ }
+ classesLabel.textContent = classesText;
+
+ let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
+ return DOMUtils.hasPseudoClassLock(aNode, pseudo);
+ }, this);
+ pseudosLabel.textContent = pseudos.join("");
+
+ fragment.appendChild(tagLabel);
+ fragment.appendChild(idLabel);
+ fragment.appendChild(classesLabel);
+ fragment.appendChild(pseudosLabel);
+
+ return fragment;
+ },
+
+ /**
+ * Open the sibling menu.
+ *
+ * @param aButton the button representing the node.
+ * @param aNode the node we want the siblings from.
+ */
+ openSiblingMenu: function BC_openSiblingMenu(aButton, aNode)
+ {
+ // We make sure that the targeted node is selected
+ // because we want to use the nodemenu that only works
+ // for inspector.selection
+ this.selection.setNode(aNode, "breadcrumbs");
+
+ let title = this.chromeDoc.createElement("menuitem");
+ title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings"));
+ title.setAttribute("disabled", "true");
+
+ let separator = this.chromeDoc.createElement("menuseparator");
+
+ let items = [title, separator];
+
+ let nodes = aNode.parentNode.childNodes;
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].nodeType == aNode.ELEMENT_NODE) {
+ let item = this.chromeDoc.createElement("menuitem");
+ if (nodes[i] === aNode) {
+ item.setAttribute("disabled", "true");
+ item.setAttribute("checked", "true");
+ }
+
+ item.setAttribute("type", "radio");
+ item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i]));
+
+ let selection = this.selection;
+ item.onmouseup = (function(aNode) {
+ return function() {
+ selection.setNode(aNode, "breadcrumbs");
+ }
+ })(nodes[i]);
+
+ items.push(item);
+ }
+ }
+ this.inspector.showNodeMenu(aButton, "before_start", items);
+ },
+
+ /**
+ * Generic event handler.
+ *
+ * @param nsIDOMEvent event
+ * The DOM event object.
+ */
+ handleEvent: function BC_handleEvent(event)
+ {
+ if (event.type == "mousedown" && event.button == 0) {
+ // on Click and Hold, open the Siblings menu
+
+ let timer;
+ let container = this.container;
+
+ function openMenu(event) {
+ cancelHold();
+ let target = event.originalTarget;
+ if (target.tagName == "button") {
+ target.onBreadcrumbsHold();
+ }
+ }
+
+ function handleClick(event) {
+ cancelHold();
+ let target = event.originalTarget;
+ if (target.tagName == "button") {
+ target.onBreadcrumbsClick();
+ }
+ }
+
+ let window = this.chromeWin;
+ function cancelHold(event) {
+ window.clearTimeout(timer);
+ container.removeEventListener("mouseout", cancelHold, false);
+ container.removeEventListener("mouseup", handleClick, false);
+ }
+
+ container.addEventListener("mouseout", cancelHold, false);
+ container.addEventListener("mouseup", handleClick, false);
+ timer = window.setTimeout(openMenu, 500, event);
+ }
+
+ if (event.type == "keypress" && this.selection.isElementNode()) {
+ let node = null;
+ switch (event.keyCode) {
+ case this.chromeWin.KeyEvent.DOM_VK_LEFT:
+ if (this.currentIndex != 0) {
+ node = this.nodeHierarchy[this.currentIndex - 1].node;
+ }
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
+ if (this.currentIndex < this.nodeHierarchy.length - 1) {
+ node = this.nodeHierarchy[this.currentIndex + 1].node;
+ }
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_UP:
+ node = this.selection.node.previousSibling;
+ while (node && (node.nodeType != node.ELEMENT_NODE)) {
+ node = node.previousSibling;
+ }
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_DOWN:
+ node = this.selection.node.nextSibling;
+ while (node && (node.nodeType != node.ELEMENT_NODE)) {
+ node = node.nextSibling;
+ }
+ break;
+ }
+ if (node) {
+ this.selection.setNode(node, "breadcrumbs");
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Remove nodes and delete properties.
+ */
+ destroy: function BC_destroy()
+ {
+ this.nodeHierarchy.forEach(function(crumb) {
+ if (LayoutHelpers.isNodeConnected(crumb.node)) {
+ DOMUtils.clearPseudoClassLocks(crumb.node);
+ }
+ });
+
+ this.selection.off("new-node", this.update);
+ this.selection.off("pseudoclass", this.updateSelectors);
+ this.selection.off("attribute-changed", this.updateSelectors);
+
+ this.container.removeEventListener("underflow", this.onscrollboxreflow, false);
+ this.container.removeEventListener("overflow", this.onscrollboxreflow, false);
+ this.onscrollboxreflow = null;
+
+ this.empty();
+ this.container.removeEventListener("mousedown", this, true);
+ this.container.removeEventListener("keypress", this, true);
+ this.container = null;
+ this.nodeHierarchy = null;
+ },
+
+ /**
+ * Empty the breadcrumbs container.
+ */
+ empty: function BC_empty()
+ {
+ while (this.container.hasChildNodes()) {
+ this.container.removeChild(this.container.firstChild);
+ }
+ },
+
+ /**
+ * Re-init the cache and remove all the buttons.
+ */
+ invalidateHierarchy: function BC_invalidateHierarchy()
+ {
+ this.inspector.hideNodeMenu();
+ this.nodeHierarchy = [];
+ this.empty();
+ },
+
+ /**
+ * Set which button represent the selected node.
+ *
+ * @param aIdx Index of the displayed-button to select
+ */
+ setCursor: function BC_setCursor(aIdx)
+ {
+ // Unselect the previously selected button
+ if (this.currentIndex > -1 && this.currentIndex < this.nodeHierarchy.length) {
+ this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
+ }
+ if (aIdx > -1) {
+ this.nodeHierarchy[aIdx].button.setAttribute("checked", "true");
+ if (this.hadFocus)
+ this.nodeHierarchy[aIdx].button.focus();
+ }
+ this.currentIndex = aIdx;
+ },
+
+ /**
+ * Get the index of the node in the cache.
+ *
+ * @param aNode
+ * @returns integer the index, -1 if not found
+ */
+ indexOf: function BC_indexOf(aNode)
+ {
+ let i = this.nodeHierarchy.length - 1;
+ for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
+ if (this.nodeHierarchy[i].node === aNode) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Remove all the buttons and their references in the cache
+ * after a given index.
+ *
+ * @param aIdx
+ */
+ cutAfter: function BC_cutAfter(aIdx)
+ {
+ while (this.nodeHierarchy.length > (aIdx + 1)) {
+ let toRemove = this.nodeHierarchy.pop();
+ this.container.removeChild(toRemove.button);
+ }
+ },
+
+ /**
+ * Build a button representing the node.
+ *
+ * @param aNode The node from the page.
+ * @returns aNode The <button>.
+ */
+ buildButton: function BC_buildButton(aNode)
+ {
+ let button = this.chromeDoc.createElement("button");
+ button.appendChild(this.prettyPrintNodeAsXUL(aNode));
+ button.className = "breadcrumbs-widget-item";
+
+ button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode));
+
+ button.onkeypress = function onBreadcrumbsKeypress(e) {
+ if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
+ e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)
+ button.click();
+ }
+
+ button.onBreadcrumbsClick = function onBreadcrumbsClick() {
+ this.selection.setNode(aNode, "breadcrumbs");
+ }.bind(this);
+
+ button.onclick = (function _onBreadcrumbsRightClick(event) {
+ button.focus();
+ if (event.button == 2) {
+ this.openSiblingMenu(button, aNode);
+ }
+ }).bind(this);
+
+ button.onBreadcrumbsHold = (function _onBreadcrumbsHold() {
+ this.openSiblingMenu(button, aNode);
+ }).bind(this);
+ return button;
+ },
+
+ /**
+ * Connecting the end of the breadcrumbs to a node.
+ *
+ * @param aNode The node to reach.
+ */
+ expand: function BC_expand(aNode)
+ {
+ let fragment = this.chromeDoc.createDocumentFragment();
+ let toAppend = aNode;
+ let lastButtonInserted = null;
+ let originalLength = this.nodeHierarchy.length;
+ let stopNode = null;
+ if (originalLength > 0) {
+ stopNode = this.nodeHierarchy[originalLength - 1].node;
+ }
+ while (toAppend && toAppend.tagName && toAppend != stopNode) {
+ let button = this.buildButton(toAppend);
+ fragment.insertBefore(button, lastButtonInserted);
+ lastButtonInserted = button;
+ this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button});
+ toAppend = this.DOMHelpers.getParentObject(toAppend);
+ }
+ this.container.appendChild(fragment, this.container.firstChild);
+ },
+
+ /**
+ * Get a child of a node that can be displayed in the breadcrumbs
+ * and that is probably visible. See LOW_PRIORITY_ELEMENTS.
+ *
+ * @param aNode The parent node.
+ * @returns nsIDOMNode|null
+ */
+ getInterestingFirstNode: function BC_getInterestingFirstNode(aNode)
+ {
+ let nextChild = this.DOMHelpers.getChildObject(aNode, 0);
+ let fallback = null;
+
+ while (nextChild) {
+ if (nextChild.nodeType == aNode.ELEMENT_NODE) {
+ if (!(nextChild.tagName in LOW_PRIORITY_ELEMENTS)) {
+ return nextChild;
+ }
+ if (!fallback) {
+ fallback = nextChild;
+ }
+ }
+ nextChild = this.DOMHelpers.getNextSibling(nextChild);
+ }
+ return fallback;
+ },
+
+
+ /**
+ * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
+ *
+ * @param aNode
+ * @returns Index of the ancestor in the cache
+ */
+ getCommonAncestor: function BC_getCommonAncestor(aNode)
+ {
+ let node = aNode;
+ while (node) {
+ let idx = this.indexOf(node);
+ if (idx > -1) {
+ return idx;
+ } else {
+ node = this.DOMHelpers.getParentObject(node);
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Make sure that the latest node in the breadcrumbs is not the selected node
+ * if the selected node still has children.
+ */
+ ensureFirstChild: function BC_ensureFirstChild()
+ {
+ // If the last displayed node is the selected node
+ if (this.currentIndex == this.nodeHierarchy.length - 1) {
+ let node = this.nodeHierarchy[this.currentIndex].node;
+ let child = this.getInterestingFirstNode(node);
+ // If the node has a child
+ if (child) {
+ // Show this child
+ this.expand(child);
+ }
+ }
+ },
+
+ /**
+ * Ensure the selected node is visible.
+ */
+ scroll: function BC_scroll()
+ {
+ // FIXME bug 684352: make sure its immediate neighbors are visible too.
+
+ let scrollbox = this.container;
+ let element = this.nodeHierarchy[this.currentIndex].button;
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ this.chromeWin.clearTimeout(this._ensureVisibleTimeout);
+ this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() {
+ scrollbox.ensureElementIsVisible(element);
+ }, ENSURE_SELECTION_VISIBLE_DELAY);
+ },
+
+ updateSelectors: function BC_updateSelectors()
+ {
+ for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
+ let crumb = this.nodeHierarchy[i];
+ let button = crumb.button;
+
+ while(button.hasChildNodes()) {
+ button.removeChild(button.firstChild);
+ }
+ button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
+ button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node));
+ }
+ },
+
+ /**
+ * Update the breadcrumbs display when a new node is selected.
+ */
+ update: function BC_update()
+ {
+ this.inspector.hideNodeMenu();
+
+ let cmdDispatcher = this.chromeDoc.commandDispatcher;
+ this.hadFocus = (cmdDispatcher.focusedElement &&
+ cmdDispatcher.focusedElement.parentNode == this.container);
+
+ if (!this.selection.isConnected()) {
+ this.cutAfter(-1); // remove all the crumbs
+ return;
+ }
+
+ if (!this.selection.isElementNode()) {
+ this.setCursor(-1); // no selection
+ return;
+ }
+
+ let idx = this.indexOf(this.selection.node);
+
+ // Is the node already displayed in the breadcrumbs?
+ if (idx > -1) {
+ // 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.DOMHelpers.getParentObject(this.selection.node);
+ let idx = this.getCommonAncestor(parent);
+ this.cutAfter(idx);
+ }
+ // we append the missing button between the end of the breadcrumbs display
+ // and the current node.
+ this.expand(this.selection.node);
+
+ // we select the current node button
+ idx = this.indexOf(this.selection.node);
+ this.setCursor(idx);
+ }
+ // Add the first child of the very last node of the breadcrumbs if possible.
+ this.ensureFirstChild();
+ this.updateSelectors();
+
+ // Make sure the selected node and its neighbours are visible.
+ this.scroll();
+ },
+}
+
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
diff --git a/browser/devtools/inspector/highlighter.js b/browser/devtools/inspector/highlighter.js
new file mode 100644
index 000000000..753c882eb
--- /dev/null
+++ b/browser/devtools/inspector/highlighter.js
@@ -0,0 +1,798 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cu, Cc, Ci} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let EventEmitter = require("devtools/shared/event-emitter");
+
+const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+ // add ":visited" and ":link" after bug 713106 is fixed
+
+/**
+ * A highlighter mechanism.
+ *
+ * The highlighter is built dynamically into the browser element.
+ * The caller is in charge of destroying the highlighter (ie, the highlighter
+ * won't be destroyed if a new tab is selected for example).
+ *
+ * API:
+ *
+ * // Constructor and destructor.
+ * Highlighter(aTab, aInspector)
+ * void destroy();
+ *
+ * // Show and hide the highlighter
+ * void show();
+ * void hide();
+ * boolean isHidden();
+ *
+ * // Redraw the highlighter if the visible portion of the node has changed.
+ * void invalidateSize(aScroll);
+ *
+ * Events:
+ *
+ * "closed" - Highlighter is closing
+ * "highlighting" - Highlighter is highlighting
+ * "locked" - The selected node has been locked
+ * "unlocked" - The selected ndoe has been unlocked
+ *
+ * Structure:
+ * <stack class="highlighter-container">
+ * <box class="highlighter-outline-container">
+ * <box class="highlighter-outline" locked="true/false"/>
+ * </box>
+ * <box class="highlighter-controls">
+ * <box class="highlighter-nodeinfobar-container" position="top/bottom" locked="true/false">
+ * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"/>
+ * <hbox class="highlighter-nodeinfobar">
+ * <toolbarbutton class="highlighter-nodeinfobar-inspectbutton highlighter-nodeinfobar-button"/>
+ * <hbox class="highlighter-nodeinfobar-text">tagname#id.class1.class2</hbox>
+ * <toolbarbutton class="highlighter-nodeinfobar-menu highlighter-nodeinfobar-button">…</toolbarbutton>
+ * </hbox>
+ * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
+ * </box>
+ * </box>
+ * </stack>
+ *
+ */
+
+
+/**
+ * Constructor.
+ *
+ * @param aTarget The inspection target.
+ * @param aInspector Inspector panel.
+ * @param aToolbox The toolbox holding the inspector.
+ */
+function Highlighter(aTarget, aInspector, aToolbox)
+{
+ this.target = aTarget;
+ this.tab = aTarget.tab;
+ this.toolbox = aToolbox;
+ this.browser = this.tab.linkedBrowser;
+ this.chromeDoc = this.tab.ownerDocument;
+ this.chromeWin = this.chromeDoc.defaultView;
+ this.inspector = aInspector
+
+ EventEmitter.decorate(this);
+
+ this._init();
+}
+
+exports.Highlighter = Highlighter;
+
+Highlighter.prototype = {
+ get selection() {
+ return this.inspector.selection;
+ },
+
+ _init: function Highlighter__init()
+ {
+ this.unlockAndFocus = this.unlockAndFocus.bind(this);
+ this.updateInfobar = this.updateInfobar.bind(this);
+ this.highlight = this.highlight.bind(this);
+
+ let stack = this.browser.parentNode;
+ this.win = this.browser.contentWindow;
+ this._highlighting = false;
+
+ this.highlighterContainer = this.chromeDoc.createElement("stack");
+ this.highlighterContainer.className = "highlighter-container";
+
+ this.outline = this.chromeDoc.createElement("box");
+ this.outline.className = "highlighter-outline";
+
+ let outlineContainer = this.chromeDoc.createElement("box");
+ outlineContainer.appendChild(this.outline);
+ outlineContainer.className = "highlighter-outline-container";
+
+ // The controlsBox will host the different interactive
+ // elements of the highlighter (buttons, toolbars, ...).
+ let controlsBox = this.chromeDoc.createElement("box");
+ controlsBox.className = "highlighter-controls";
+ this.highlighterContainer.appendChild(outlineContainer);
+ this.highlighterContainer.appendChild(controlsBox);
+
+ // Insert the highlighter right after the browser
+ stack.insertBefore(this.highlighterContainer, stack.childNodes[1]);
+
+ this.buildInfobar(controlsBox);
+
+ this.transitionDisabler = null;
+ this.pageEventsMuter = null;
+
+ this.unlockAndFocus();
+
+ this.selection.on("new-node", this.highlight);
+ this.selection.on("new-node", this.updateInfobar);
+ this.selection.on("pseudoclass", this.updateInfobar);
+ this.selection.on("attribute-changed", this.updateInfobar);
+
+ this.onToolSelected = function(event, id) {
+ if (id != "inspector") {
+ this.chromeWin.clearTimeout(this.pageEventsMuter);
+ this.detachMouseListeners();
+ this.disabled = true;
+ this.hide();
+ } else {
+ if (!this.locked) {
+ this.attachMouseListeners();
+ }
+ this.disabled = false;
+ this.show();
+ }
+ }.bind(this);
+ this.toolbox.on("select", this.onToolSelected);
+
+ this.hidden = true;
+ this.highlight();
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function Highlighter_destroy()
+ {
+ this.inspectButton.removeEventListener("command", this.unlockAndFocus);
+ this.inspectButton = null;
+
+ this.toolbox.off("select", this.onToolSelected);
+ this.toolbox = null;
+
+ this.selection.off("new-node", this.highlight);
+ this.selection.off("new-node", this.updateInfobar);
+ this.selection.off("pseudoclass", this.updateInfobar);
+ this.selection.off("attribute-changed", this.updateInfobar);
+
+ this.detachMouseListeners();
+ this.detachPageListeners();
+
+ this.chromeWin.clearTimeout(this.transitionDisabler);
+ this.chromeWin.clearTimeout(this.pageEventsMuter);
+ this.boundCloseEventHandler = null;
+ this._contentRect = null;
+ this._highlightRect = null;
+ this._highlighting = false;
+ this.outline = null;
+ this.nodeInfo = null;
+ this.highlighterContainer.parentNode.removeChild(this.highlighterContainer);
+ this.highlighterContainer = null;
+ this.win = null
+ this.browser = null;
+ this.chromeDoc = null;
+ this.chromeWin = null;
+ this.tabbrowser = null;
+
+ this.emit("closed");
+ },
+
+ /**
+ * Show the outline, and select a node.
+ */
+ highlight: function Highlighter_highlight()
+ {
+ if (this.selection.reason != "highlighter") {
+ this.lock();
+ }
+
+ let canHighlightNode = this.selection.isNode() &&
+ this.selection.isConnected() &&
+ this.selection.isElementNode();
+
+ if (canHighlightNode) {
+ if (this.selection.reason != "navigateaway") {
+ this.disabled = false;
+ }
+ this.show();
+ this.updateInfobar();
+ this.invalidateSize();
+ if (!this._highlighting &&
+ this.selection.reason != "highlighter") {
+ LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
+ }
+ } else {
+ this.disabled = true;
+ this.hide();
+ }
+ },
+
+ /**
+ * Update the highlighter size and position.
+ */
+ invalidateSize: function Highlighter_invalidateSize()
+ {
+ let canHiglightNode = this.selection.isNode() &&
+ this.selection.isConnected() &&
+ this.selection.isElementNode();
+
+ if (!canHiglightNode)
+ return;
+
+ let clientRect = this.selection.node.getBoundingClientRect();
+ let rect = LayoutHelpers.getDirtyRect(this.selection.node);
+ this.highlightRectangle(rect);
+
+ this.moveInfobar();
+
+ if (this._highlighting) {
+ this.showOutline();
+ this.emit("highlighting");
+ }
+ },
+
+ /**
+ * Show the highlighter if it has been hidden.
+ */
+ show: function() {
+ if (!this.hidden || this.disabled) return;
+ this.showOutline();
+ this.showInfobar();
+ this.computeZoomFactor();
+ this.attachPageListeners();
+ this.invalidateSize();
+ this.hidden = false;
+ },
+
+ /**
+ * Hide the highlighter, the outline and the infobar.
+ */
+ hide: function() {
+ if (this.hidden) return;
+ this.hideOutline();
+ this.hideInfobar();
+ this.detachPageListeners();
+ this.hidden = true;
+ },
+
+ /**
+ * Is the highlighter visible?
+ *
+ * @return boolean
+ */
+ isHidden: function() {
+ return this.hidden;
+ },
+
+ /**
+ * Lock a node. Stops the inspection.
+ */
+ lock: function() {
+ if (this.locked === true) return;
+ this.outline.setAttribute("locked", "true");
+ this.nodeInfo.container.setAttribute("locked", "true");
+ this.detachMouseListeners();
+ this.locked = true;
+ this.emit("locked");
+ },
+
+ /**
+ * Start inspecting.
+ * Unlock the current node (if any), and select any node being hovered.
+ */
+ unlock: function() {
+ if (this.locked === false) return;
+ this.outline.removeAttribute("locked");
+ this.nodeInfo.container.removeAttribute("locked");
+ this.attachMouseListeners();
+ this.locked = false;
+ if (this.selection.isElementNode() &&
+ this.selection.isConnected()) {
+ this.showOutline();
+ }
+ this.emit("unlocked");
+ },
+
+ /**
+ * Focus the browser before unlocking.
+ */
+ unlockAndFocus: function Highlighter_unlockAndFocus() {
+ if (this.locked === false) return;
+ this.chromeWin.focus();
+ this.unlock();
+ },
+
+ /**
+ * Hide the infobar
+ */
+ hideInfobar: function Highlighter_hideInfobar() {
+ this.nodeInfo.container.setAttribute("force-transitions", "true");
+ this.nodeInfo.container.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Show the infobar
+ */
+ showInfobar: function Highlighter_showInfobar() {
+ this.nodeInfo.container.removeAttribute("hidden");
+ this.moveInfobar();
+ this.nodeInfo.container.removeAttribute("force-transitions");
+ },
+
+ /**
+ * Hide the outline
+ */
+ hideOutline: function Highlighter_hideOutline() {
+ this.outline.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Show the outline
+ */
+ showOutline: function Highlighter_showOutline() {
+ if (this._highlighting)
+ this.outline.removeAttribute("hidden");
+ },
+
+ /**
+ * Build the node Infobar.
+ *
+ * <box class="highlighter-nodeinfobar-container">
+ * <box class="Highlighter-nodeinfobar-arrow-top"/>
+ * <hbox class="highlighter-nodeinfobar">
+ * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton"/>
+ * <hbox class="highlighter-nodeinfobar-text">
+ * <xhtml:span class="highlighter-nodeinfobar-tagname"/>
+ * <xhtml:span class="highlighter-nodeinfobar-id"/>
+ * <xhtml:span class="highlighter-nodeinfobar-classes"/>
+ * <xhtml:span class="highlighter-nodeinfobar-pseudo-classes"/>
+ * </hbox>
+ * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-menu"/>
+ * </hbox>
+ * <box class="Highlighter-nodeinfobar-arrow-bottom"/>
+ * </box>
+ *
+ * @param nsIDOMElement aParent
+ * The container of the infobar.
+ */
+ buildInfobar: function Highlighter_buildInfobar(aParent)
+ {
+ let container = this.chromeDoc.createElement("box");
+ container.className = "highlighter-nodeinfobar-container";
+ container.setAttribute("position", "top");
+ container.setAttribute("disabled", "true");
+
+ let nodeInfobar = this.chromeDoc.createElement("hbox");
+ nodeInfobar.className = "highlighter-nodeinfobar";
+
+ let arrowBoxTop = this.chromeDoc.createElement("box");
+ arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top";
+
+ let arrowBoxBottom = this.chromeDoc.createElement("box");
+ arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom";
+
+ let tagNameLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ tagNameLabel.className = "highlighter-nodeinfobar-tagname";
+
+ let idLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ idLabel.className = "highlighter-nodeinfobar-id";
+
+ let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ classesBox.className = "highlighter-nodeinfobar-classes";
+
+ let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes";
+
+ // Add some content to force a better boundingClientRect down below.
+ pseudoClassesBox.textContent = "&nbsp;";
+
+ // Create buttons
+
+ this.inspectButton = this.chromeDoc.createElement("toolbarbutton");
+ this.inspectButton.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton"
+ let toolbarInspectButton = this.inspector.panelDoc.getElementById("inspector-inspect-toolbutton");
+ this.inspectButton.setAttribute("tooltiptext", toolbarInspectButton.getAttribute("tooltiptext"));
+ this.inspectButton.addEventListener("command", this.unlockAndFocus);
+
+ let nodemenu = this.chromeDoc.createElement("toolbarbutton");
+ nodemenu.setAttribute("type", "menu");
+ nodemenu.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-menu"
+ nodemenu.setAttribute("tooltiptext",
+ this.strings.GetStringFromName("nodeMenu.tooltiptext"));
+
+ nodemenu.onclick = function() {
+ this.inspector.showNodeMenu(nodemenu, "after_start");
+ }.bind(this);
+
+ // <hbox class="highlighter-nodeinfobar-text"/>
+ let texthbox = this.chromeDoc.createElement("hbox");
+ texthbox.className = "highlighter-nodeinfobar-text";
+ texthbox.setAttribute("align", "center");
+ texthbox.setAttribute("flex", "1");
+
+ texthbox.addEventListener("mousedown", function(aEvent) {
+ // On click, show the node:
+ if (this.selection.isElementNode()) {
+ LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
+ }
+ }.bind(this), true);
+
+ texthbox.appendChild(tagNameLabel);
+ texthbox.appendChild(idLabel);
+ texthbox.appendChild(classesBox);
+ texthbox.appendChild(pseudoClassesBox);
+
+ nodeInfobar.appendChild(this.inspectButton);
+ nodeInfobar.appendChild(texthbox);
+ nodeInfobar.appendChild(nodemenu);
+
+ container.appendChild(arrowBoxTop);
+ container.appendChild(nodeInfobar);
+ container.appendChild(arrowBoxBottom);
+
+ aParent.appendChild(container);
+
+ let barHeight = container.getBoundingClientRect().height;
+
+ this.nodeInfo = {
+ tagNameLabel: tagNameLabel,
+ idLabel: idLabel,
+ classesBox: classesBox,
+ pseudoClassesBox: pseudoClassesBox,
+ container: container,
+ barHeight: barHeight,
+ };
+ },
+
+ /**
+ * Highlight a rectangular region.
+ *
+ * @param object aRect
+ * The rectangle region to highlight.
+ * @returns boolean
+ * True if the rectangle was highlighted, false otherwise.
+ */
+ highlightRectangle: function Highlighter_highlightRectangle(aRect)
+ {
+ if (!aRect) {
+ this.unhighlight();
+ return;
+ }
+
+ let oldRect = this._contentRect;
+
+ if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
+ aRect.width == oldRect.width && aRect.height == oldRect.height) {
+ return; // same rectangle
+ }
+
+ let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect);
+
+ if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
+ aRectScaled.width > 0 && aRectScaled.height > 0) {
+
+ this.showOutline();
+
+ // The bottom div and the right div are flexibles (flex=1).
+ // We don't need to resize them.
+ let top = "top:" + aRectScaled.top + "px;";
+ let left = "left:" + aRectScaled.left + "px;";
+ let width = "width:" + aRectScaled.width + "px;";
+ let height = "height:" + aRectScaled.height + "px;";
+ this.outline.setAttribute("style", top + left + width + height);
+
+ this._highlighting = true;
+ } else {
+ this.unhighlight();
+ }
+
+ this._contentRect = aRect; // save orig (non-scaled) rect
+ this._highlightRect = aRectScaled; // and save the scaled rect.
+
+ return;
+ },
+
+ /**
+ * Clear the highlighter surface.
+ */
+ unhighlight: function Highlighter_unhighlight()
+ {
+ this._highlighting = false;
+ this.hideOutline();
+ },
+
+ /**
+ * Update node information (tagName#id.class)
+ */
+ updateInfobar: function Highlighter_updateInfobar()
+ {
+ if (!this.selection.isElementNode()) {
+ this.nodeInfo.tagNameLabel.textContent = "";
+ this.nodeInfo.idLabel.textContent = "";
+ this.nodeInfo.classesBox.textContent = "";
+ this.nodeInfo.pseudoClassesBox.textContent = "";
+ return;
+ }
+
+ let node = this.selection.node;
+
+ // Tag name
+ this.nodeInfo.tagNameLabel.textContent = node.tagName;
+
+ // ID
+ this.nodeInfo.idLabel.textContent = node.id ? "#" + node.id : "";
+
+ // Classes
+ let classes = this.nodeInfo.classesBox;
+
+ classes.textContent = node.classList.length ?
+ "." + Array.join(node.classList, ".") : "";
+
+ // Pseudo-classes
+ let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
+ return DOMUtils.hasPseudoClassLock(node, pseudo);
+ }, this);
+
+ let pseudoBox = this.nodeInfo.pseudoClassesBox;
+ pseudoBox.textContent = pseudos.join("");
+ },
+
+ /**
+ * Move the Infobar to the right place in the highlighter.
+ */
+ moveInfobar: function Highlighter_moveInfobar()
+ {
+ if (this._highlightRect) {
+ let winHeight = this.win.innerHeight * this.zoom;
+ let winWidth = this.win.innerWidth * this.zoom;
+
+ let rect = {top: this._highlightRect.top,
+ left: this._highlightRect.left,
+ width: this._highlightRect.width,
+ height: this._highlightRect.height};
+
+ rect.top = Math.max(rect.top, 0);
+ rect.left = Math.max(rect.left, 0);
+ rect.width = Math.max(rect.width, 0);
+ rect.height = Math.max(rect.height, 0);
+
+ rect.top = Math.min(rect.top, winHeight);
+ rect.left = Math.min(rect.left, winWidth);
+
+ this.nodeInfo.container.removeAttribute("disabled");
+ // Can the bar be above the node?
+ if (rect.top < this.nodeInfo.barHeight) {
+ // No. Can we move the toolbar under the node?
+ if (rect.top + rect.height +
+ this.nodeInfo.barHeight > winHeight) {
+ // No. Let's move it inside.
+ this.nodeInfo.container.style.top = rect.top + "px";
+ this.nodeInfo.container.setAttribute("position", "overlap");
+ } else {
+ // Yes. Let's move it under the node.
+ this.nodeInfo.container.style.top = rect.top + rect.height + "px";
+ this.nodeInfo.container.setAttribute("position", "bottom");
+ }
+ } else {
+ // Yes. Let's move it on top of the node.
+ this.nodeInfo.container.style.top =
+ rect.top - this.nodeInfo.barHeight + "px";
+ this.nodeInfo.container.setAttribute("position", "top");
+ }
+
+ let barWidth = this.nodeInfo.container.getBoundingClientRect().width;
+ let left = rect.left + rect.width / 2 - barWidth / 2;
+
+ // Make sure the whole infobar is visible
+ if (left < 0) {
+ left = 0;
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ } else {
+ if (left + barWidth > winWidth) {
+ left = winWidth - barWidth;
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ } else {
+ this.nodeInfo.container.removeAttribute("hide-arrow");
+ }
+ }
+ this.nodeInfo.container.style.left = left + "px";
+ } else {
+ this.nodeInfo.container.style.left = "0";
+ this.nodeInfo.container.style.top = "0";
+ this.nodeInfo.container.setAttribute("position", "top");
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ }
+ },
+
+ /**
+ * Store page zoom factor.
+ */
+ computeZoomFactor: function Highlighter_computeZoomFactor() {
+ this.zoom =
+ this.win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .fullZoom;
+ },
+
+ /////////////////////////////////////////////////////////////////////////
+ //// Event Handling
+
+ attachMouseListeners: function Highlighter_attachMouseListeners()
+ {
+ this.browser.addEventListener("mousemove", this, true);
+ this.browser.addEventListener("click", this, true);
+ this.browser.addEventListener("dblclick", this, true);
+ this.browser.addEventListener("mousedown", this, true);
+ this.browser.addEventListener("mouseup", this, true);
+ },
+
+ detachMouseListeners: function Highlighter_detachMouseListeners()
+ {
+ this.browser.removeEventListener("mousemove", this, true);
+ this.browser.removeEventListener("click", this, true);
+ this.browser.removeEventListener("dblclick", this, true);
+ this.browser.removeEventListener("mousedown", this, true);
+ this.browser.removeEventListener("mouseup", this, true);
+ },
+
+ attachPageListeners: function Highlighter_attachPageListeners()
+ {
+ this.browser.addEventListener("resize", this, true);
+ this.browser.addEventListener("scroll", this, true);
+ this.browser.addEventListener("MozAfterPaint", this, true);
+ },
+
+ detachPageListeners: function Highlighter_detachPageListeners()
+ {
+ this.browser.removeEventListener("resize", this, true);
+ this.browser.removeEventListener("scroll", this, true);
+ this.browser.removeEventListener("MozAfterPaint", this, true);
+ },
+
+ /**
+ * Generic event handler.
+ *
+ * @param nsIDOMEvent aEvent
+ * The DOM event object.
+ */
+ handleEvent: function Highlighter_handleEvent(aEvent)
+ {
+ switch (aEvent.type) {
+ case "click":
+ this.handleClick(aEvent);
+ break;
+ case "mousemove":
+ this.brieflyIgnorePageEvents();
+ this.handleMouseMove(aEvent);
+ break;
+ case "resize":
+ this.computeZoomFactor();
+ break;
+ case "MozAfterPaint":
+ case "scroll":
+ this.brieflyDisableTransitions();
+ this.invalidateSize();
+ break;
+ case "dblclick":
+ case "mousedown":
+ case "mouseup":
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ },
+
+ /**
+ * Disable the CSS transitions for a short time to avoid laggy animations
+ * during scrolling or resizing.
+ */
+ brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
+ {
+ if (this.transitionDisabler) {
+ this.chromeWin.clearTimeout(this.transitionDisabler);
+ } else {
+ this.outline.setAttribute("disable-transitions", "true");
+ this.nodeInfo.container.setAttribute("disable-transitions", "true");
+ }
+ this.transitionDisabler =
+ this.chromeWin.setTimeout(function() {
+ this.outline.removeAttribute("disable-transitions");
+ this.nodeInfo.container.removeAttribute("disable-transitions");
+ this.transitionDisabler = null;
+ }.bind(this), 500);
+ },
+
+ /**
+ * Don't listen to page events while inspecting with the mouse.
+ */
+ brieflyIgnorePageEvents: function Highlighter_brieflyIgnorePageEvents()
+ {
+ // The goal is to keep smooth animations while inspecting.
+ // CSS Transitions might be interrupted because of a MozAfterPaint
+ // event that would triger an invalidateSize() call.
+ // So we don't listen to events that would trigger an invalidateSize()
+ // call.
+ //
+ // Side effect, zoom levels are not updated during this short period.
+ // It's very unlikely this would happen, but just in case, we call
+ // computeZoomFactor() when reattaching the events.
+ if (this.pageEventsMuter) {
+ this.chromeWin.clearTimeout(this.pageEventsMuter);
+ } else {
+ this.detachPageListeners();
+ }
+ this.pageEventsMuter =
+ this.chromeWin.setTimeout(function() {
+ this.attachPageListeners();
+ // Just in case the zoom level changed while ignoring the paint events
+ this.computeZoomFactor();
+ this.pageEventsMuter = null;
+ }.bind(this), 500);
+ },
+
+ /**
+ * Handle clicks.
+ *
+ * @param nsIDOMEvent aEvent
+ * The DOM event.
+ */
+ handleClick: function Highlighter_handleClick(aEvent)
+ {
+ // Stop inspection when the user clicks on a node.
+ if (aEvent.button == 0) {
+ this.lock();
+ let node = this.selection.node;
+ this.selection.setNode(node, "highlighter-lock");
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ },
+
+ /**
+ * Handle mousemoves in panel.
+ *
+ * @param nsiDOMEvent aEvent
+ * The MouseEvent triggering the method.
+ */
+ handleMouseMove: function Highlighter_handleMouseMove(aEvent)
+ {
+ let doc = aEvent.target.ownerDocument;
+
+ // This should never happen, but just in case, we don't let the
+ // highlighter highlight browser nodes.
+ if (doc && doc != this.chromeDoc) {
+ let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
+ aEvent.clientX, aEvent.clientY);
+ if (element && element != this.selection.node) {
+ this.selection.setNode(element, "highlighter");
+ }
+ }
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
+});
+
+XPCOMUtils.defineLazyGetter(Highlighter.prototype, "strings", function () {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/inspector.properties");
+});
diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js
new file mode 100644
index 000000000..5d809cfa0
--- /dev/null
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -0,0 +1,657 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let Promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+let {CssLogic} = require("devtools/styleinspector/css-logic");
+
+loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView);
+loader.lazyGetter(this, "Selection", () => require ("devtools/inspector/selection").Selection);
+loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs);
+loader.lazyGetter(this, "Highlighter", () => require("devtools/inspector/highlighter").Highlighter);
+loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar);
+loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch);
+
+const LAYOUT_CHANGE_TIMER = 250;
+
+/**
+ * Represents an open instance of the Inspector for a tab.
+ * The inspector controls the highlighter, the breadcrumbs,
+ * the markup view, and the sidebar (computed view, rule view
+ * and layout view).
+ */
+function InspectorPanel(iframeWindow, toolbox) {
+ this._toolbox = toolbox;
+ this._target = toolbox._target;
+ this.panelDoc = iframeWindow.document;
+ this.panelWin = iframeWindow;
+ this.panelWin.inspector = this;
+
+ EventEmitter.decorate(this);
+}
+
+exports.InspectorPanel = InspectorPanel;
+
+InspectorPanel.prototype = {
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ open: function InspectorPanel_open() {
+ let deferred = Promise.defer();
+
+ this.onNavigatedAway = this.onNavigatedAway.bind(this);
+ this.target.on("navigate", this.onNavigatedAway);
+
+ this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
+ this.lastNodemenuItem = this.nodemenu.lastChild;
+ this._setupNodeMenu = this._setupNodeMenu.bind(this);
+ this._resetNodeMenu = this._resetNodeMenu.bind(this);
+ this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
+ this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
+
+ // Create an empty selection
+ this._selection = new Selection();
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.selection.on("new-node", this.onNewSelection);
+ this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
+ this.selection.on("before-new-node", this.onBeforeNewSelection);
+ this.onDetached = this.onDetached.bind(this);
+ this.selection.on("detached", this.onDetached);
+
+ this.breadcrumbs = new HTMLBreadcrumbs(this);
+
+ if (this.target.isLocalTab) {
+ this.browser = this.target.tab.linkedBrowser;
+ this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
+ this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
+
+ this.highlighter = new Highlighter(this.target, this, this._toolbox);
+ let button = this.panelDoc.getElementById("inspector-inspect-toolbutton");
+ button.hidden = false;
+ this.onLockStateChanged = function() {
+ if (this.highlighter.locked) {
+ button.removeAttribute("checked");
+ this._toolbox.raise();
+ } else {
+ button.setAttribute("checked", "true");
+ }
+ }.bind(this);
+ this.highlighter.on("locked", this.onLockStateChanged);
+ this.highlighter.on("unlocked", this.onLockStateChanged);
+
+ // Show a warning when the debugger is paused.
+ // We show the warning only when the inspector
+ // is selected.
+ this.updateDebuggerPausedWarning = function() {
+ let notificationBox = this._toolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
+ if (!notification && this._toolbox.currentToolId == "inspector" &&
+ this.target.isThreadPaused) {
+ let message = this.strings.GetStringFromName("debuggerPausedWarning.message");
+ notificationBox.appendNotification(message,
+ "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
+ }
+
+ if (notification && this._toolbox.currentToolId != "inspector") {
+ notificationBox.removeNotification(notification);
+ }
+
+ if (notification && !this.target.isThreadPaused) {
+ notificationBox.removeNotification(notification);
+ }
+
+ }.bind(this);
+ 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", function() {
+ this.isReady = true;
+
+ // All the components are initialized. Let's select a node.
+ if (this.target.isLocalTab) {
+ this._selection.setNode(
+ this._getDefaultNodeForSelection(this.browser.contentDocument));
+ } else if (this.target.window) {
+ this._selection.setNode(
+ this._getDefaultNodeForSelection(this.target.window.document));
+ }
+
+ if (this.highlighter) {
+ this.highlighter.unlock();
+ }
+
+ this.markup.expandNode(this.selection.node);
+
+ this.emit("ready");
+ deferred.resolve(this);
+ }.bind(this));
+
+ this.setupSearchBox();
+ this.setupSidebar();
+
+ return deferred.promise;
+ },
+
+ /**
+ * Select node for default selection
+ */
+ _getDefaultNodeForSelection : function(document) {
+ // if available set body node as default selected node
+ // else set documentElement
+ var defaultNode = document.body || document.documentElement;
+ return defaultNode;
+ },
+
+ /**
+ * Selection object (read only)
+ */
+ get selection() {
+ return this._selection;
+ },
+
+ /**
+ * Target getter.
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Target setter.
+ */
+ set target(value) {
+ this._target = value;
+ },
+
+ /**
+ * Expose gViewSourceUtils so that other tools can make use of them.
+ */
+ get viewSourceUtils() {
+ return this.panelWin.gViewSourceUtils;
+ },
+
+ /**
+ * 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 InspectorPanel_markDirty() {
+ this.isDirty = true;
+ },
+
+ /**
+ * Hooks the searchbar to show result and auto completion suggestions.
+ */
+ setupSearchBox: function InspectorPanel_setupSearchBox() {
+ let searchDoc;
+ if (this.target.isLocalTab) {
+ searchDoc = this.browser.contentDocument;
+ } else if (this.target.window) {
+ searchDoc = this.target.window.document;
+ } else {
+ return;
+ }
+ // Initiate the selectors search object.
+ let setNodeFunction = function(node) {
+ this.selection.setNode(node, "selectorsearch");
+ }.bind(this);
+ if (this.searchSuggestions) {
+ this.searchSuggestions.destroy();
+ this.searchSuggestions = null;
+ }
+ this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
+ this.searchSuggestions = new SelectorSearch(searchDoc, this.searchBox, setNodeFunction);
+ },
+
+ /**
+ * Build the sidebar.
+ */
+ setupSidebar: function InspectorPanel_setupSidebar() {
+ let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
+ this.sidebar = new ToolSidebar(tabbox, this, "inspector");
+
+ let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
+
+ this._setDefaultSidebar = function(event, toolId) {
+ Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
+ }.bind(this);
+
+ this.sidebar.on("select", this._setDefaultSidebar);
+ this.toggleHighlighter = this.toggleHighlighter.bind(this);
+
+ this.sidebar.addTab("ruleview",
+ "chrome://browser/content/devtools/cssruleview.xhtml",
+ "ruleview" == defaultTab);
+
+ this.sidebar.addTab("computedview",
+ "chrome://browser/content/devtools/computedview.xhtml",
+ "computedview" == defaultTab);
+
+ if (Services.prefs.getBoolPref("devtools.fontinspector.enabled")) {
+ this.sidebar.addTab("fontinspector",
+ "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml",
+ "fontinspector" == defaultTab);
+ }
+
+ this.sidebar.addTab("layoutview",
+ "chrome://browser/content/devtools/layoutview/view.xhtml",
+ "layoutview" == defaultTab);
+
+ let ruleViewTab = this.sidebar.getTab("ruleview");
+ ruleViewTab.addEventListener("mouseover", this.toggleHighlighter, false);
+ ruleViewTab.addEventListener("mouseout", this.toggleHighlighter, false);
+
+ this.sidebar.show();
+ },
+
+ /**
+ * Reset the inspector on navigate away.
+ */
+ onNavigatedAway: function InspectorPanel_onNavigatedAway(event, payload) {
+ let newWindow = payload._navPayload || payload;
+ this.selection.setNode(null);
+ this._destroyMarkup();
+ this.isDirty = false;
+
+ let onDOMReady = function() {
+ newWindow.removeEventListener("DOMContentLoaded", onDOMReady, true);
+
+ if (this._destroyed) {
+ return;
+ }
+
+ if (!this.selection.node) {
+ let defaultNode = this._getDefaultNodeForSelection(newWindow.document);
+ this.selection.setNode(defaultNode, "navigateaway");
+ }
+ this._initMarkup();
+
+ this.once("markuploaded", () => {
+ this.markup.expandNode(this.selection.node);
+ });
+
+ this.setupSearchBox();
+ }.bind(this);
+
+ if (newWindow.document.readyState == "loading") {
+ newWindow.addEventListener("DOMContentLoaded", onDOMReady, true);
+ } else {
+ onDOMReady();
+ }
+ },
+
+ /**
+ * When a new node is selected.
+ */
+ onNewSelection: function InspectorPanel_onNewSelection() {
+ this.cancelLayoutChange();
+ },
+
+ /**
+ * When a new node is selected, before the selection has changed.
+ */
+ onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event,
+ node) {
+ if (this.breadcrumbs.indexOf(node) == -1) {
+ // only clear locks if we'd have to update breadcrumbs
+ this.clearPseudoClasses();
+ }
+ },
+
+ /**
+ * When a node is deleted, select its parent node.
+ */
+ onDetached: function InspectorPanel_onDetached(event, parentNode) {
+ this.cancelLayoutChange();
+ this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
+ this.selection.setNode(parentNode, "detached");
+ },
+
+ /**
+ * Destroy the inspector.
+ */
+ destroy: function InspectorPanel__destroy() {
+ if (this._destroyed) {
+ return Promise.resolve(null);
+ }
+ this._destroyed = true;
+
+ this.cancelLayoutChange();
+
+ if (this.browser) {
+ this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
+ this.browser = null;
+ }
+
+ this.target.off("navigate", this.onNavigatedAway);
+
+ if (this.highlighter) {
+ this.highlighter.off("locked", this.onLockStateChanged);
+ this.highlighter.off("unlocked", this.onLockStateChanged);
+ this.highlighter.destroy();
+ }
+
+ this.target.off("thread-paused", this.updateDebuggerPausedWarning);
+ this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
+ this._toolbox.off("select", this.updateDebuggerPausedWarning);
+
+ this._toolbox = null;
+
+ this.sidebar.off("select", this._setDefaultSidebar);
+ this.sidebar.destroy();
+ this.sidebar = null;
+
+ this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
+ this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
+ this.breadcrumbs.destroy();
+ this.searchSuggestions.destroy();
+ this.selection.off("new-node", this.onNewSelection);
+ this.selection.off("before-new-node", this.onBeforeNewSelection);
+ this.selection.off("detached", this.onDetached);
+ this._destroyMarkup();
+ this._selection.destroy();
+ this._selection = null;
+ this.panelWin.inspector = null;
+ this.target = null;
+ this.panelDoc = null;
+ this.panelWin = null;
+ this.breadcrumbs = null;
+ this.searchSuggestions = null;
+ this.lastNodemenuItem = null;
+ this.nodemenu = null;
+ this.highlighter = null;
+
+ return Promise.resolve(null);
+ },
+
+ /**
+ * Show the node menu.
+ */
+ showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) {
+ if (aExtraItems) {
+ for (let item of aExtraItems) {
+ this.nodemenu.appendChild(item);
+ }
+ }
+ this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false);
+ },
+
+ hideNodeMenu: function InspectorPanel_hideNodeMenu() {
+ this.nodemenu.hidePopup();
+ },
+
+ /**
+ * Disable the delete item if needed. Update the pseudo classes.
+ */
+ _setupNodeMenu: function InspectorPanel_setupNodeMenu() {
+ // Set the pseudo classes
+ for (let name of ["hover", "active", "focus"]) {
+ let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
+
+ if (this.selection.isElementNode()) {
+ let checked = DOMUtils.hasPseudoClassLock(this.selection.node, ":" + name);
+ menu.setAttribute("checked", checked);
+ menu.removeAttribute("disabled");
+ } else {
+ menu.setAttribute("disabled", "true");
+ }
+ }
+
+ // Disable delete item if needed
+ let deleteNode = this.panelDoc.getElementById("node-menu-delete");
+ if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) {
+ deleteNode.setAttribute("disabled", "true");
+ } else {
+ deleteNode.removeAttribute("disabled");
+ }
+
+ // Disable / enable "Copy Unique Selector", "Copy inner HTML" &
+ // "Copy outer HTML" as appropriate
+ let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
+ let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
+ let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
+ if (this.selection.isElementNode()) {
+ unique.removeAttribute("disabled");
+ copyInnerHTML.removeAttribute("disabled");
+ copyOuterHTML.removeAttribute("disabled");
+ } else {
+ unique.setAttribute("disabled", "true");
+ copyInnerHTML.setAttribute("disabled", "true");
+ copyOuterHTML.setAttribute("disabled", "true");
+ }
+ },
+
+ _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
+ // Remove any extra items
+ while (this.lastNodemenuItem.nextSibling) {
+ let toDelete = this.lastNodemenuItem.nextSibling;
+ toDelete.parentNode.removeChild(toDelete);
+ }
+ },
+
+ _initMarkup: function InspectorPanel_initMarkup() {
+ 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.setAttribute("context", "inspector-node-popup");
+
+ // This is needed to enable tooltips inside the iframe document.
+ this._boundMarkupFrameLoad = function InspectorPanel_initMarkupPanel_onload() {
+ this._markupFrame.contentWindow.focus();
+ this._onMarkupFrameLoad();
+ }.bind(this);
+ this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
+
+ this._markupBox.setAttribute("hidden", true);
+ this._markupBox.appendChild(this._markupFrame);
+ this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
+ },
+
+ _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
+ this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
+ delete this._boundMarkupFrameLoad;
+
+ this._markupBox.removeAttribute("hidden");
+
+ let controllerWindow = this._toolbox.doc.defaultView;
+ this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
+
+ this.emit("markuploaded");
+ },
+
+ _destroyMarkup: function InspectorPanel__destroyMarkup() {
+ if (this._boundMarkupFrameLoad) {
+ this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
+ delete this._boundMarkupFrameLoad;
+ }
+
+ if (this.markup) {
+ this.markup.destroy();
+ delete this.markup;
+ }
+
+ if (this._markupFrame) {
+ this._markupFrame.parentNode.removeChild(this._markupFrame);
+ delete this._markupFrame;
+ }
+ },
+
+ /**
+ * Toggle a pseudo class.
+ */
+ togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) {
+ if (this.selection.isElementNode()) {
+ if (DOMUtils.hasPseudoClassLock(this.selection.node, aPseudo)) {
+ this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
+ DOMUtils.removePseudoClassLock(crumb.node, aPseudo);
+ });
+ } else {
+ let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
+ let node = this.selection.node;
+ do {
+ DOMUtils.addPseudoClassLock(node, aPseudo);
+ node = node.parentNode;
+ } while (hierarchical && node.parentNode)
+ }
+ }
+ this.selection.emit("pseudoclass");
+ this.breadcrumbs.scroll();
+ },
+
+ /**
+ * Clear any pseudo-class locks applied to the current hierarchy.
+ */
+ clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
+ this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
+ try {
+ DOMUtils.clearPseudoClassLocks(crumb.node);
+ } catch(e) {
+ // Ignore dead nodes after navigation.
+ }
+ });
+ },
+
+ /**
+ * Toggle the highlighter when ruleview is hovered.
+ */
+ toggleHighlighter: function InspectorPanel_toggleHighlighter(event)
+ {
+ if (event.type == "mouseover") {
+ this.highlighter.hide();
+ }
+ else if (event.type == "mouseout") {
+ this.highlighter.show();
+ }
+ },
+
+ /**
+ * Copy the innerHTML of the selected Node to the clipboard.
+ */
+ copyInnerHTML: function InspectorPanel_copyInnerHTML()
+ {
+ if (!this.selection.isNode()) {
+ return;
+ }
+ let toCopy = this.selection.node.innerHTML;
+ if (toCopy) {
+ clipboardHelper.copyString(toCopy);
+ }
+ },
+
+ /**
+ * Copy the outerHTML of the selected Node to the clipboard.
+ */
+ copyOuterHTML: function InspectorPanel_copyOuterHTML()
+ {
+ if (!this.selection.isNode()) {
+ return;
+ }
+ let toCopy = this.selection.node.outerHTML;
+ if (toCopy) {
+ clipboardHelper.copyString(toCopy);
+ }
+ },
+
+ /**
+ * Copy a unique selector of the selected Node to the clipboard.
+ */
+ copyUniqueSelector: function InspectorPanel_copyUniqueSelector()
+ {
+ if (!this.selection.isNode()) {
+ return;
+ }
+
+ let toCopy = CssLogic.findCssSelector(this.selection.node);
+ if (toCopy) {
+ clipboardHelper.copyString(toCopy);
+ }
+ },
+
+ /**
+ * Delete the selected node.
+ */
+ deleteNode: function IUI_deleteNode() {
+ if (!this.selection.isNode() ||
+ this.selection.isRoot()) {
+ return;
+ }
+
+ let toDelete = this.selection.node;
+
+ let parent = this.selection.node.parentNode;
+
+ // 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(toDelete);
+ } else {
+ // remove the node from content
+ parent.removeChild(toDelete);
+ }
+ },
+
+ /**
+ * Schedule a low-priority change event for things like paint
+ * and resize.
+ */
+ scheduleLayoutChange: function Inspector_scheduleLayoutChange()
+ {
+ if (this._timer) {
+ return null;
+ }
+ this._timer = this.panelWin.setTimeout(function() {
+ this.emit("layout-change");
+ this._timer = null;
+ }.bind(this), LAYOUT_CHANGE_TIMER);
+ },
+
+ /**
+ * Cancel a pending low-priority change event if any is
+ * scheduled.
+ */
+ cancelLayoutChange: function Inspector_cancelLayoutChange()
+ {
+ if (this._timer) {
+ this.panelWin.clearTimeout(this._timer);
+ delete this._timer;
+ }
+ },
+
+}
+
+/////////////////////////////////////////////////////////////////////////
+//// Initializers
+
+loader.lazyGetter(InspectorPanel.prototype, "strings",
+ function () {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/inspector.properties");
+ });
+
+loader.lazyGetter(this, "clipboardHelper", function() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+});
+
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
diff --git a/browser/devtools/inspector/inspector.css b/browser/devtools/inspector/inspector.css
new file mode 100644
index 000000000..9e6e889f3
--- /dev/null
+++ b/browser/devtools/inspector/inspector.css
@@ -0,0 +1,28 @@
+/* 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/. */
+
+#inspector-sidebar {
+ min-width: 250px;
+}
+
+#searchbox-panel-listbox {
+ width: 250px;
+ max-width: 250px;
+ overflow-x: hidden;
+}
+
+#searchbox-panel-listbox > richlistitem,
+#searchbox-panel-listbox > richlistitem[selected] {
+ overflow-x: hidden;
+}
+
+#searchbox-panel-listbox > richlistitem > .initial-value {
+ max-width: 130px;
+ margin-left: 15px;
+}
+
+#searchbox-panel-listbox > richlistitem > .autocomplete-value {
+ max-width: 150px;
+}
diff --git a/browser/devtools/inspector/inspector.xul b/browser/devtools/inspector/inspector.xul
new file mode 100644
index 000000000..130f1b75b
--- /dev/null
+++ b/browser/devtools/inspector/inspector.xul
@@ -0,0 +1,95 @@
+<?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://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/inspector/inspector.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/inspector.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/devtools/inspector.dtd" >
+ %inspectorDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://global/content/viewSourceUtils.js"/>
+
+ <commandset>
+ <command id="nodeSearchCommand"
+ oncommand="inspector.searchBox.focus()"/>
+ </commandset>
+
+ <keyset>
+ <key id="nodeSearchKey"
+ key="&inspectorSearchHTML.key;"
+ modifiers="accel"
+ command="nodeSearchCommand"/>
+ </keyset>
+
+ <popupset id="inspectorPopupSet">
+ <!-- Used by the Markup Panel, the Highlighter and the Breadcrumbs -->
+ <menupopup id="inspector-node-popup">
+ <menuitem id="node-menu-copyinner"
+ label="&inspectorHTMLCopyInner.label;"
+ accesskey="&inspectorHTMLCopyInner.accesskey;"
+ oncommand="inspector.copyInnerHTML()"/>
+ <menuitem id="node-menu-copyouter"
+ label="&inspectorHTMLCopyOuter.label;"
+ accesskey="&inspectorHTMLCopyOuter.accesskey;"
+ oncommand="inspector.copyOuterHTML()"/>
+ <menuitem id="node-menu-copyuniqueselector"
+ label="&inspectorCopyUniqueSelector.label;"
+ accesskey="&inspectorCopyUniqueSelector.accesskey;"
+ oncommand="inspector.copyUniqueSelector()"/>
+ <menuseparator/>
+ <menuitem id="node-menu-delete"
+ label="&inspectorHTMLDelete.label;"
+ accesskey="&inspectorHTMLDelete.accesskey;"
+ oncommand="inspector.deleteNode()"/>
+ <menuseparator/>
+ <menuitem id="node-menu-pseudo-hover"
+ label=":hover" type="checkbox"
+ oncommand="inspector.togglePseudoClass(':hover')"/>
+ <menuitem id="node-menu-pseudo-active"
+ label=":active" type="checkbox"
+ oncommand="inspector.togglePseudoClass(':active')"/>
+ <menuitem id="node-menu-pseudo-focus"
+ label=":focus" type="checkbox"
+ oncommand="inspector.togglePseudoClass(':focus')"/>
+ </menupopup>
+ </popupset>
+
+ <box flex="1" class="devtools-responsive-container">
+ <vbox flex="1">
+ <toolbar id="inspector-toolbar"
+ class="devtools-toolbar"
+ nowindowdrag="true">
+ <toolbarbutton id="inspector-inspect-toolbutton"
+ tooltiptext="&inspector.selectButton.tooltip;"
+ class="devtools-toolbarbutton"
+ hidden="true"
+ oncommand="inspector.highlighter.unlockAndFocus()"/>
+ <arrowscrollbox id="inspector-breadcrumbs"
+ class="breadcrumbs-widget-container"
+ flex="1" orient="horizontal"
+ clicktoscroll="true"/>
+ <textbox id="inspector-searchbox"
+ type="search"
+ timeout="50"
+ class="devtools-searchinput"
+ placeholder="&inspectorSearchHTML.label;"/>
+ </toolbar>
+ <vbox flex="1" id="markup-box">
+ </vbox>
+ </vbox>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs" hidden="true">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>
diff --git a/browser/devtools/inspector/moz.build b/browser/devtools/inspector/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/inspector/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/inspector/selection.js b/browser/devtools/inspector/selection.js
new file mode 100644
index 000000000..6963f6fe3
--- /dev/null
+++ b/browser/devtools/inspector/selection.js
@@ -0,0 +1,235 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cu} = require("chrome");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * API
+ *
+ * new Selection(node=null, track={attributes,detached});
+ * 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" when the inner node changed
+ * "before-new-node" when the inner node is set to change
+ * "attribute-changed" when an attribute is changed (only if tracked)
+ * "detached" when the node (or one of its parents) is removed from the document (only if tracked)
+ * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked)
+ */
+
+/**
+ * A Selection object. Hold a reference to a node.
+ * Includes some helpers, fire some helpful events.
+ *
+ * @param node Inner node.
+ * Can be null. Can be (un)set in the future via the "node" property;
+ * @param trackAttribute Tell if events should be fired when the attributes of
+ * the ndoe change.
+ *
+ */
+function Selection(node=null, track={attributes:true,detached:true}) {
+ EventEmitter.decorate(this);
+ this._onMutations = this._onMutations.bind(this);
+ this.track = track;
+ this.setNode(node);
+}
+
+exports.Selection = Selection;
+
+Selection.prototype = {
+ _node: null,
+
+ _onMutations: function(mutations) {
+ let attributeChange = 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()) {
+ parentNode = m.target;
+ detached = true;
+ }
+ }
+ }
+
+ if (attributeChange)
+ this.emit("attribute-changed");
+ if (detached)
+ this.emit("detached", parentNode);
+ },
+
+ _attachEvents: function SN__attachEvents() {
+ if (!this.window || !this.isNode() || !this.track) {
+ return;
+ }
+
+ if (this.track.attributes) {
+ this._nodeObserver = new this.window.MutationObserver(this._onMutations);
+ this._nodeObserver.observe(this.node, {attributes: true});
+ }
+
+ if (this.track.detached) {
+ this._docObserver = new this.window.MutationObserver(this._onMutations);
+ this._docObserver.observe(this.document.documentElement, {childList: true, subtree: true});
+ }
+ },
+
+ _detachEvents: function SN__detachEvents() {
+ // `disconnect` fail if node's document has
+ // been deleted.
+ try {
+ if (this._nodeObserver)
+ this._nodeObserver.disconnect();
+ } catch(e) {}
+ try {
+ if (this._docObserver)
+ this._docObserver.disconnect();
+ } catch(e) {}
+ },
+
+ destroy: function SN_destroy() {
+ this._detachEvents();
+ this.setNode(null);
+ },
+
+ setNode: function SN_setNode(value, reason="unknown") {
+ this.reason = reason;
+ if (value !== this._node) {
+ this.emit("before-new-node", value, reason);
+ let previousNode = this._node;
+ this._detachEvents();
+ this._node = value;
+ this._attachEvents();
+ this.emit("new-node", previousNode, this.reason);
+ }
+ },
+
+ get node() {
+ return this._node;
+ },
+
+ get window() {
+ if (this.isNode()) {
+ return this.node.ownerDocument.defaultView;
+ }
+ return null;
+ },
+
+ get document() {
+ if (this.isNode()) {
+ return this.node.ownerDocument;
+ }
+ return null;
+ },
+
+ isRoot: function SN_isRootNode() {
+ return this.isNode() &&
+ this.isConnected() &&
+ this.node.ownerDocument.documentElement === this.node;
+ },
+
+ isNode: function SN_isNode() {
+ return (this.node &&
+ !Cu.isDeadWrapper(this.node) &&
+ this.node.ownerDocument &&
+ this.node.ownerDocument.defaultView &&
+ this.node instanceof this.node.ownerDocument.defaultView.Node);
+ },
+
+ isConnected: function SN_isConnected() {
+ try {
+ let doc = this.document;
+ return doc && doc.defaultView && doc.documentElement.contains(this.node);
+ } catch (e) {
+ // "can't access dead object" error
+ return false;
+ }
+ },
+
+ isHTMLNode: function SN_isHTMLNode() {
+ let xhtml_ns = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.node.namespaceURI == xhtml_ns;
+ },
+
+ // Node type
+
+ isElementNode: function SN_isElementNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.ELEMENT_NODE;
+ },
+
+ isAttributeNode: function SN_isAttributeNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.ATTRIBUTE_NODE;
+ },
+
+ isTextNode: function SN_isTextNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.TEXT_NODE;
+ },
+
+ isCDATANode: function SN_isCDATANode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.CDATA_SECTION_NODE;
+ },
+
+ isEntityRefNode: function SN_isEntityRefNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_REFERENCE_NODE;
+ },
+
+ isEntityNode: function SN_isEntityNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_NODE;
+ },
+
+ isProcessingInstructionNode: function SN_isProcessingInstructionNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isCommentNode: function SN_isCommentNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isDocumentNode: function SN_isDocumentNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_NODE;
+ },
+
+ isDocumentTypeNode: function SN_isDocumentTypeNode() {
+ return this.isNode() && this.node.nodeType ==this.window. Node.DOCUMENT_TYPE_NODE;
+ },
+
+ isDocumentFragmentNode: function SN_isDocumentFragmentNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_FRAGMENT_NODE;
+ },
+
+ isNotationNode: function SN_isNotationNode() {
+ return this.isNode() && this.node.nodeType == this.window.Node.NOTATION_NODE;
+ },
+}
diff --git a/browser/devtools/inspector/selector-search.js b/browser/devtools/inspector/selector-search.js
new file mode 100644
index 000000000..6e66d3643
--- /dev/null
+++ b/browser/devtools/inspector/selector-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 {Cu} = require("chrome");
+
+loader.lazyGetter(this, "AutocompletePopup", () => {
+ return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
+});
+
+// Maximum number of selector suggestions shown in the panel.
+const MAX_SUGGESTIONS = 15;
+
+/**
+ * Converts any input box on a page to a CSS selector search and suggestion box.
+ *
+ * @constructor
+ * @param nsIDOMDocument aContentDocument
+ * The content document which inspector is attached to.
+ * @param nsiInputElement aInputNode
+ * The input element to which the panel will be attached and from where
+ * search input will be taken.
+ * @param Function aCallback
+ * The method to callback when a search is available.
+ * This method is called with the matched node as the first argument.
+ */
+function SelectorSearch(aContentDocument, aInputNode, aCallback) {
+ this.doc = aContentDocument;
+ this.callback = aCallback;
+ this.searchBox = aInputNode;
+ this.panelDoc = this.searchBox.ownerDocument;
+
+ // initialize variables.
+ this._lastSearched = null;
+ this._lastValidSearch = "";
+ this._lastToLastValidSearch = null;
+ this._searchResults = null;
+ this._searchSuggestions = {};
+ this._searchIndex = 0;
+
+ // bind!
+ this._showPopup = this._showPopup.bind(this);
+ this._onHTMLSearch = this._onHTMLSearch.bind(this);
+ this._onSearchKeypress = this._onSearchKeypress.bind(this);
+ this._onListBoxKeypress = this._onListBoxKeypress.bind(this);
+
+ // Options for the AutocompletePopup.
+ let options = {
+ panelId: "inspector-searchbox-panel",
+ listBoxId: "searchbox-panel-listbox",
+ fixedWidth: true,
+ autoSelect: true,
+ position: "before_start",
+ direction: "ltr",
+ onClick: this._onListBoxKeypress,
+ onKeypress: this._onListBoxKeypress,
+ };
+ this.searchPopup = new AutocompletePopup(this.panelDoc, options);
+
+ // event listeners.
+ this.searchBox.addEventListener("command", this._onHTMLSearch, true);
+ this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
+}
+
+exports.SelectorSearch = SelectorSearch;
+
+SelectorSearch.prototype = {
+
+ // The possible states of the query.
+ States: {
+ CLASS: "class",
+ ID: "id",
+ TAG: "tag",
+ },
+
+ // 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:
+ this._state = lastChar == "."
+ ? this.States.CLASS
+ : lastChar == "#"
+ ? this.States.ID
+ : 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 '.'.
+ this._state = (lastChar == " " || lastChar == ">")
+ ? this.States.TAG
+ : lastChar == "#"
+ ? this.States.ID
+ : 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 '#'.
+ this._state = (lastChar == " " || lastChar == ">")
+ ? this.States.TAG
+ : lastChar == "."
+ ? this.States.CLASS
+ : this.States.ID;
+ }
+ break;
+ }
+ }
+ return this._state;
+ },
+
+ /**
+ * Removes event listeners and cleans up references.
+ */
+ destroy: function SelectorSearch_destroy() {
+ // event listeners.
+ this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
+ this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
+ this.searchPopup.destroy();
+ this.searchPopup = null;
+ this.searchBox = null;
+ this.doc = null;
+ this.panelDoc = null;
+ this._searchResults = null;
+ this._searchSuggestions = null;
+ this.callback = null;
+ },
+
+ /**
+ * The command callback for the input box. This function is automatically
+ * invoked as the user is typing if the input box type is search.
+ */
+ _onHTMLSearch: function SelectorSearch__onHTMLSearch() {
+ let query = this.searchBox.value;
+ if (query == this._lastSearched) {
+ return;
+ }
+ this._lastSearched = query;
+ this._searchIndex = 0;
+
+ if (query.length == 0) {
+ this._lastValidSearch = "";
+ this.searchBox.removeAttribute("filled");
+ this.searchBox.classList.remove("devtools-no-search-result");
+ if (this.searchPopup.isOpen) {
+ this.searchPopup.hidePopup();
+ }
+ return;
+ }
+
+ this.searchBox.setAttribute("filled", true);
+ try {
+ this._searchResults = this.doc.querySelectorAll(query);
+ }
+ catch (ex) {
+ this._searchResults = [];
+ }
+ if (this._searchResults.length > 0) {
+ this._lastValidSearch = query;
+ // Even though the selector matched atleast one node, there is still
+ // possibility of suggestions.
+ if (query.match(/[\s>+]$/)) {
+ // If the query has a space or '>' at the end, create a selector to match
+ // the children of the selector inside the search box by adding a '*'.
+ this._lastValidSearch += "*";
+ }
+ else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+ // If the query is a partial descendant selector which does not matches
+ // any node, remove the last incomplete part and add a '*' to match
+ // everything. For ex, convert 'foo > b' to 'foo > *' .
+ let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
+ this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+ }
+
+ if (!query.slice(-1).match(/[\.#\s>+]/)) {
+ // Hide the popup if we have some matching nodes and the query is not
+ // ending with [.# >] which means that the selector is not at the
+ // beginning of a new class, tag or id.
+ if (this.searchPopup.isOpen) {
+ this.searchPopup.hidePopup();
+ }
+ }
+ else {
+ this.showSuggestions();
+ }
+ this.searchBox.classList.remove("devtools-no-search-result");
+ this.callback(this._searchResults[0]);
+ }
+ else {
+ if (query.match(/[\s>+]$/)) {
+ this._lastValidSearch = query + "*";
+ }
+ else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+ let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
+ this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+ }
+ this.searchBox.classList.add("devtools-no-search-result");
+ this.showSuggestions();
+ }
+ },
+
+ /**
+ * Handles keypresses inside the input box.
+ */
+ _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
+ let query = this.searchBox.value;
+ switch(aEvent.keyCode) {
+ case aEvent.DOM_VK_ENTER:
+ case aEvent.DOM_VK_RETURN:
+ if (query == this._lastSearched) {
+ this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
+ }
+ else {
+ this._onHTMLSearch();
+ return;
+ }
+ break;
+
+ case aEvent.DOM_VK_UP:
+ if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
+ this.searchPopup.focus();
+ if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
+ this.searchPopup.selectedIndex =
+ Math.max(0, this.searchPopup.itemCount - 2);
+ }
+ else {
+ this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
+ }
+ this.searchBox.value = this.searchPopup.selectedItem.label;
+ }
+ else if (--this._searchIndex < 0) {
+ this._searchIndex = this._searchResults.length - 1;
+ }
+ break;
+
+ case aEvent.DOM_VK_DOWN:
+ if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
+ this.searchPopup.focus();
+ this.searchPopup.selectedIndex = 0;
+ this.searchBox.value = this.searchPopup.selectedItem.label;
+ }
+ this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
+ break;
+
+ case aEvent.DOM_VK_TAB:
+ if (this.searchPopup.isOpen &&
+ this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
+ .preLabel == query) {
+ this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
+ this.searchBox.value = this.searchPopup.selectedItem.label;
+ this._onHTMLSearch();
+ }
+ break;
+
+ case aEvent.DOM_VK_BACK_SPACE:
+ case aEvent.DOM_VK_DELETE:
+ // need to throw away the lastValidSearch.
+ this._lastToLastValidSearch = null;
+ // This gets the most complete selector from the query. For ex.
+ // '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar'
+ // '.foo +bar' returns '.foo +' and likewise.
+ this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
+ query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
+ ["",""])[1];
+ return;
+
+ default:
+ return;
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ if (this._searchResults.length > 0) {
+ this.callback(this._searchResults[this._searchIndex]);
+ }
+ },
+
+ /**
+ * Handles keypress and mouse click on the suggestions richlistbox.
+ */
+ _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
+ switch(aEvent.keyCode || aEvent.button) {
+ case aEvent.DOM_VK_ENTER:
+ case aEvent.DOM_VK_RETURN:
+ case aEvent.DOM_VK_TAB:
+ case 0: // left mouse button
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ this.searchBox.value = this.searchPopup.selectedItem.label;
+ this.searchBox.focus();
+ this._onHTMLSearch();
+ break;
+
+ case aEvent.DOM_VK_UP:
+ if (this.searchPopup.selectedIndex == 0) {
+ this.searchPopup.selectedIndex = -1;
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ this.searchBox.focus();
+ }
+ else {
+ let index = this.searchPopup.selectedIndex;
+ this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label;
+ }
+ break;
+
+ case aEvent.DOM_VK_DOWN:
+ if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
+ this.searchPopup.selectedIndex = -1;
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ this.searchBox.focus();
+ }
+ else {
+ let index = this.searchPopup.selectedIndex;
+ this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label;
+ }
+ break;
+
+ case aEvent.DOM_VK_BACK_SPACE:
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ this.searchBox.focus();
+ if (this.searchBox.selectionStart > 0) {
+ this.searchBox.value =
+ this.searchBox.value.substring(0, this.searchBox.selectionStart - 1);
+ }
+ this._lastToLastValidSearch = null;
+ let query = this.searchBox.value;
+ this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
+ query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
+ ["",""])[1];
+ this._onHTMLSearch();
+ break;
+ }
+ },
+
+
+ /**
+ * Populates the suggestions list and show the suggestion popup.
+ */
+ _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
+ // Sort alphabetically in increaseing order.
+ aList = aList.sort();
+ // Sort based on count= in decreasing order.
+ aList = aList.sort(function([a1,a2], [b1,b2]) {
+ return a2 < b2;
+ });
+
+ let total = 0;
+ let query = this.searchBox.value;
+ let toLowerCase = false;
+ let items = [];
+ // In case of tagNames, change the case to small.
+ if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
+ toLowerCase = true;
+ }
+ for (let [value, count] of aList) {
+ // for cases like 'div ' or 'div >' or 'div+'
+ if (query.match(/[\s>+]$/)) {
+ value = query + value;
+ }
+ // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
+ else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
+ let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
+ value = query.slice(0, -1 * lastPart.length + 1) + value;
+ }
+ // for cases like 'div.class' or '#foo.bar' and likewise
+ else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
+ let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
+ value = query.slice(0, -1 * lastPart.length + 1) + value;
+ }
+ let item = {
+ preLabel: query,
+ label: value,
+ count: count
+ };
+ if (toLowerCase) {
+ item.label = value.toLowerCase();
+ }
+ items.unshift(item);
+ if (++total > MAX_SUGGESTIONS - 1) {
+ break;
+ }
+ }
+ if (total > 0) {
+ this.searchPopup.setItems(items);
+ this.searchPopup.openPopup(this.searchBox);
+ }
+ else {
+ this.searchPopup.hidePopup();
+ }
+ },
+
+ /**
+ * Suggests classes,ids and tags based on the user input as user types in the
+ * searchbox.
+ */
+ showSuggestions: function SelectorSearch_showSuggestions() {
+ let query = this.searchBox.value;
+ if (this._lastValidSearch != "" &&
+ this._lastToLastValidSearch != this._lastValidSearch) {
+ this._searchSuggestions = {
+ ids: new Map(),
+ classes: new Map(),
+ tags: new Map(),
+ };
+
+ let nodes = [];
+ try {
+ nodes = this.doc.querySelectorAll(this._lastValidSearch);
+ } catch (ex) {}
+ for (let node of nodes) {
+ this._searchSuggestions.ids.set(node.id, 1);
+ this._searchSuggestions.tags
+ .set(node.tagName,
+ (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
+ for (let className of node.classList) {
+ this._searchSuggestions.classes
+ .set(className,
+ (this._searchSuggestions.classes.get(className) || 0) + 1);
+ }
+ }
+ this._lastToLastValidSearch = this._lastValidSearch;
+ }
+ else if (this._lastToLastValidSearch != this._lastValidSearch) {
+ this._searchSuggestions = {
+ ids: new Map(),
+ classes: new Map(),
+ tags: new Map(),
+ };
+
+ if (query.length == 0) {
+ return;
+ }
+
+ let nodes = null;
+ if (this.state == this.States.CLASS) {
+ nodes = this.doc.querySelectorAll("[class]");
+ for (let node of nodes) {
+ for (let className of node.classList) {
+ this._searchSuggestions.classes
+ .set(className,
+ (this._searchSuggestions.classes.get(className) || 0) + 1);
+ }
+ }
+ }
+ else if (this.state == this.States.ID) {
+ nodes = this.doc.querySelectorAll("[id]");
+ for (let node of nodes) {
+ this._searchSuggestions.ids.set(node.id, 1);
+ }
+ }
+ else if (this.state == this.States.TAG) {
+ nodes = this.doc.getElementsByTagName("*");
+ for (let node of nodes) {
+ this._searchSuggestions.tags
+ .set(node.tagName,
+ (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
+ }
+ }
+ else {
+ return;
+ }
+ this._lastToLastValidSearch = this._lastValidSearch;
+ }
+
+ // Filter the suggestions based on search box value.
+ let result = [];
+ let firstPart = "";
+ if (this.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];
+ for (let [tag, count] of this._searchSuggestions.tags) {
+ if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) {
+ result.push([tag, count]);
+ }
+ }
+ }
+ else if (this.state == this.States.CLASS) {
+ // gets the class that is being completed. For ex. '.foo.b' returns 'b'
+ firstPart = query.match(/\.([^\.]*)$/)[1];
+ for (let [className, count] of this._searchSuggestions.classes) {
+ if (className.startsWith(firstPart)) {
+ result.push(["." + className, count]);
+ }
+ }
+ firstPart = "." + firstPart;
+ }
+ else if (this.state == this.States.ID) {
+ // gets the id that is being completed. For ex. '.foo#b' returns 'b'
+ firstPart = query.match(/#([^#]*)$/)[1];
+ for (let [id, count] of this._searchSuggestions.ids) {
+ if (id.startsWith(firstPart)) {
+ result.push(["#" + id, 1]);
+ }
+ }
+ firstPart = "#" + firstPart;
+ }
+
+ this._showPopup(result, firstPart);
+ },
+};
diff --git a/browser/devtools/inspector/test/Makefile.in b/browser/devtools/inspector/test/Makefile.in
new file mode 100644
index 000000000..fd8333f80
--- /dev/null
+++ b/browser/devtools/inspector/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_inspector_iframeTest.js \
+ browser_inspector_initialization.js \
+ browser_inspector_highlighter.js \
+ browser_inspector_scrolling.js \
+ browser_inspector_bug_665880.js \
+ browser_inspector_infobar.js \
+ browser_inspector_breadcrumbs.html \
+ browser_inspector_breadcrumbs.js \
+ browser_inspector_invalidate.js \
+ browser_inspector_menu.js \
+ browser_inspector_menu.html \
+ browser_inspector_pseudoClass_menu.js \
+ browser_inspector_destroyselection.html \
+ browser_inspector_destroyselection.js \
+ browser_inspector_bug_699308_iframe_navigation.js \
+ browser_inspector_bug_672902_keyboard_shortcuts.js \
+ browser_inspector_sidebarstate.js \
+ browser_inspector_pseudoclass_lock.js \
+ browser_inspector_cmd_inspect.js \
+ browser_inspector_cmd_inspect.html \
+ browser_inspector_highlighter_autohide.js \
+ browser_inspector_changes.js \
+ browser_inspector_bug_674871.js \
+ browser_inspector_bug_817558_delete_node.js \
+ browser_inspector_bug_650804_search.js \
+ browser_inspector_bug_650804_search.html \
+ browser_inspector_bug_831693_input_suggestion.js \
+ browser_inspector_bug_831693_searchbox_panel_navigation.js \
+ browser_inspector_bug_831693_combinator_suggestions.js \
+ browser_inspector_bug_831693_search_suggestions.html \
+ browser_inspector_bug_835722_infobar_reappears.js \
+ browser_inspector_bug_840156_destroy_after_navigation.js \
+ head.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.html b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.html
new file mode 100644
index 000000000..bba7d21c1
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div {
+ min-height: 10px; min-width: 10px;
+ border: 1px solid red;
+ margin: 10px;
+ }
+ </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>
+ </body>
+</html>
diff --git a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js
new file mode 100644
index 000000000..a6d1b2c31
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ let nodes = [
+ {nodeId: "i1111", result: "i1 i11 i111 i1111"},
+ {nodeId: "i22", result: "i2 i22 i221"},
+ {nodeId: "i2111", result: "i2 i21 i211 i2111"},
+ {nodeId: "i21", result: "i2 i21 i211 i2111"},
+ {nodeId: "i22211", result: "i2 i22 i222 i2221 i22211"},
+ {nodeId: "i22", result: "i2 i22 i222 i2221 i22211"},
+ ];
+
+ let doc;
+ let nodes;
+ let cursor;
+ let inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_breadcrumbs.html";
+
+ function setupTest()
+ {
+ for (let i = 0; i < nodes.length; i++) {
+ let node = doc.getElementById(nodes[i].nodeId);
+ nodes[i].node = node;
+ ok(nodes[i].node, "node " + nodes[i].nodeId + " found");
+ }
+
+ openInspector(runTests);
+ }
+
+ function runTests(aInspector)
+ {
+ inspector = aInspector;
+ cursor = 0;
+ inspector.selection.on("new-node", nodeSelected);
+ executeSoon(function() {
+ inspector.selection.setNode(nodes[0].node);
+ });
+ }
+
+ function nodeSelected()
+ {
+ executeSoon(function() {
+ performTest();
+ cursor++;
+ if (cursor >= nodes.length) {
+ inspector.selection.off("new-node", nodeSelected);
+ finishUp();
+ } else {
+ let node = nodes[cursor].node;
+ inspector.selection.setNode(node);
+ }
+ });
+ }
+
+ function performTest()
+ {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let panel = gDevTools.getToolbox(target).getPanel("inspector");
+ let container = panel.panelDoc.getElementById("inspector-breadcrumbs");
+ let buttonsLabelIds = nodes[cursor].result.split(" ");
+
+ // html > body > …
+ is(container.childNodes.length, buttonsLabelIds.length + 2, "Node " + cursor + ": 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 " + cursor + ": button " + i + " matches");
+ }
+
+ let checkedButton = container.querySelector("button[checked]");
+ let labelId = checkedButton.querySelector(".breadcrumbs-widget-item-id");
+ let id = inspector.selection.node.id;
+ is(labelId.textContent, "#" + id, "Node " + cursor + ": selection matches");
+ }
+
+ function finishUp() {
+ doc = nodes = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html
new file mode 100644
index 000000000..262eb0be6
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_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/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
new file mode 100644
index 000000000..17d79ea7a
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let inspector, searchBox, state;
+ let keypressStates = [3,4,8,18,19,20,21,22];
+
+ // 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
+ // ]
+ let keyStates = [
+ ["d", "b1", false],
+ ["i", "b1", false],
+ ["v", "d1", true],
+ ["VK_DOWN", "d2", true],
+ ["VK_ENTER", "d1", true],
+ [".", "d1", false],
+ ["c", "d1", false],
+ ["1", "d2", true],
+ ["VK_DOWN", "d2", true],
+ ["VK_BACK_SPACE", "d2", false],
+ ["VK_BACK_SPACE", "d2", false],
+ ["VK_BACK_SPACE", "d1", true],
+ ["VK_BACK_SPACE", "d1", false],
+ ["VK_BACK_SPACE", "d1", false],
+ ["VK_BACK_SPACE", "d1", true],
+ [".", "d1", false],
+ ["c", "d1", false],
+ ["1", "d2", true],
+ ["VK_DOWN", "s2", true],
+ ["VK_DOWN", "p1", true],
+ ["VK_UP", "s2", true],
+ ["VK_UP", "d2", true],
+ ["VK_UP", "p1", true],
+ ["VK_BACK_SPACE", "p1", false],
+ ["2", "p3", true],
+ ["VK_BACK_SPACE", "p3", false],
+ ["VK_BACK_SPACE", "p3", false],
+ ["VK_BACK_SPACE", "p3", true],
+ ["r", "p3", false],
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html";
+
+ function $(id) {
+ if (id == null) return null;
+ return content.document.getElementById(id);
+ }
+
+ function setupTest()
+ {
+ openInspector(startTest);
+ }
+
+ function startTest(aInspector)
+ {
+ inspector = aInspector;
+ inspector.selection.setNode($("b1"));
+ searchBox =
+ inspector.panelWin.document.getElementById("inspector-searchbox");
+
+ focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+ searchBox.addEventListener("command", checkState, true);
+ searchBox.addEventListener("keypress", checkState, true);
+ checkStateAndMoveOn(0);
+ });
+ }
+
+ function checkStateAndMoveOn(index) {
+ if (index == keyStates.length) {
+ finishUp();
+ return;
+ }
+
+ let [key, id, isValid] = keyStates[index];
+ state = index;
+
+ info("pressing key " + key + " to get id " + id);
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ }
+
+ function checkState(event) {
+ if (event.type == "keypress" && keypressStates.indexOf(state) == -1) {
+ return;
+ }
+ executeSoon(function() {
+ let [key, id, isValid] = keyStates[state];
+ info(inspector.selection.node.id + " is selected with text " +
+ inspector.searchBox.value);
+ is(inspector.selection.node, $(id),
+ "Correct node is selected for state " + state);
+ is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
+ "Correct searchbox result state for state " + state);
+ checkStateAndMoveOn(state + 1);
+ });
+ }
+
+ function finishUp() {
+ searchBox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_665880.js b/browser/devtools/inspector/test/browser_inspector_bug_665880.js
new file mode 100644
index 000000000..6d990d5c8
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_665880.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test()
+{
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ let doc;
+ let objectNode;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(setupObjectInspectionTest, content);
+ }, true);
+
+ content.location = "data:text/html,<object style='padding: 100px'><p>foobar</p></object>";
+
+ function setupObjectInspectionTest()
+ {
+ objectNode = doc.querySelector("object");
+ ok(objectNode, "we have the object node");
+ openInspector(runObjectInspectionTest);
+ }
+
+ function runObjectInspectionTest(inspector)
+ {
+ inspector.highlighter.once("locked", performTestComparison);
+ inspector.selection.setNode(objectNode, "");
+ }
+
+ function performTestComparison()
+ {
+ is(getActiveInspector().selection.node, objectNode, "selection matches node");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ executeSoon(function() {
+ gDevTools.closeToolbox(target);
+ finishUp();
+ });
+ }
+
+
+ function finishUp() {
+ doc = objectNode = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js
new file mode 100644
index 000000000..79ba64580
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+// Tests that the keybindings for highlighting different elements work as
+// intended.
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let doc;
+ let node;
+ let inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupKeyBindingsTest, content);
+ }, true);
+
+ content.location = "data:text/html,<html><head><title>Test for the " +
+ "highlighter keybindings</title></head><body><h1>Hello" +
+ "</h1><p><strong>Greetings, earthlings!</strong> I come" +
+ " in peace.</body></html>";
+
+ function setupKeyBindingsTest()
+ {
+ openInspector(findAndHighlightNode);
+ }
+
+ function findAndHighlightNode(aInspector, aToolbox)
+ {
+ inspector = aInspector;
+
+ executeSoon(function() {
+ inspector.selection.once("new-node", highlightHeaderNode);
+ // Test that navigating around without a selected node gets us to the
+ // head element.
+ node = doc.querySelector("h1");
+ let bc = inspector.breadcrumbs;
+ bc.nodeHierarchy[bc.currentIndex].button.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", { });
+ });
+ }
+
+ function highlightHeaderNode()
+ {
+ is(inspector.selection.node, node, "selected h1 element");
+
+ executeSoon(function() {
+ inspector.selection.once("new-node", highlightParagraphNode);
+ // Test that moving to the next sibling works.
+ node = doc.querySelector("p");
+ EventUtils.synthesizeKey("VK_DOWN", { });
+ });
+ }
+
+ function highlightParagraphNode()
+ {
+ is(inspector.selection.node, node, "selected p element");
+
+ executeSoon(function() {
+ inspector.selection.once("new-node", highlightHeaderNodeAgain);
+ // Test that moving to the previous sibling works.
+ node = doc.querySelector("h1");
+ EventUtils.synthesizeKey("VK_UP", { });
+ });
+ }
+
+ function highlightHeaderNodeAgain()
+ {
+ is(inspector.selection.node, node, "selected h1 element");
+
+ executeSoon(function() {
+ inspector.selection.once("new-node", highlightParentNode);
+ // Test that moving to the parent works.
+ node = doc.querySelector("body");
+ EventUtils.synthesizeKey("VK_LEFT", { });
+ });
+ }
+
+ function highlightParentNode()
+ {
+ is(inspector.selection.node, node, "selected body element");
+ finishUp();
+ }
+
+ function finishUp() {
+ doc = node = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_674871.js b/browser/devtools/inspector/test/browser_inspector_bug_674871.js
new file mode 100644
index 000000000..ed448d8f1
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_674871.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let doc;
+ let iframeNode, iframeBodyNode;
+
+ let iframeSrc = "<style>" +
+ "body {" +
+ "margin:0;" +
+ "height:100%;" +
+ "background-color:red" +
+ "}" +
+ "</style>" +
+ "<body></body>";
+ let docSrc = "<style>" +
+ "iframe {" +
+ "height:200px;" +
+ "border: 11px solid black;" +
+ "padding: 13px;" +
+ "}" +
+ "body,iframe {" +
+ "margin:0" +
+ "}" +
+ "</style>" +
+ "<body>" +
+ "<iframe src='data:text/html," + iframeSrc + "'></iframe>" +
+ "</body>";
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "data:text/html," + docSrc;
+
+ function setupTest()
+ {
+ iframeNode = doc.querySelector("iframe");
+ iframeBodyNode = iframeNode.contentDocument.querySelector("body");
+ ok(iframeNode, "we have the iframe node");
+ ok(iframeBodyNode, "we have the body node");
+ openInspector(runTests);
+ }
+
+ function runTests(inspector)
+ {
+ executeSoon(function() {
+ inspector.highlighter.once("highlighting", isTheIframeSelected);
+ moveMouseOver(iframeNode, 1, 1);
+ });
+ }
+
+ function isTheIframeSelected()
+ {
+ let inspector = getActiveInspector();
+
+ is(inspector.selection.node, iframeNode, "selection matches node");
+ iframeNode.style.marginBottom = doc.defaultView.innerHeight + "px";
+ doc.defaultView.scrollBy(0, 40);
+
+ executeSoon(function() {
+ inspector.selection.once("new-node", isTheIframeContentSelected);
+ moveMouseOver(iframeNode, 40, 40);
+ });
+ }
+
+ function isTheIframeContentSelected()
+ {
+ let inspector = getActiveInspector();
+ is(inspector.selection.node, iframeBodyNode, "selection matches node");
+ // 184 == 200 + 11(border) + 13(padding) - 40(scroll)
+ is(inspector.highlighter._highlightRect.height, 184,
+ "highlighter height");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ finishUp();
+ }
+
+ function finishUp() {
+ doc = iframeNode = iframeBodyNode = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+
+
+ function moveMouseOver(aElement, x, y)
+ {
+ EventUtils.synthesizeMouse(aElement, x, y, {type: "mousemove"},
+ aElement.ownerDocument.defaultView);
+ }
+
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js b/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js
new file mode 100644
index 000000000..3f9355dd7
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let iframe;
+ let iframeLoads = 0;
+ let checksAfterLoads = false;
+ let inspector;
+
+ function startTest() {
+ openInspector(runInspectorTests);
+ }
+
+ function runInspectorTests(aInspector) {
+ inspector = aInspector;
+
+ iframe = content.document.querySelector("iframe");
+ ok(iframe, "found the iframe element");
+
+ ok(inspector.highlighter._highlighting, "Inspector is highlighting");
+
+ iframe.addEventListener("load", onIframeLoad, false);
+
+ executeSoon(function() {
+ iframe.contentWindow.location = "javascript:location.reload()";
+ });
+ }
+
+ function onIframeLoad() {
+ if (++iframeLoads != 2) {
+ executeSoon(function() {
+ iframe.contentWindow.location = "javascript:location.reload()";
+ });
+ return;
+ }
+
+ iframe.removeEventListener("load", onIframeLoad, false);
+
+ ok(inspector.highlighter._highlighting, "Inspector is highlighting after iframe nav");
+
+ checksAfterLoads = true;
+
+ finishTest();
+ }
+
+ function finishTest() {
+ is(iframeLoads, 2, "iframe loads");
+ ok(checksAfterLoads, "the Inspector tests got the chance to run after iframe reloads");
+
+ iframe = null;
+ gBrowser.removeCurrentTab();
+ executeSoon(finish);
+ }
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,<p>bug 699308 - test iframe navigation" +
+ "<iframe src='data:text/html,hello world'></iframe>";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
new file mode 100644
index 000000000..da286979b
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+ //ignoreAllUncaughtExceptions();
+
+ let node, iframe, inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_destroyselection.html";
+
+ function setupTest()
+ {
+ iframe = content.document.querySelector("iframe");
+ node = iframe.contentDocument.querySelector("span");
+ openInspector(runTests);
+ }
+
+ function runTests(aInspector)
+ {
+ inspector = aInspector;
+ inspector.selection.setNode(node);
+
+ let parentNode = node.parentNode;
+ parentNode.removeChild(node);
+
+ let tmp = {};
+ Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
+ ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
+ executeSoon(function() {
+ is(inspector.selection.node, parentNode, "parent of selection got selected");
+
+ finishUp();
+ });
+ }
+
+ function finishUp() {
+ node = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
new file mode 100644
index 000000000..1d22bb55f
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let inspector, searchBox, state, popup;
+
+ // 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
+ // ]
+ let keyStates = [
+ ["d", [["div", 4]]],
+ ["i", [["div", 4]]],
+ ["v", []],
+ [" ", [["div div", 2], ["div span", 2]]],
+ [">", [["div >div", 2], ["div >span", 2]]],
+ ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
+ ["+", [["div +span"]]],
+ ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
+ ["VK_BACK_SPACE", []],
+ ["VK_BACK_SPACE", [["div", 4]]],
+ ["VK_BACK_SPACE", [["div", 4]]],
+ ["VK_BACK_SPACE", []],
+ ["p", []],
+ [" ", [["p strong"]]],
+ ["+", [["p +button"], ["p +p"]]],
+ ["b", [["p +button"]]],
+ ["u", [["p +button"]]],
+ ["t", [["p +button"]]],
+ ["t", [["p +button"]]],
+ ["o", [["p +button"]]],
+ ["n", []],
+ ["+", [["p +button+p"]]],
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
+
+ function $(id) {
+ if (id == null) return null;
+ return content.document.getElementById(id);
+ }
+
+ function setupTest()
+ {
+ openInspector(startTest);
+ }
+
+ function startTest(aInspector)
+ {
+ inspector = aInspector;
+ searchBox =
+ inspector.panelWin.document.getElementById("inspector-searchbox");
+ popup = inspector.searchSuggestions.searchPopup;
+
+ focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+ searchBox.addEventListener("command", checkState, true);
+ checkStateAndMoveOn(0);
+ });
+ }
+
+ function checkStateAndMoveOn(index) {
+ if (index == keyStates.length) {
+ finishUp();
+ return;
+ }
+
+ let [key, suggestions] = keyStates[index];
+ state = index;
+
+ info("pressing key " + key + " to get suggestions " +
+ JSON.stringify(suggestions));
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ }
+
+ function checkState(event) {
+ executeSoon(function() {
+ let [key, suggestions] = keyStates[state];
+ let actualSuggestions = popup.getItems();
+ is(popup._panel.state == "open" || popup._panel.state == "showing"
+ ? actualSuggestions.length: 0, suggestions.length,
+ "There are expected number of suggestions at " + state + "th step.");
+ actualSuggestions = actualSuggestions.reverse();
+ for (let i = 0; i < suggestions.length; i++) {
+ is(suggestions[i][0], actualSuggestions[i].label,
+ "The suggestion at " + i + "th index for " + state +
+ "th step is correct.")
+ is(suggestions[i][1] || 1, actualSuggestions[i].count,
+ "The count for suggestion at " + i + "th index for " + state +
+ "th step is correct.")
+ }
+ checkStateAndMoveOn(state + 1);
+ });
+ }
+
+ function finishUp() {
+ searchBox = null;
+ popup = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
new file mode 100644
index 000000000..03a5ec335
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let inspector, searchBox, state, popup;
+
+ // 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
+ // ]
+ let keyStates = [
+ ["d", [["div", 2]]],
+ ["i", [["div", 2]]],
+ ["v", []],
+ [".", [["div.c1"]]],
+ ["VK_BACK_SPACE", []],
+ ["#", [["div#d1"], ["div#d2"]]],
+ ["VK_BACK_SPACE", []],
+ ["VK_BACK_SPACE", [["div", 2]]],
+ ["VK_BACK_SPACE", [["div", 2]]],
+ ["VK_BACK_SPACE", []],
+ [".", [[".c1", 3], [".c2"]]],
+ ["c", [[".c1", 3], [".c2"]]],
+ ["2", []],
+ ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+ ["1", []],
+ ["#", [["#d2"], ["#p1"], ["#s2"]]],
+ ["VK_BACK_SPACE", []],
+ ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+ ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+ ["VK_BACK_SPACE", []],
+ ["#", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
+ ["p", [["#p1"], ["#p2"], ["#p3"]]],
+ ["VK_BACK_SPACE", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
+ ["VK_BACK_SPACE", []],
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html";
+
+ function $(id) {
+ if (id == null) return null;
+ return content.document.getElementById(id);
+ }
+
+ function setupTest()
+ {
+ openInspector(startTest);
+ }
+
+ function startTest(aInspector)
+ {
+ inspector = aInspector;
+ searchBox =
+ inspector.panelWin.document.getElementById("inspector-searchbox");
+ popup = inspector.searchSuggestions.searchPopup;
+
+ focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+ searchBox.addEventListener("command", checkState, true);
+ checkStateAndMoveOn(0);
+ });
+ }
+
+ function checkStateAndMoveOn(index) {
+ if (index == keyStates.length) {
+ finishUp();
+ return;
+ }
+
+ let [key, suggestions] = keyStates[index];
+ state = index;
+
+ info("pressing key " + key + " to get suggestions " +
+ JSON.stringify(suggestions));
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ }
+
+ function checkState(event) {
+ executeSoon(function() {
+ let [key, suggestions] = keyStates[state];
+ let actualSuggestions = popup.getItems();
+ is(popup._panel.state == "open" || popup._panel.state == "showing"
+ ? actualSuggestions.length: 0, suggestions.length,
+ "There are expected number of suggestions at " + state + "th step.");
+ actualSuggestions = actualSuggestions.reverse();
+ for (let i = 0; i < suggestions.length; i++) {
+ is(suggestions[i][0], actualSuggestions[i].label,
+ "The suggestion at " + i + "th index for " + state +
+ "th step is correct.")
+ is(suggestions[i][1] || 1, actualSuggestions[i].count,
+ "The count for suggestion at " + i + "th index for " + state +
+ "th step is correct.")
+ }
+ checkStateAndMoveOn(state + 1);
+ });
+ }
+
+ function finishUp() {
+ searchBox = null;
+ popup = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html b/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html
new file mode 100644
index 000000000..a84a2e3d4
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_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/browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js b/browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js
new file mode 100644
index 000000000..6ccafe5bd
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ let inspector, searchBox, state, panel;
+ let panelOpeningStates = [0, 3, 9, 14, 17];
+ let panelClosingStates = [2, 8, 13, 16];
+
+ // The various states of the inspector: [key, query]
+ // [
+ // what key to press,
+ // what should be the text in the searchbox
+ // ]
+ let keyStates = [
+ ["d", "d"],
+ ["i", "di"],
+ ["v", "div"],
+ [".", "div."],
+ ["VK_UP", "div.c1"],
+ ["VK_DOWN", "div.l1"],
+ ["VK_DOWN", "div.l1"],
+ ["VK_BACK_SPACE", "div.l"],
+ ["VK_TAB", "div.l1"],
+ [" ", "div.l1 "],
+ ["VK_UP", "div.l1 DIV"],
+ ["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 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_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", ""],
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
+
+ function $(id) {
+ if (id == null) return null;
+ return content.document.getElementById(id);
+ }
+
+ function setupTest()
+ {
+ openInspector(startTest);
+ }
+
+ function startTest(aInspector)
+ {
+ inspector = aInspector;
+ searchBox =
+ inspector.panelWin.document.getElementById("inspector-searchbox");
+ panel = inspector.searchSuggestions.searchPopup._list;
+
+ focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+ searchBox.addEventListener("keypress", checkState, true);
+ panel.addEventListener("keypress", checkState, true);
+ checkStateAndMoveOn(0);
+ });
+ }
+
+ function checkStateAndMoveOn(index) {
+ if (index == keyStates.length) {
+ finishUp();
+ return;
+ }
+
+ let [key, query] = keyStates[index];
+ state = index;
+
+ info("pressing key " + key + " to get searchbox value as " + query);
+ EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+ }
+
+ function checkState(event) {
+ if (panelOpeningStates.indexOf(state) != -1 &&
+ !inspector.searchSuggestions.searchPopup.isOpen) {
+ info("Panel is not open, should wait before it shows up.");
+ panel.parentNode.addEventListener("popupshown", function retry() {
+ panel.parentNode.removeEventListener("popupshown", retry, false);
+ info("Panel is visible now");
+ executeSoon(checkState);
+ }, false);
+ return;
+ }
+ else if (panelClosingStates.indexOf(state) != -1 &&
+ panel.parentNode.state != "closed") {
+ info("Panel is open, should wait for it to close.");
+ panel.parentNode.addEventListener("popuphidden", function retry() {
+ panel.parentNode.removeEventListener("popuphidden", retry, false);
+ info("Panel is hidden now");
+ executeSoon(checkState);
+ }, false);
+ return;
+ }
+
+ // Using setTimout as the "command" event fires at delay after keypress
+ window.setTimeout(function() {
+ let [key, query] = keyStates[state];
+
+ if (searchBox.value == query) {
+ ok(true, "The suggestion at " + state + "th step on " +
+ "pressing " + key + " key is correct.");
+ }
+ else {
+ info("value is not correct, waiting longer for state " + state +
+ " with panel " + panel.parentNode.state);
+ checkState();
+ return;
+ }
+ checkStateAndMoveOn(state + 1);
+ }, 200);
+ }
+
+ function finishUp() {
+ searchBox = null;
+ panel = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_835722_infobar_reappears.js b/browser/devtools/inspector/test/browser_inspector_bug_835722_infobar_reappears.js
new file mode 100644
index 000000000..503d4a319
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_835722_infobar_reappears.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let inspector, utils;
+
+ function startLocationTests() {
+ openInspector(runInspectorTests);
+ }
+
+ function runInspectorTests(aInspector) {
+ inspector = aInspector;
+ utils = inspector.panelWin
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ ok(utils, "utils is defined");
+ executeSoon(function() {
+ inspector.selection.once("new-node", onNewSelection);
+ info("selecting the DOCTYPE node");
+ inspector.selection.setNode(content.document.doctype, "test");
+ });
+ }
+
+ function sendMouseEvent(node, type, x, y) {
+ let rect = node.getBoundingClientRect();
+ let left = rect.left + x;
+ let top = rect.top + y;
+ utils.sendMouseEventToWindow(type, left, top, 0, 1, 0, false, 0, 0);
+ }
+
+ function onNewSelection() {
+ is(inspector.highlighter.isHidden(), true,
+ "The infobar should be hidden now on selecting a non element node.");
+ inspector.sidebar.select("ruleview");
+ let ruleView = inspector.sidebar.getTab("ruleview");
+ ruleView.addEventListener("mouseover", function onMouseOver() {
+ ruleView.removeEventListener("mouseover", onMouseOver, false);
+ is(inspector.highlighter.isHidden(), true,
+ "The infobar was hidden so mouseover on the rules view did nothing");
+ executeSoon(mouseOutAndContinue);
+ }, false);
+ sendMouseEvent(ruleView, "mouseover", 10, 10);
+ }
+
+ function mouseOutAndContinue() {
+ let ruleView = inspector.sidebar.getTab("ruleview");
+ info("adding mouseout listener");
+ ruleView.addEventListener("mouseout", function onMouseOut() {
+ info("mouseout happened");
+ ruleView.removeEventListener("mouseout", onMouseOut, false);
+ is(inspector.highlighter.isHidden(), true,
+ "The infobar should not be visible after we mouseout of rules view");
+ switchToWebConsole();
+ }, false);
+ info("Synthesizing mouseout on " + ruleView);
+ sendMouseEvent(inspector._markupBox, "mousemove", 50, 50);
+ info("mouseout synthesized");
+ }
+
+ function switchToWebConsole() {
+ inspector.selection.once("new-node", function() {
+ is(inspector.highlighter.isHidden(), false,
+ "The infobar should be visible after we select a div.");
+ gDevTools.showToolbox(inspector.target, "webconsole").then(function() {
+ is(inspector.highlighter.isHidden(), true,
+ "The infobar should not be visible after we switched to webconsole");
+ reloadAndWait();
+ });
+ });
+ inspector.selection.setNode(content.document.querySelector("div"), "test");
+ }
+
+ function reloadAndWait() {
+ gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
+ waitForFocus(testAfterReload, content);
+ }, true);
+ content.location.reload();
+ }
+
+ function testAfterReload() {
+ is(inspector.highlighter.isHidden(), true,
+ "The infobar should not be visible after we reload with webconsole shown");
+ testEnd();
+ }
+
+ function testEnd() {
+ gBrowser.removeCurrentTab();
+ utils = null;
+ executeSoon(finish);
+ }
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
+ waitForFocus(startLocationTests, content);
+ }, true);
+
+ content.location = "data:text/html,<!DOCTYPE html><div>Infobar should not " +
+ "reappear</div><p>init</p>";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_840156_destroy_after_navigation.js b/browser/devtools/inspector/test/browser_inspector_bug_840156_destroy_after_navigation.js
new file mode 100644
index 000000000..17072c7a6
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_840156_destroy_after_navigation.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Promise = devtools.require("sdk/core/promise");
+let Toolbox = devtools.Toolbox;
+let TargetFactory = devtools.TargetFactory;
+
+function test() {
+ waitForExplicitFinish();
+
+ const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+ const URL_2 = "data:text/plain;charset=UTF-8,12345";
+
+ let target, toolbox;
+
+ // open tab, load URL_1, and wait for load to finish
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let deferred = Promise.defer();
+ let browser = gBrowser.getBrowserForTab(tab);
+ function onTabLoad() {
+ browser.removeEventListener("load", onTabLoad, true);
+ deferred.resolve(null);
+ }
+ browser.addEventListener("load", onTabLoad, true);
+ browser.loadURI(URL_1);
+
+ // open devtools panel
+ deferred.promise
+ .then(function () gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM))
+ .then(function (aToolbox) { toolbox = aToolbox; })
+
+ // select the inspector
+ .then(function () toolbox.selectTool("inspector"))
+
+ // navigate to URL_2
+ .then(function () {
+ let deferred = Promise.defer();
+ target.once("navigate", function () deferred.resolve());
+ browser.loadURI(URL_2);
+ return deferred.promise;
+ })
+
+ // destroy the toolbox (and hence the inspector) before the load completes
+ .then(function () toolbox.destroy())
+
+ // this (or any other) exception should not occur:
+ // [JavaScript Error: "TypeError: self.selection is null" {file: "resource:///modules/devtools/InspectorPanel.jsm" line: 250}]
+
+ .then(function cleanUp() {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_changes.js b/browser/devtools/inspector/test/browser_inspector_changes.js
new file mode 100644
index 000000000..a3088e51d
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_changes.js
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+let doc;
+let testDiv;
+
+function test() {
+ let inspector;
+
+ function createDocument()
+ {
+ doc.body.innerHTML = '<div id="testdiv">Test div!</div>';
+ doc.title = "Inspector Change Test";
+ openInspector(runInspectorTests);
+ }
+
+
+ function getInspectorProp(aName)
+ {
+ let computedview = inspector.sidebar.getWindowForTab("computedview").computedview.view;
+ for each (let view in computedview.propertyViews) {
+ if (view.name == aName) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ function runInspectorTests(aInspector)
+ {
+ inspector = aInspector;
+ inspector.sidebar.once("computedview-ready", function() {
+ info("Computed View ready");
+ inspector.sidebar.select("computedview");
+
+ testDiv = doc.getElementById("testdiv");
+
+ testDiv.style.fontSize = "10px";
+
+ // Start up the style inspector panel...
+ Services.obs.addObserver(stylePanelTests, "StyleInspector-populated", false);
+
+ inspector.selection.setNode(testDiv);
+ });
+ }
+
+ function stylePanelTests()
+ {
+ Services.obs.removeObserver(stylePanelTests, "StyleInspector-populated");
+
+ let computedview = inspector.sidebar.getWindowForTab("computedview").computedview;
+ ok(computedview, "Style Panel has a cssHtmlTree");
+
+ let propView = getInspectorProp("font-size");
+ is(propView.value, "10px", "Style inspector should be showing the correct font size.");
+
+ Services.obs.addObserver(stylePanelAfterChange, "StyleInspector-populated", false);
+
+ testDiv.style.fontSize = "15px";
+ inspector.emit("layout-change");
+ }
+
+ function stylePanelAfterChange()
+ {
+ Services.obs.removeObserver(stylePanelAfterChange, "StyleInspector-populated");
+
+ let propView = getInspectorProp("font-size");
+ is(propView.value, "15px", "Style inspector should be showing the new font size.");
+
+ stylePanelNotActive();
+ }
+
+ function stylePanelNotActive()
+ {
+ // Tests changes made while the style panel is not active.
+ inspector.sidebar.select("ruleview");
+
+ executeSoon(function() {
+ Services.obs.addObserver(stylePanelAfterSwitch, "StyleInspector-populated", false);
+ testDiv.style.fontSize = "20px";
+ inspector.sidebar.select("computedview");
+ });
+ }
+
+ function stylePanelAfterSwitch()
+ {
+ Services.obs.removeObserver(stylePanelAfterSwitch, "StyleInspector-populated");
+
+ let propView = getInspectorProp("font-size");
+ is(propView.value, "20px", "Style inspector should be showing the newest font size.");
+
+ finishTest();
+ }
+
+ function finishTest()
+ {
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_cmd_inspect.html b/browser/devtools/inspector/test/browser_inspector_cmd_inspect.html
new file mode 100644
index 000000000..a7d28828c
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_cmd_inspect.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/browser/devtools/inspector/test/browser_inspector_cmd_inspect.js b/browser/devtools/inspector/test/browser_inspector_cmd_inspect.js
new file mode 100644
index 000000000..f060e59be
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_cmd_inspect.js
@@ -0,0 +1,131 @@
+/* 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/browser/devtools/inspector/" +
+ "test/browser_inspector_cmd_inspect.html";
+
+function test() {
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return helpers.audit(options, [
+ {
+ setup: "inspect",
+ check: {
+ input: 'inspect',
+ hints: ' <selector>',
+ markup: 'VVVVVVV',
+ status: 'ERROR',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect h1",
+ check: {
+ input: 'inspect h1',
+ hints: '',
+ markup: 'VVVVVVVVII',
+ status: 'ERROR',
+ args: {
+ selector: { message: 'No matches' },
+ }
+ },
+ },
+ {
+ setup: "inspect span",
+ check: {
+ input: 'inspect span',
+ hints: '',
+ markup: 'VVVVVVVVEEEE',
+ status: 'ERROR',
+ args: {
+ selector: { message: 'Too many matches (2)' },
+ }
+ },
+ },
+ {
+ setup: "inspect div",
+ check: {
+ input: 'inspect div',
+ hints: '',
+ markup: 'VVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect .someclas",
+ check: {
+ input: 'inspect .someclas',
+ hints: '',
+ markup: 'VVVVVVVVIIIIIIIII',
+ status: 'ERROR',
+ args: {
+ selector: { message: 'No matches' },
+ }
+ },
+ },
+ {
+ setup: "inspect .someclass",
+ check: {
+ input: 'inspect .someclass',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect #someid",
+ check: {
+ input: 'inspect #someid',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect button[disabled]",
+ check: {
+ input: 'inspect button[disabled]',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect p>strong",
+ check: {
+ input: 'inspect p>strong',
+ hints: '',
+ markup: 'VVVVVVVVVVVVVVVV',
+ status: 'VALID',
+ args: {
+ selector: { message: '' },
+ }
+ },
+ },
+ {
+ setup: "inspect :root",
+ check: {
+ input: 'inspect :root',
+ hints: '',
+ markup: 'VVVVVVVVVVVVV',
+ status: 'VALID'
+ },
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_destroyselection.html b/browser/devtools/inspector/test/browser_inspector_destroyselection.html
new file mode 100644
index 000000000..70edbd936
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_destroyselection.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/browser/devtools/inspector/test/browser_inspector_destroyselection.js b/browser/devtools/inspector/test/browser_inspector_destroyselection.js
new file mode 100644
index 000000000..bf24c5e34
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_destroyselection.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+ //ignoreAllUncaughtExceptions();
+
+ let node, iframe, inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_destroyselection.html";
+
+ function setupTest()
+ {
+ iframe = content.document.querySelector("iframe");
+ node = iframe.contentDocument.querySelector("span");
+ openInspector(runTests);
+ }
+
+ function runTests(aInspector)
+ {
+ inspector = aInspector;
+ inspector.selection.setNode(node);
+
+ iframe.parentNode.removeChild(iframe);
+ iframe = null;
+
+ let tmp = {};
+ Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
+ ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
+ ok(!inspector.selection.isConnected(), "Selection considered as disconnected");
+
+ finishUp();
+ }
+
+ function finishUp() {
+ node = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_highlighter.js b/browser/devtools/inspector/test/browser_inspector_highlighter.js
new file mode 100644
index 000000000..8987ddddd
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter.js
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+let doc;
+let h1;
+let div;
+
+function createDocument()
+{
+ let div = doc.createElement("div");
+ let 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 Highlighter Meatballs";
+ 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.";
+ let div3 = doc.createElement("div");
+ div3.id = "checkOutThisWickedSpread";
+ div3.setAttribute("style", "position: absolute; top: 20px; right: 20px; height: 20px; width: 20px; background-color: yellow; border: 1px dashed black;");
+ let p4 = doc.createElement("p");
+ p4.setAttribute("style", "font-weight: 200; font-size: 8px; text-align: center;");
+ p4.textContent = "Smörgåsbord!";
+ div.appendChild(h1);
+ div.appendChild(p1);
+ div.appendChild(p2);
+ div2.appendChild(p3);
+ div3.appendChild(p4);
+ doc.body.appendChild(div);
+ doc.body.appendChild(div2);
+ doc.body.appendChild(div3);
+
+ openInspector(setupHighlighterTests);
+}
+
+function setupHighlighterTests()
+{
+ h1 = doc.querySelector("h1");
+ ok(h1, "we have the header");
+
+ let i = getActiveInspector();
+ i.highlighter.unlockAndFocus();
+ i.highlighter.outline.setAttribute("disable-transitions", "true");
+
+ executeSoon(function() {
+ i.selection.once("new-node", performTestComparisons);
+ EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content);
+ });
+}
+
+function performTestComparisons(evt)
+{
+ let i = getActiveInspector();
+ i.highlighter.lock();
+ ok(isHighlighting(), "highlighter is highlighting");
+ is(getHighlitNode(), h1, "highlighter matches selection")
+ is(i.selection.node, h1, "selection matches node");
+ is(i.selection.node, getHighlitNode(), "selection matches highlighter");
+
+
+ div = doc.querySelector("div#checkOutThisWickedSpread");
+
+ executeSoon(function() {
+ i.selection.once("new-node", finishTestComparisons);
+ i.selection.setNode(div);
+ });
+}
+
+function finishTestComparisons()
+{
+ let i = getActiveInspector();
+
+ // get dimensions of div element
+ let divDims = div.getBoundingClientRect();
+ let divWidth = divDims.width;
+ let divHeight = divDims.height;
+
+ // get dimensions of the outline
+ let outlineDims = i.highlighter.outline.getBoundingClientRect();
+ let outlineWidth = outlineDims.width;
+ let outlineHeight = outlineDims.height;
+
+ // Disabled due to bug 716245
+ //is(outlineWidth, divWidth, "outline width matches dimensions of element (no zoom)");
+ //is(outlineHeight, divHeight, "outline height matches dimensions of element (no zoom)");
+
+ // zoom the page by a factor of 2
+ let contentViewer = gBrowser.selectedBrowser.docShell.contentViewer
+ .QueryInterface(Ci.nsIMarkupDocumentViewer);
+ contentViewer.fullZoom = 2;
+
+ // We wait at least 500ms to make sure the highlighter is not "mutting" the
+ // resize event
+
+ window.setTimeout(function() {
+ // check what zoom factor we're at, should be 2
+ let zoom = i.highlighter.zoom;
+ is(zoom, 2, "zoom is 2?");
+
+ // simulate the zoomed dimensions of the div element
+ let divDims = div.getBoundingClientRect();
+ let divWidth = divDims.width * zoom;
+ let divHeight = divDims.height * zoom;
+
+ // now zoomed, get new dimensions the outline
+ let outlineDims = i.highlighter.outline.getBoundingClientRect();
+ let outlineWidth = outlineDims.width;
+ let outlineHeight = outlineDims.height;
+
+ // Disabled due to bug 716245
+ //is(outlineWidth, divWidth, "outline width matches dimensions of element (no zoom)");
+ //is(outlineHeight, divHeight, "outline height matches dimensions of element (no zoom)");
+
+ doc = h1 = div = null;
+ executeSoon(finishUp);
+ }, 500);
+}
+
+function finishUp() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_highlighter_autohide.js b/browser/devtools/inspector/test/browser_inspector_highlighter_autohide.js
new file mode 100644
index 000000000..8dfca604b
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter_autohide.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test()
+{
+ let toolbox;
+ let inspector;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(startInspector, content);
+ }, true);
+ content.location = "data:text/html,mop"
+
+ function startInspector() {
+ info("Tab loaded");
+ openInspector(function(aInspector) {
+ inspector = aInspector;
+ ok(!inspector.highlighter.hidden, "Highlighter is visible");
+ toolbox = inspector._toolbox;
+ toolbox.once("webconsole-selected", onWebConsoleSelected);
+ toolbox.selectTool("webconsole");
+ });
+ }
+
+ function onWebConsoleSelected() {
+ executeSoon(function() {
+ ok(inspector.highlighter.hidden, "Highlighter is hidden");
+ toolbox.once("inspector-selected", onInspectorSelected);
+ toolbox.selectTool("inspector");
+ });
+ }
+
+ function onInspectorSelected() {
+ executeSoon(function() {
+ ok(!inspector.highlighter.hidden, "Highlighter is visible once inspector reopen");
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_iframeTest.js b/browser/devtools/inspector/test/browser_inspector_iframeTest.js
new file mode 100644
index 000000000..8d23b0493
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_iframeTest.js
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+let doc;
+let div1;
+let div2;
+let iframe1;
+let iframe2;
+
+function createDocument()
+{
+ doc.title = "Inspector iframe Tests";
+
+ iframe1 = doc.createElement('iframe');
+
+ iframe1.addEventListener("load", function () {
+ iframe1.removeEventListener("load", arguments.callee, false);
+
+ div1 = iframe1.contentDocument.createElement('div');
+ div1.textContent = 'little div';
+ iframe1.contentDocument.body.appendChild(div1);
+
+ iframe2 = iframe1.contentDocument.createElement('iframe');
+
+ iframe2.addEventListener('load', function () {
+ iframe2.removeEventListener("load", arguments.callee, false);
+
+ div2 = iframe2.contentDocument.createElement('div');
+ div2.textContent = 'nested div';
+ iframe2.contentDocument.body.appendChild(div2);
+
+ openInspector(runIframeTests);
+ }, false);
+
+ iframe2.src = 'data:text/html,nested iframe';
+ iframe1.contentDocument.body.appendChild(iframe2);
+ }, false);
+
+ iframe1.src = 'data:text/html,little iframe';
+ doc.body.appendChild(iframe1);
+}
+
+function moveMouseOver(aElement)
+{
+ EventUtils.synthesizeMouse(aElement, 2, 2, {type: "mousemove"},
+ aElement.ownerDocument.defaultView);
+}
+
+function runIframeTests()
+{
+ getActiveInspector().selection.once("new-node", performTestComparisons1);
+ moveMouseOver(div1)
+}
+
+function performTestComparisons1()
+{
+ let i = getActiveInspector();
+ is(i.selection.node, div1, "selection matches div1 node");
+ is(getHighlitNode(), div1, "highlighter matches selection");
+
+ i.selection.once("new-node", performTestComparisons2);
+ executeSoon(function() {
+ moveMouseOver(div2);
+ });
+}
+
+function performTestComparisons2()
+{
+ let i = getActiveInspector();
+
+ is(i.selection.node, div2, "selection matches div2 node");
+ is(getHighlitNode(), div2, "highlighter matches selection");
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ gBrowser.selectedBrowser.focus();
+ createDocument();
+ }, true);
+
+ content.location = "data:text/html,iframe tests for inspector";
+
+ registerCleanupFunction(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+ });
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_infobar.js b/browser/devtools/inspector/test/browser_inspector_infobar.js
new file mode 100644
index 000000000..1e20539cd
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_infobar.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ let doc;
+ let nodes;
+ let cursor;
+ let inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupInfobarTest, content);
+ }, true);
+
+ let style = "body{width:100%;height: 100%} div {position: absolute;height: 100px;width: 500px}#bottom {bottom: 0px}#vertical {height: 100%}#farbottom{bottom: -200px}";
+ let html = "<style>" + style + "</style><div id=vertical></div><div id=top class='class1 class2'></div><div id=bottom></div><div id=farbottom></div>"
+
+ content.location = "data:text/html," + encodeURIComponent(html);
+
+ function setupInfobarTest()
+ {
+ nodes = [
+ {node: doc.querySelector("#top"), position: "bottom", tag: "DIV", id: "#top", classes: ".class1.class2"},
+ {node: doc.querySelector("#vertical"), position: "overlap", tag: "DIV", id: "#vertical", classes: ""},
+ {node: doc.querySelector("#bottom"), position: "top", tag: "DIV", id: "#bottom", classes: ""},
+ {node: doc.querySelector("body"), position: "overlap", tag: "BODY", id: "", classes: ""},
+ {node: doc.querySelector("#farbottom"), position: "top", tag: "DIV", id: "#farbottom", classes: ""},
+ ]
+
+ for (let i = 0; i < nodes.length; i++) {
+ ok(nodes[i].node, "node " + i + " found");
+ }
+
+ openInspector(runTests);
+ }
+
+ function runTests(aInspector)
+ {
+ inspector = aInspector;
+ cursor = 0;
+ executeSoon(function() {
+ inspector.selection.setNode(nodes[0].node, "");
+ nodeSelected();
+ });
+ }
+
+ function nodeSelected()
+ {
+ executeSoon(function() {
+ performTest();
+ cursor++;
+ if (cursor >= nodes.length) {
+ finishUp();
+ } else {
+ let node = nodes[cursor].node;
+ inspector.selection.setNode(node, "");
+ nodeSelected();
+ }
+ });
+ }
+
+ function performTest()
+ {
+ let browser = gBrowser.selectedBrowser;
+ let stack = browser.parentNode;
+
+ let container = stack.querySelector(".highlighter-nodeinfobar-container");
+ is(container.getAttribute("position"), nodes[cursor].position, "node " + cursor + ": position matches.");
+
+ let tagNameLabel = stack.querySelector(".highlighter-nodeinfobar-tagname");
+ is(tagNameLabel.textContent, nodes[cursor].tag, "node " + cursor + ": tagName matches.");
+
+ let idLabel = stack.querySelector(".highlighter-nodeinfobar-id");
+ is(idLabel.textContent, nodes[cursor].id, "node " + cursor + ": id matches.");
+
+ let classesBox = stack.querySelector(".highlighter-nodeinfobar-classes");
+ is(classesBox.textContent, nodes[cursor].classes, "node " + cursor + ": classes match.");
+ }
+
+ function finishUp() {
+ doc = nodes = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
+
diff --git a/browser/devtools/inspector/test/browser_inspector_initialization.js b/browser/devtools/inspector/test/browser_inspector_initialization.js
new file mode 100644
index 000000000..56e6ec493
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_initialization.js
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+let doc;
+let salutation;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<div id="first" style="{ margin: 10em; ' +
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA}">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="{font-size: 12pt}">hi.</p>\n' +
+ '<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>\n' +
+ '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>\n' +
+ '<p id="closing">end transmission</p>\n' +
+ '</div>';
+ doc.title = "Inspector Initialization Test";
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ startInspectorTests(toolbox);
+ }).then(null, console.error);
+}
+
+function startInspectorTests(toolbox)
+{
+ 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, gBrowser.selectedTab, "Valid target");
+ ok(inspector.highlighter, "Highlighter is up");
+
+ let p = doc.querySelector("p");
+
+ inspector.selection.setNode(p);
+
+ testHighlighter(p);
+ testMarkupView(p);
+ testBreadcrumbs(p);
+
+ let span = doc.querySelector("span");
+ span.scrollIntoView();
+
+ inspector.selection.setNode(span);
+
+ testHighlighter(span);
+ testMarkupView(span);
+ testBreadcrumbs(span);
+
+ toolbox.once("destroyed", function() {
+ ok("true", "'destroyed' notification received.");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
+ executeSoon(runContextMenuTest);
+ });
+ toolbox.destroy();
+}
+
+
+function testHighlighter(node)
+{
+ ok(isHighlighting(), "Highlighter is highlighting");
+ is(getHighlitNode(), node, "Right node is highlighted");
+}
+
+function testMarkupView(node)
+{
+ let i = getActiveInspector();
+ is(i.markup._selectedContainer.node, node, "Right node is selected in the markup view");
+}
+
+function testBreadcrumbs(node)
+{
+ let b = getActiveInspector().breadcrumbs;
+ let expectedText = b.prettyPrintNodeAsText(node);
+ let button = b.container.querySelector("button[checked=true]");
+ ok(button, "A crumbs is checked=true");
+ is(button.getAttribute("tooltiptext"), expectedText, "Crumb refers to the right node");
+}
+
+function _clickOnInspectMenuItem(node) {
+ document.popupNode = node;
+ var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+ return contextMenu.inspectNode();
+}
+
+function runContextMenuTest()
+{
+ salutation = doc.getElementById("salutation");
+ _clickOnInspectMenuItem(salutation).then(testInitialNodeIsSelected);
+}
+
+function testInitialNodeIsSelected() {
+ testHighlighter(salutation);
+ testMarkupView(salutation);
+ testBreadcrumbs(salutation);
+ inspectNodesFromContextTestWhileOpen();
+}
+
+function inspectNodesFromContextTestWhileOpen()
+{
+ let closing = doc.getElementById("closing");
+ getActiveInspector().selection.once("new-node", function() {
+ ok(true, "Get selection's 'new-node' selection");
+ executeSoon(function() {
+ testHighlighter(closing);
+ testMarkupView(closing);
+ testBreadcrumbs(closing);
+ finishInspectorTests();
+ }
+ )});
+ _clickOnInspectMenuItem(closing);
+}
+
+function finishInspectorTests(subject, topic, aWinIdString)
+{
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_invalidate.js b/browser/devtools/inspector/test/browser_inspector_invalidate.js
new file mode 100644
index 000000000..3fc9a2043
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_invalidate.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+
+ let doc;
+ let div;
+ let inspector;
+
+ function createDocument()
+ {
+ div = doc.createElement("div");
+ div.setAttribute("style", "width: 100px; height: 100px; background:yellow;");
+ doc.body.appendChild(div);
+
+ openInspector(runTest);
+ }
+
+ function runTest(inspector)
+ {
+ inspector.selection.setNode(div);
+
+ executeSoon(function() {
+ let outline = inspector.highlighter.outline;
+ is(outline.style.width, "100px", "selection has the right width");
+
+ div.style.width = "200px";
+ function pollTest() {
+ if (outline.style.width == "100px") {
+ setTimeout(pollTest, 10);
+ return;
+ }
+ is(outline.style.width, "200px", "selection updated");
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+ setTimeout(pollTest, 10);
+ });
+ }
+
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_menu.html b/browser/devtools/inspector/test/browser_inspector_menu.html
new file mode 100644
index 000000000..52c94923b
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_menu.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<head>
+ <title>Inspector Tree Menu Test</title>
+</head>
+<body>
+ <div>
+ <h1>Inspector Tree Menu Test</h1>
+ <p>This is some example text</p>
+ </div>
+</body>
diff --git a/browser/devtools/inspector/test/browser_inspector_menu.js b/browser/devtools/inspector/test/browser_inspector_menu.js
new file mode 100644
index 000000000..8e257b1b8
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_menu.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+
+ waitForExplicitFinish();
+
+ let doc;
+ let inspector;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/" +
+ "inspector/test/browser_inspector_menu.html";
+
+ function setupTest() {
+ openInspector(runTests);
+ }
+
+ function runTests(aInspector) {
+ inspector = aInspector;
+ checkDocTypeMenuItems();
+ }
+
+ function checkDocTypeMenuItems() {
+ info("Checking context menu entries for doctype node");
+ inspector.selection.setNode(doc.doctype);
+ let docTypeNode = getMarkupTagNodeContaining("<!DOCTYPE html>");
+
+ // Right-click doctype tag
+ contextMenuClick(docTypeNode);
+
+ checkDisabled("node-menu-copyinner");
+ checkDisabled("node-menu-copyouter");
+ checkDisabled("node-menu-copyuniqueselector");
+ checkDisabled("node-menu-delete");
+
+ for (let name of ["hover", "active", "focus"]) {
+ checkDisabled("node-menu-pseudo-" + name);
+ }
+
+ checkElementMenuItems();
+ }
+
+ function checkElementMenuItems() {
+ info("Checking context menu entries for p tag");
+ inspector.selection.setNode(doc.querySelector("p"));
+ let tag = getMarkupTagNodeContaining("p");
+
+ // Right-click p tag
+ contextMenuClick(tag);
+
+ checkEnabled("node-menu-copyinner");
+ checkEnabled("node-menu-copyouter");
+ checkEnabled("node-menu-copyuniqueselector");
+ checkEnabled("node-menu-delete");
+
+ for (let name of ["hover", "active", "focus"]) {
+ checkEnabled("node-menu-pseudo-" + name);
+ }
+
+ testCopyInnerMenu();
+ }
+
+ function testCopyInnerMenu() {
+ let copyInner = inspector.panelDoc.getElementById("node-menu-copyinner");
+ ok(copyInner, "the popup menu has a copy inner html menu item");
+
+ waitForClipboard("This is some example text",
+ function() { copyInner.doCommand(); },
+ testCopyOuterMenu, testCopyOuterMenu);
+ }
+
+ function testCopyOuterMenu() {
+ let copyOuter = inspector.panelDoc.getElementById("node-menu-copyouter");
+ ok(copyOuter, "the popup menu has a copy outer html menu item");
+
+ waitForClipboard("<p>This is some example text</p>",
+ function() { copyOuter.doCommand(); },
+ testCopyUniqueSelectorMenu, testCopyUniqueSelectorMenu);
+ }
+
+ function testCopyUniqueSelectorMenu() {
+ let copyUniqueSelector = inspector.panelDoc.getElementById("node-menu-copyuniqueselector");
+ ok(copyUniqueSelector, "the popup menu has a copy unique selector menu item");
+
+ waitForClipboard("body > div:nth-child(1) > p:nth-child(2)",
+ function() { copyUniqueSelector.doCommand(); },
+ testDeleteNode, testDeleteNode);
+ }
+
+ function testDeleteNode() {
+ let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
+ ok(deleteNode, "the popup menu has a delete menu item");
+
+ inspector.selection.once("detached", deleteTest);
+
+ let commandEvent = document.createEvent("XULCommandEvent");
+ commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
+ false, false, null);
+ deleteNode.dispatchEvent(commandEvent);
+ }
+
+ function deleteTest() {
+ let p = doc.querySelector("P");
+ is(p, null, "node deleted");
+
+ deleteRootNode();
+ }
+
+ function deleteRootNode() {
+ inspector.selection.setNode(doc.documentElement);
+ let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
+ let commandEvent = inspector.panelDoc.createEvent("XULCommandEvent");
+ commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
+ false, false, null);
+ deleteNode.dispatchEvent(commandEvent);
+ executeSoon(isRootStillAlive);
+ }
+
+ function isRootStillAlive() {
+ ok(doc.documentElement, "Document element still alive.");
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+
+ function getMarkupTagNodeContaining(text) {
+ let tags = inspector._markupFrame.contentDocument.querySelectorAll("span");
+ for (let tag of tags) {
+ if (tag.textContent == text) {
+ return tag;
+ }
+ }
+ }
+
+ function checkEnabled(elementId) {
+ let elt = inspector.panelDoc.getElementById(elementId);
+ ok(!elt.hasAttribute("disabled"),
+ '"' + elt.label + '" context menu option is not disabled');
+ }
+
+ function checkDisabled(elementId) {
+ let elt = inspector.panelDoc.getElementById(elementId);
+ ok(elt.hasAttribute("disabled"),
+ '"' + elt.label + '" context menu option is disabled');
+ }
+
+ function contextMenuClick(element) {
+ let evt = element.ownerDocument.createEvent('MouseEvents');
+ let button = 2; // right click
+
+ evt.initMouseEvent('contextmenu', true, true,
+ element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false,
+ false, false, false, button, null);
+
+ element.dispatchEvent(evt);
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js b/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
new file mode 100644
index 000000000..f8e2baf1c
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+
+ let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+ let pseudos = ["hover", "active", "focus"];
+
+ let doc;
+ let div;
+ let menu;
+ let inspector;
+
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,pseudo-class lock node menu tests";
+
+ function createDocument()
+ {
+ div = doc.createElement("div");
+ div.textContent = "test div";
+
+ doc.body.appendChild(div);
+
+ openInspector(selectNode);
+ }
+
+ function selectNode(aInspector)
+ {
+ inspector = aInspector;
+ inspector.selection.setNode(div);
+ performTests();
+ }
+
+ function performTests()
+ {
+ menu = inspector.panelDoc.getElementById("inspector-node-popup");
+ menu.addEventListener("popupshowing", testMenuItems, true);
+ menu.openPopup();
+ }
+
+ function testMenuItems()
+ {
+ menu.removeEventListener("popupshowing", testMenuItems, true);
+
+ for each (let pseudo in pseudos) {
+ let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
+ ok(menuitem, ":" + pseudo + " menuitem exists");
+
+ menuitem.doCommand();
+
+ is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
+ "pseudo-class lock has been applied");
+ }
+ finishUp();
+ }
+
+ function finishUp()
+ {
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
new file mode 100644
index 000000000..b0d9e17fc
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+let doc;
+let parentDiv, div, div2;
+let inspector;
+let ruleview;
+
+let pseudo = ":hover";
+
+function test()
+{
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,pseudo-class lock tests";
+}
+
+function createDocument()
+{
+ parentDiv = doc.createElement("div");
+ parentDiv.textContent = "parent div";
+
+ div = doc.createElement("div");
+ div.textContent = "test div";
+
+ div2 = doc.createElement("div");
+ div2.textContent = "test div2";
+
+ let head = doc.getElementsByTagName('head')[0];
+ let style = doc.createElement('style');
+ let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }');
+
+ style.appendChild(rules);
+ head.appendChild(style);
+ parentDiv.appendChild(div);
+ parentDiv.appendChild(div2);
+ doc.body.appendChild(parentDiv);
+
+ openInspector(selectNode);
+}
+
+function selectNode(aInspector)
+{
+ inspector = aInspector;
+ inspector.selection.setNode(div);
+ inspector.sidebar.once("ruleview-ready", function() {
+ ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
+ inspector.sidebar.select("ruleview");
+ performTests();
+ });
+}
+
+function performTests()
+{
+ // toggle the class
+ inspector.togglePseudoClass(pseudo);
+
+ testAdded();
+
+ // toggle the lock off
+ inspector.togglePseudoClass(pseudo);
+
+ testRemoved();
+ testRemovedFromUI();
+
+ // toggle it back on
+ inspector.togglePseudoClass(pseudo);
+
+ testNavigate();
+
+ // close the inspector
+ finishUp();
+}
+
+function testNavigate()
+{
+ inspector.selection.setNode(parentDiv);
+
+ // make sure it's still on after naving to parent
+ is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
+ "pseudo-class lock is still applied after inspecting ancestor");
+
+ inspector.selection.setNode(div2);
+
+ // make sure it's removed after naving to a non-hierarchy node
+ is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
+ "pseudo-class lock is removed after inspecting sibling node");
+
+ // toggle it back on
+ inspector.selection.setNode(div);
+ inspector.togglePseudoClass(pseudo);
+}
+
+function testAdded()
+{
+ // lock is applied to it and ancestors
+ let node = div;
+ do {
+ is(DOMUtils.hasPseudoClassLock(node, pseudo), true,
+ "pseudo-class lock has been applied");
+ node = node.parentNode;
+ } while (node.parentNode)
+
+ // infobar selector contains pseudo-class
+ let pseudoClassesBox = getActiveInspector().highlighter.nodeInfo.pseudoClassesBox;
+ is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
+
+ // ruleview contains pseudo-class rule
+ is(ruleview.element.children.length, 3,
+ "rule view is showing 3 rules for pseudo-class locked div");
+
+ is(ruleview.element.children[1]._ruleEditor.rule.selectorText,
+ "div:hover", "rule view is showing " + pseudo + " rule");
+}
+
+function testRemoved()
+{
+ // lock removed from node and ancestors
+ let node = div;
+ do {
+ is(DOMUtils.hasPseudoClassLock(node, pseudo), false,
+ "pseudo-class lock has been removed");
+ node = node.parentNode;
+ } while (node.parentNode)
+}
+
+function testRemovedFromUI()
+{
+ // infobar selector doesn't contain pseudo-class
+ let pseudoClassesBox = getActiveInspector().highlighter.nodeInfo.pseudoClassesBox;
+ is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
+
+ // ruleview no longer contains pseudo-class rule
+ is(ruleview.element.children.length, 2,
+ "rule view is showing 2 rules after removing lock");
+}
+
+function finishUp()
+{
+ gDevTools.once("toolbox-destroyed", function() {
+ testRemoved();
+ inspector = ruleview = null;
+ doc = div = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.destroy();
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_scrolling.js b/browser/devtools/inspector/test/browser_inspector_scrolling.js
new file mode 100644
index 000000000..85db0080e
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_scrolling.js
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+let doc;
+let div;
+let iframe;
+let inspector;
+
+function createDocument()
+{
+ doc.title = "Inspector scrolling Tests";
+
+ iframe = doc.createElement("iframe");
+
+ iframe.addEventListener("load", function () {
+ iframe.removeEventListener("load", arguments.callee, false);
+
+ div = iframe.contentDocument.createElement("div");
+ div.textContent = "big div";
+ div.setAttribute("style", "height:500px; width:500px; border:1px solid gray;");
+ iframe.contentDocument.body.appendChild(div);
+ openInspector(inspectNode);
+ }, false);
+
+ iframe.src = "data:text/html,foo bar";
+ doc.body.appendChild(iframe);
+}
+
+function inspectNode(aInspector)
+{
+ inspector = aInspector;
+
+ inspector.highlighter.once("locked", performScrollingTest);
+ executeSoon(function() {
+ inspector.selection.setNode(div, "");
+ });
+}
+
+function performScrollingTest()
+{
+ executeSoon(function() {
+ EventUtils.synthesizeWheel(div, 10, 10,
+ { deltaY: 50.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL },
+ iframe.contentWindow);
+ });
+
+ gBrowser.selectedBrowser.addEventListener("scroll", function() {
+ gBrowser.selectedBrowser.removeEventListener("scroll", arguments.callee,
+ false);
+
+ is(iframe.contentDocument.body.scrollTop, 50, "inspected iframe scrolled");
+
+ inspector = div = iframe = doc = null;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+ finish();
+ }, false);
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,mouse scrolling test for inspector";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_sidebarstate.js b/browser/devtools/inspector/test/browser_inspector_sidebarstate.js
new file mode 100644
index 000000000..ae4f2b561
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_sidebarstate.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let inspector;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<h1>Sidebar state test</h1>';
+ doc.title = "Sidebar State Test";
+
+ openInspector(function(panel) {
+ inspector = panel;
+ inspector.sidebar.select("ruleview");
+ inspectorRuleViewOpened();
+ });
+}
+
+function inspectorRuleViewOpened()
+{
+ is(inspector.sidebar.getCurrentTabID(), "ruleview", "Rule View is selected by default");
+
+ // Select the computed view and turn off the inspector.
+ inspector.sidebar.select("computedview");
+
+ gDevTools.once("toolbox-destroyed", inspectorClosed);
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ executeSoon(function() {
+ toolbox.destroy();
+ });
+}
+
+function inspectorClosed()
+{
+ openInspector(function(panel) {
+ inspector = panel;
+ if (inspector.sidebar.getCurrentTabID()) {
+ // Default sidebar already selected.
+ testNewDefaultTab();
+ } else {
+ // Default sidebar still to be selected.
+ inspector.sidebar.once("select", testNewDefaultTab);
+ }
+ });
+}
+
+function testNewDefaultTab()
+{
+ is(inspector.sidebar.getCurrentTabID(), "computedview", "Computed view is selected by default.");
+
+ finishTest();
+}
+
+
+function finishTest()
+{
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
diff --git a/browser/devtools/inspector/test/browser_inspector_tree_height.js b/browser/devtools/inspector/test/browser_inspector_tree_height.js
new file mode 100644
index 000000000..ad15d8c2b
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_tree_height.js
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+let doc;
+let salutation;
+let closing;
+
+const NEWHEIGHT = 226;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<div id="first" style="{ margin: 10em; ' +
+ 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA}">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="{font-size: 12pt}">hi.</p>\n' +
+ '<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>\n' +
+ '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}">Maybe more inspector test-cases!</span></p>\n' +
+ '<p id="closing">end transmission</p>\n' +
+ '</div>';
+ doc.title = "Inspector Initialization Test";
+ startInspectorTests();
+}
+
+function startInspectorTests()
+{
+ ok(InspectorUI, "InspectorUI variable exists");
+ Services.obs.addObserver(runInspectorTests,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+ InspectorUI.toggleInspectorUI();
+}
+
+function runInspectorTests()
+{
+ Services.obs.removeObserver(runInspectorTests,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+
+ if (InspectorUI.treePanelEnabled) {
+ Services.obs.addObserver(treePanelTests,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
+
+ InspectorUI.stopInspecting();
+
+ InspectorUI.treePanel.open();
+ } else
+ finishInspectorTests();
+}
+
+function treePanelTests()
+{
+ Services.obs.removeObserver(treePanelTests,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
+ Services.obs.addObserver(treePanelTests2,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
+
+ ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
+
+ let height = Services.prefs.getIntPref("devtools.inspector.htmlHeight");
+
+ is(InspectorUI.treePanel.container.height, height,
+ "Container height is " + height);
+
+ InspectorUI.treePanel.container.height = NEWHEIGHT;
+
+ executeSoon(function() {
+ InspectorUI.treePanel.close();
+ InspectorUI.treePanel.open();
+ });
+}
+
+function treePanelTests2()
+{
+ Services.obs.removeObserver(treePanelTests2,
+ InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
+
+ ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
+
+ let height = Services.prefs.getIntPref("devtools.inspector.htmlHeight");
+
+ is(InspectorUI.treePanel.container.height, NEWHEIGHT,
+ "Container height is now " + height);
+
+ InspectorUI.treePanel.close();
+ executeSoon(function() {
+ finishInspectorTests()
+ });
+}
+
+function finishInspectorTests()
+{
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic tests for inspector";
+}
+
diff --git a/browser/devtools/inspector/test/head.js b/browser/devtools/inspector/test/head.js
new file mode 100644
index 000000000..e5b84ee0c
--- /dev/null
+++ b/browser/devtools/inspector/test/head.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+let tempScope = {};
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
+let LayoutHelpers = tempScope.LayoutHelpers;
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+let TargetFactory = devtools.TargetFactory;
+
+Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+let console = tempScope.console;
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+
+function openInspector(callback)
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ callback(toolbox.getCurrentPanel(), toolbox);
+ }).then(null, console.error);
+}
+
+function getActiveInspector()
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.getToolbox(target).getPanel("inspector");
+}
+
+function isHighlighting()
+{
+ let outline = getActiveInspector().highlighter.outline;
+ return !(outline.getAttribute("hidden") == "true");
+}
+
+function getHighlitNode()
+{
+ let h = getActiveInspector().highlighter;
+ if (!isHighlighting() || !h._contentRect)
+ return null;
+
+ let a = {
+ x: h._contentRect.left,
+ y: h._contentRect.top
+ };
+
+ let b = {
+ x: a.x + h._contentRect.width,
+ y: a.y + h._contentRect.height
+ };
+
+ // Get midpoint of diagonal line.
+ let midpoint = midPoint(a, b);
+
+ return LayoutHelpers.getElementFromPoint(h.win.document, midpoint.x,
+ midpoint.y);
+}
+
+
+function midPoint(aPointA, aPointB)
+{
+ let pointC = { };
+ pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
+ pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
+ return pointC;
+}
+
+function computedView()
+{
+ let sidebar = getActiveInspector().sidebar;
+ let iframe = sidebar.tabbox.querySelector(".iframe-computedview");
+ return iframe.contentWindow.computedView;
+}
+
+function computedViewTree()
+{
+ return computedView().view;
+}
+
+function ruleView()
+{
+ let sidebar = getActiveInspector().sidebar;
+ let iframe = sidebar.tabbox.querySelector(".iframe-ruleview");
+ return iframe.contentWindow.ruleView;
+}
+
+function synthesizeKeyFromKeyTag(aKeyId) {
+ let key = document.getElementById(aKeyId);
+ isnot(key, null, "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("ctrl"),
+ altKey: modifiersAttr.match("alt"),
+ metaKey: modifiersAttr.match("meta"),
+ accelKey: modifiersAttr.match("accel")
+ }
+
+ EventUtils.synthesizeKey(name, modifiers);
+}
+
+function focusSearchBoxUsingShortcut(panelWin, callback) {
+ panelWin.focus();
+ let key = panelWin.document.getElementById("nodeSearchKey");
+ isnot(key, null, "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("ctrl"),
+ altKey: modifiersAttr.match("alt"),
+ metaKey: modifiersAttr.match("meta"),
+ accelKey: modifiersAttr.match("accel")
+ }
+
+ let searchBox = panelWin.document.getElementById("inspector-searchbox");
+ searchBox.addEventListener("focus", function onFocus() {
+ searchBox.removeEventListener("focus", onFocus, false);
+ callback && callback();
+ }, false);
+ EventUtils.synthesizeKey(name, modifiers);
+}
+
diff --git a/browser/devtools/inspector/test/moz.build b/browser/devtools/inspector/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/inspector/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/jar.mn b/browser/devtools/jar.mn
new file mode 100644
index 000000000..8d91a547e
--- /dev/null
+++ b/browser/devtools/jar.mn
@@ -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/.
+
+browser.jar:
+ content/browser/devtools/widgets.css (shared/widgets/widgets.css)
+ content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
+ content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml)
+ content/browser/devtools/markup-view.css (markupview/markup-view.css)
+ content/browser/devtools/netmonitor.xul (netmonitor/netmonitor.xul)
+ content/browser/devtools/netmonitor.css (netmonitor/netmonitor.css)
+ content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
+ content/browser/devtools/netmonitor-view.js (netmonitor/netmonitor-view.js)
+ content/browser/devtools/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml)
+ content/browser/devtools/webconsole.js (webconsole/webconsole.js)
+* content/browser/devtools/webconsole.xul (webconsole/webconsole.xul)
+* content/browser/devtools/scratchpad.xul (scratchpad/scratchpad.xul)
+ content/browser/devtools/scratchpad.js (scratchpad/scratchpad.js)
+ content/browser/devtools/splitview.css (shared/splitview.css)
+ content/browser/devtools/theme-switching.js (shared/theme-switching.js)
+ content/browser/devtools/styleeditor.xul (styleeditor/styleeditor.xul)
+ content/browser/devtools/styleeditor.css (styleeditor/styleeditor.css)
+ content/browser/devtools/computedview.xhtml (styleinspector/computedview.xhtml)
+ content/browser/devtools/cssruleview.xhtml (styleinspector/cssruleview.xhtml)
+ content/browser/devtools/ruleview.css (styleinspector/ruleview.css)
+ content/browser/devtools/layoutview/view.js (layoutview/view.js)
+ content/browser/devtools/layoutview/view.xhtml (layoutview/view.xhtml)
+ content/browser/devtools/layoutview/view.css (layoutview/view.css)
+ content/browser/devtools/fontinspector/font-inspector.js (fontinspector/font-inspector.js)
+ content/browser/devtools/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml)
+ content/browser/devtools/fontinspector/font-inspector.css (fontinspector/font-inspector.css)
+ content/browser/devtools/orion.js (sourceeditor/orion/orion.js)
+* content/browser/devtools/source-editor-overlay.xul (sourceeditor/source-editor-overlay.xul)
+ content/browser/devtools/debugger.xul (debugger/debugger.xul)
+ content/browser/devtools/debugger.css (debugger/debugger.css)
+ content/browser/devtools/debugger-controller.js (debugger/debugger-controller.js)
+ content/browser/devtools/debugger-view.js (debugger/debugger-view.js)
+ content/browser/devtools/debugger-toolbar.js (debugger/debugger-toolbar.js)
+ content/browser/devtools/debugger-panes.js (debugger/debugger-panes.js)
+ content/browser/devtools/profiler.xul (profiler/profiler.xul)
+ content/browser/devtools/cleopatra.html (profiler/cleopatra/cleopatra.html)
+ content/browser/devtools/profiler/cleopatra/css/ui.css (profiler/cleopatra/css/ui.css)
+ content/browser/devtools/profiler/cleopatra/css/tree.css (profiler/cleopatra/css/tree.css)
+ content/browser/devtools/profiler/cleopatra/css/devtools.css (profiler/cleopatra/css/devtools.css)
+ content/browser/devtools/profiler/cleopatra/js/strings.js (profiler/cleopatra/js/strings.js)
+ content/browser/devtools/profiler/cleopatra/js/parser.js (profiler/cleopatra/js/parser.js)
+ content/browser/devtools/profiler/cleopatra/js/parserWorker.js (profiler/cleopatra/js/parserWorker.js)
+ content/browser/devtools/profiler/cleopatra/js/tree.js (profiler/cleopatra/js/tree.js)
+ content/browser/devtools/profiler/cleopatra/js/ui.js (profiler/cleopatra/js/ui.js)
+ content/browser/devtools/profiler/cleopatra/js/ProgressReporter.js (profiler/cleopatra/js/ProgressReporter.js)
+ content/browser/devtools/profiler/cleopatra/js/devtools.js (profiler/cleopatra/js/devtools.js)
+ content/browser/devtools/profiler/cleopatra/images/circlearrow.svg (profiler/cleopatra/images/circlearrow.svg)
+ content/browser/devtools/profiler/cleopatra/images/noise.png (profiler/cleopatra/images/noise.png)
+ content/browser/devtools/profiler/cleopatra/images/throbber.svg (profiler/cleopatra/images/throbber.svg)
+ content/browser/devtools/profiler/cleopatra/images/treetwisty.svg (profiler/cleopatra/images/treetwisty.svg)
+ content/browser/devtools/commandline.css (commandline/commandline.css)
+ content/browser/devtools/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
+ content/browser/devtools/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml)
+ content/browser/devtools/framework/toolbox-window.xul (framework/toolbox-window.xul)
+ content/browser/devtools/framework/toolbox-options.xul (framework/toolbox-options.xul)
+ content/browser/devtools/framework/toolbox-options.js (framework/toolbox-options.js)
+* content/browser/devtools/framework/toolbox.xul (framework/toolbox.xul)
+ content/browser/devtools/framework/toolbox.css (framework/toolbox.css)
+ content/browser/devtools/inspector/inspector.xul (inspector/inspector.xul)
+ content/browser/devtools/inspector/inspector.css (inspector/inspector.css)
+ content/browser/devtools/connect.xhtml (framework/connect/connect.xhtml)
+ content/browser/devtools/connect.css (framework/connect/connect.css)
+ content/browser/devtools/connect.js (framework/connect/connect.js)
diff --git a/browser/devtools/layoutview/moz.build b/browser/devtools/layoutview/moz.build
new file mode 100644
index 000000000..5abe8b3be
--- /dev/null
+++ b/browser/devtools/layoutview/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
+
diff --git a/browser/devtools/layoutview/test/Makefile.in b/browser/devtools/layoutview/test/Makefile.in
new file mode 100644
index 000000000..34a62289b
--- /dev/null
+++ b/browser/devtools/layoutview/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_layoutview.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/layoutview/test/browser_layoutview.js b/browser/devtools/layoutview/test/browser_layoutview.js
new file mode 100644
index 000000000..ca88b551f
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_layoutview.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("devtools.layoutview.enabled", true);
+ Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true);
+
+ let doc;
+ let node;
+ let view;
+ let inspector;
+
+ // Expected values:
+ let res1 = [
+ {selector: "#element-size", value: "160x160"},
+ {selector: ".size > span", value: "100x100"},
+ {selector: ".margin.top > span", value: 30},
+ {selector: ".margin.left > span", value: "auto"},
+ {selector: ".margin.bottom > span", value: 30},
+ {selector: ".margin.right > span", value: "auto"},
+ {selector: ".padding.top > span", value: 20},
+ {selector: ".padding.left > span", value: 20},
+ {selector: ".padding.bottom > span", value: 20},
+ {selector: ".padding.right > span", value: 20},
+ {selector: ".border.top > span", value: 10},
+ {selector: ".border.left > span", value: 10},
+ {selector: ".border.bottom > span", value: 10},
+ {selector: ".border.right > span", value: 10},
+ ];
+
+ let res2 = [
+ {selector: "#element-size", value: "190x210"},
+ {selector: ".size > span", value: "100x150"},
+ {selector: ".margin.top > span", value: 30},
+ {selector: ".margin.left > span", value: "auto"},
+ {selector: ".margin.bottom > span", value: 30},
+ {selector: ".margin.right > span", value: "auto"},
+ {selector: ".padding.top > span", value: 20},
+ {selector: ".padding.left > span", value: 20},
+ {selector: ".padding.bottom > span", value: 20},
+ {selector: ".padding.right > span", value: 50},
+ {selector: ".border.top > span", value: 10},
+ {selector: ".border.left > span", value: 10},
+ {selector: ".border.bottom > span", value: 10},
+ {selector: ".border.right > span", value: 10},
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ let style = "div { position: absolute; top: 42px; left: 42px; height: 100px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}";
+ let html = "<style>" + style + "</style><div></div>"
+ content.location = "data:text/html," + encodeURIComponent(html);
+
+ function setupTest() {
+ node = doc.querySelector("div");
+ ok(node, "node found");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ openLayoutView(toolbox.getCurrentPanel());
+ });
+ }
+
+ function openLayoutView(aInspector) {
+ inspector = aInspector;
+
+ info("Inspector open");
+
+ inspector.selection.setNode(node);
+ inspector.sidebar.select("layoutview");
+ inspector.sidebar.once("layoutview-ready", viewReady);
+ }
+
+ function viewReady() {
+ info("Layout view ready");
+
+ view = inspector.sidebar.getWindowForTab("layoutview");
+
+ ok(!!view.layoutview, "LayoutView document is alive.");
+
+ test1();
+ }
+
+ function test1() {
+ let viewdoc = view.document;
+
+ 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.");
+ }
+
+ gBrowser.selectedBrowser.addEventListener("MozAfterPaint", test2, false);
+
+ inspector.selection.node.style.height = "150px";
+ inspector.selection.node.style.paddingRight = "50px";
+ }
+
+ function test2() {
+ gBrowser.selectedBrowser.removeEventListener("MozAfterPaint", test2, false);
+
+ let viewdoc = view.document;
+
+ 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.");
+ }
+
+ executeSoon(function() {
+ gDevTools.once("toolbox-destroyed", finishUp);
+ inspector._toolbox.destroy();
+ });
+ }
+
+ function finishUp() {
+ Services.prefs.clearUserPref("devtools.layoutview.enabled");
+ Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/layoutview/test/moz.build b/browser/devtools/layoutview/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/layoutview/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/layoutview/view.css b/browser/devtools/layoutview/view.css
new file mode 100644
index 000000000..337d65ded
--- /dev/null
+++ b/browser/devtools/layoutview/view.css
@@ -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/. */
+
+body, html {
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+#header {
+ -moz-box-sizing: border-box;
+ font: 12px/16px monospace;
+ width: 100%;
+ padding: 6px 9px;
+ display: -moz-box;
+ vertical-align: top;
+}
+
+#header:-moz-dir(rtl) {
+ -moz-box-direction: reverse;
+}
+
+#header > span {
+ display: -moz-box;
+}
+
+#element-size {
+ -moz-box-flex: 1;
+}
+
+#element-size:-moz-dir(rtl) {
+ -moz-box-pack: end;
+}
+
+#main {
+ margin: 0 10px 10px 10px;
+ -moz-box-sizing: border-box;
+ width: calc(100% - 2 * 10px);
+ position: absolute;
+ border-width: 1px;
+ font: 10px/12px monospace;
+}
+
+@media (min-width: 320px) {
+ body {
+ position: absolute;
+ width: 320px;
+ left: -160px;
+ margin-left: 50%;
+ }
+}
+
+
+#margins {
+ padding: 28px;
+}
+
+#content {
+ height: 20px;
+ border-width: 1px;
+}
+
+#padding {
+ border-width: 25px;
+}
+
+#borders {
+ border-width: 2px;
+ box-shadow: 0 0 16px black;
+}
+
+#main > p {
+ position: absolute;
+ pointer-events: none;
+}
+
+#main > p {
+ margin: 0;
+ text-align: center;
+}
+
+#main > p > span {
+ vertical-align: middle;
+ pointer-events: auto;
+ cursor: default;
+}
+
+.border.top {
+ left: 0; top: 23px;
+ width: 98px;
+}
+
+.border.bottom {
+ right: 0; bottom: 22px;
+ width: 98px;
+ top: auto;
+}
+
+.border.left {
+ top: 42px; left: 0;
+ width: 56px;
+}
+
+.border.right{
+ bottom: 42px; right: 0;
+ width: 56px;
+ top: auto;
+}
+
+.top, .bottom {
+ width: calc(100% - 2px);
+ text-align: center;
+}
+
+.margin.top {
+ top: 8px;
+}
+
+.margin.bottom {
+ bottom: 6px;
+}
+
+.padding.top {
+ top: 35px;
+}
+
+.padding.bottom {
+ bottom: 35px;
+}
+
+.size,
+.margin.left,
+.margin.right,
+.padding.left,
+.padding.right {
+ top: 0;
+ line-height: 132px;
+}
+
+.size {
+ width: calc(100% - 2px);
+}
+
+.margin.right,
+.margin.left {
+ width: 28px;
+}
+
+.padding.right,
+.padding.left {
+ width: 25px;
+}
+
+.margin.right {
+ right: 0;
+}
+
+.margin.left {
+ left: 0;
+}
+
+.padding.left {
+ left: 30px;
+}
+
+.padding.right {
+ right: 30px;
+}
+
+.tooltip {
+ position: absolute;
+ bottom: 0;
+ right: 2px;
+ pointer-events: none;
+}
+
+body.dim > #main > p,
+body.dim > #main > .tooltip {
+ visibility: hidden;
+}
+
diff --git a/browser/devtools/layoutview/view.js b/browser/devtools/layoutview/view.js
new file mode 100644
index 000000000..e21b0bbf0
--- /dev/null
+++ b/browser/devtools/layoutview/view.js
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 8; 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 Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+
+let {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+
+function LayoutView(aInspector, aWindow)
+{
+ this.inspector = aInspector;
+
+ // <browser> is not always available (for Chrome targets for example)
+ if (this.inspector.target.tab) {
+ this.browser = aInspector.target.tab.linkedBrowser;
+ }
+
+ this.doc = aWindow.document;
+ this.sizeLabel = this.doc.querySelector(".size > span");
+ this.sizeHeadingLabel = this.doc.getElementById("element-size");
+
+ this.init();
+}
+
+LayoutView.prototype = {
+ init: function LV_init() {
+ this.cssLogic = new CssLogic();
+
+ this.update = this.update.bind(this);
+ this.onNewNode = this.onNewNode.bind(this);
+ this.onHighlighterLocked = this.onHighlighterLocked.bind(this);
+ this.inspector.selection.on("new-node", this.onNewNode);
+ this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.on("locked", this.onHighlighterLocked);
+ }
+
+ // Store for the different dimensions of the node.
+ // 'selector' refers to the element that holds the value in view.xhtml;
+ // 'property' is what we are measuring;
+ // 'value' is the computed dimension, computed in update().
+ this.map = {
+ marginTop: {selector: ".margin.top > span",
+ property: "margin-top",
+ value: undefined},
+ marginBottom: {selector: ".margin.bottom > span",
+ property: "margin-bottom",
+ value: undefined},
+ marginLeft: {selector: ".margin.left > span",
+ property: "margin-left",
+ value: undefined},
+ marginRight: {selector: ".margin.right > span",
+ property: "margin-right",
+ value: undefined},
+ paddingTop: {selector: ".padding.top > span",
+ property: "padding-top",
+ value: undefined},
+ paddingBottom: {selector: ".padding.bottom > span",
+ property: "padding-bottom",
+ value: undefined},
+ paddingLeft: {selector: ".padding.left > span",
+ property: "padding-left",
+ value: undefined},
+ paddingRight: {selector: ".padding.right > span",
+ property: "padding-right",
+ value: undefined},
+ borderTop: {selector: ".border.top > span",
+ property: "border-top-width",
+ value: undefined},
+ borderBottom: {selector: ".border.bottom > span",
+ property: "border-bottom-width",
+ value: undefined},
+ borderLeft: {selector: ".border.left > span",
+ property: "border-left-width",
+ value: undefined},
+ borderRight: {selector: ".border.right > span",
+ property: "border-right-width",
+ value: undefined},
+ };
+
+ this.onNewNode();
+ },
+
+ /**
+ * Is the layoutview visible in the sidebar?
+ */
+ isActive: function LV_isActive() {
+ return this.inspector.sidebar.getCurrentTabID() == "layoutview";
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function LV_destroy() {
+ this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
+ this.inspector.selection.off("new-node", this.onNewNode);
+ if (this.browser) {
+ this.browser.removeEventListener("MozAfterPaint", this.update, true);
+ }
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.off("locked", this.onHighlighterLocked);
+ }
+ this.sizeHeadingLabel = null;
+ this.sizeLabel = null;
+ this.inspector = null;
+ this.doc = null;
+ },
+
+ /**
+ * Selection 'new-node' event handler.
+ */
+ onNewNode: function LV_onNewNode() {
+ if (this.isActive() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode() &&
+ this.inspector.selection.reason != "highlighter") {
+ this.cssLogic.highlight(this.inspector.selection.node);
+ this.undim();
+ } else {
+ this.dim();
+ }
+ this.update();
+ },
+
+ /**
+ * Highlighter 'locked' event handler
+ */
+ onHighlighterLocked: function LV_onHighlighterLocked() {
+ this.cssLogic.highlight(this.inspector.selection.node);
+ this.undim();
+ this.update();
+ },
+
+ /**
+ * Hide the layout boxes. No node are selected.
+ */
+ dim: function LV_dim() {
+ if (this.browser) {
+ this.browser.removeEventListener("MozAfterPaint", this.update, true);
+ }
+ this.trackingPaint = false;
+ this.doc.body.classList.add("dim");
+ this.dimmed = true;
+ },
+
+ /**
+ * Show the layout boxes. A node is selected.
+ */
+ undim: function LV_undim() {
+ if (!this.trackingPaint) {
+ if (this.browser) {
+ this.browser.addEventListener("MozAfterPaint", this.update, true);
+ }
+ this.trackingPaint = true;
+ }
+ this.doc.body.classList.remove("dim");
+ this.dimmed = false;
+ },
+
+ /**
+ * Compute the dimensions of the node and update the values in
+ * the layoutview/view.xhtml document.
+ */
+ update: function LV_update() {
+ if (!this.isActive() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ return;
+ }
+
+ let node = this.inspector.selection.node;
+
+ // First, we update the first part of the layout view, with
+ // the size of the element.
+
+ let clientRect = node.getBoundingClientRect();
+ let width = Math.round(clientRect.width);
+ let height = Math.round(clientRect.height);
+
+ let newLabel = width + "x" + height;
+ if (this.sizeHeadingLabel.textContent != newLabel) {
+ this.sizeHeadingLabel.textContent = newLabel;
+ }
+
+ // If the view is dimmed, no need to do anything more.
+ if (this.dimmed) return;
+
+ // We compute and update the values of margins & co.
+ let style = node.ownerDocument.defaultView.getComputedStyle(node);
+
+ for (let i in this.map) {
+ let property = this.map[i].property;
+ this.map[i].value = parseInt(style.getPropertyValue(property));
+ }
+
+ let margins = this.processMargins(node);
+ 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);
+ if (span.textContent.length > 0 &&
+ span.textContent == this.map[i].value) {
+ continue;
+ }
+ span.textContent = this.map[i].value;
+ }
+
+ width -= this.map.borderLeft.value + this.map.borderRight.value +
+ this.map.paddingLeft.value + this.map.paddingRight.value;
+
+ height -= this.map.borderTop.value + this.map.borderBottom.value +
+ this.map.paddingTop.value + this.map.paddingBottom.value;
+
+ let newValue = width + "x" + height;
+ if (this.sizeLabel.textContent != newValue) {
+ this.sizeLabel.textContent = newValue;
+ }
+ },
+
+ /**
+ * Find margins declared 'auto'
+ */
+ processMargins: function LV_processMargins(node) {
+ let margins = {};
+
+ for each (let prop in ["top", "bottom", "left", "right"]) {
+ let info = this.cssLogic.getPropertyInfo("margin-" + prop);
+ let selectors = info.matchedSelectors;
+ if (selectors && selectors.length > 0 && selectors[0].value == "auto") {
+ margins[prop] = "auto";
+ }
+ }
+
+ return margins;
+ },
+}
diff --git a/browser/devtools/layoutview/view.xhtml b/browser/devtools/layoutview/view.xhtml
new file mode 100644
index 000000000..5b0dcb100
--- /dev/null
+++ b/browser/devtools/layoutview/view.xhtml
@@ -0,0 +1,111 @@
+<?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 % layoutviewDTD SYSTEM "chrome://browser/locale/devtools/layoutview.dtd" >
+ %layoutviewDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&title;</title>
+
+ <script id="script-theme" type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8" src="view.js"></script>
+ <script type="application/javascript;version=1.8">
+ <![CDATA[
+ let elts;
+ let tooltip;
+
+ const Ci = Components.interfaces;
+ const Cc = Components.classes;
+
+ window.setPanel = function(panel) {
+ this.layoutview = new LayoutView(panel, window);
+
+ // Tooltip mechanism
+ elts = document.querySelectorAll("*[tooltip]");
+ tooltip = document.querySelector(".tooltip");
+ for (let i = 0; i < elts.length; i++) {
+ let elt = elts[i];
+ elt.addEventListener("mouseover", onmouseover, true);
+ elt.addEventListener("mouseout", onmouseout, true);
+ }
+
+ // Mark document as RTL or LTR:
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry);
+ let dir = chromeReg.isLocaleRTL("global");
+ document.body.setAttribute("dir", dir ? "rtl" : "ltr");
+
+ window.parent.postMessage("layoutview-ready", "*");
+ }
+
+ window.onunload = function() {
+ this.layoutview.destroy();
+ if (elts) {
+ for (let i = 0; i < elts.length; i++) {
+ let elt = elts[i];
+ elt.removeEventListener("mouseover", onmouseover, true);
+ elt.removeEventListener("mouseout", onmouseout, true);
+ }
+ }
+ }
+
+ function onmouseover(e) {
+ tooltip.textContent = e.target.getAttribute("tooltip");
+ }
+
+ function onmouseout(e) {
+ tooltip.textContent = "";
+ }
+ ]]>
+ </script>
+
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/layoutview.css" type="text/css"/>
+ <link rel="stylesheet" href="view.css" type="text/css"/>
+
+ </head>
+ <body class="theme-body">
+
+ <p id="header">
+ <span id="element-size"></span>
+ </p>
+
+ <div id="main">
+
+ <div id="margins" tooltip="&margins.tooltip;">
+ <div id="borders" tooltip="&borders.tooltip;">
+ <div id="padding" tooltip="&padding.tooltip;">
+ <div id="content" tooltip="&content.tooltip;">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <p class="border top"><span tooltip="border-top"></span></p>
+ <p class="border right"><span tooltip="border-right"></span></p>
+ <p class="border bottom"><span tooltip="border-bottom"></span></p>
+ <p class="border left"><span tooltip="border-left"></span></p>
+
+ <p class="margin top"><span tooltip="margin-top"></span></p>
+ <p class="margin right"><span tooltip="margin-right"></span></p>
+ <p class="margin bottom"><span tooltip="margin-bottom"></span></p>
+ <p class="margin left"><span tooltip="margin-left"></span></p>
+
+ <p class="padding top"><span tooltip="padding-top"></span></p>
+ <p class="padding right"><span tooltip="padding-right"></span></p>
+ <p class="padding bottom"><span tooltip="padding-bottom"></span></p>
+ <p class="padding left"><span tooltip="padding-left"></span></p>
+
+ <p class="size"><span tooltip="&content.tooltip;"></span></p>
+
+ <span class="tooltip"></span>
+
+ </div>
+
+ </body>
+</html>
diff --git a/browser/devtools/main.js b/browser/devtools/main.js
new file mode 100644
index 000000000..5475a5e49
--- /dev/null
+++ b/browser/devtools/main.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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");
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+Object.defineProperty(exports, "Toolbox", {
+ get: () => require("devtools/framework/toolbox").Toolbox
+});
+Object.defineProperty(exports, "TargetFactory", {
+ get: () => require("devtools/framework/target").TargetFactory
+});
+
+loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
+
+// Panels
+loader.lazyGetter(this, "OptionsPanel", function() require("devtools/framework/toolbox-options").OptionsPanel);
+loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector/inspector-panel").InspectorPanel);
+loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
+loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm");
+loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
+loader.lazyImporter(this, "ProfilerPanel", "resource:///modules/devtools/ProfilerPanel.jsm");
+loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm");
+
+// Strings
+const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
+const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
+const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
+const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
+const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
+const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
+const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
+loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
+loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
+loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
+loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
+loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
+loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
+loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
+
+let Tools = {};
+exports.Tools = Tools;
+
+// Definitions
+Tools.options = {
+ id: "options",
+ ordinal: 0,
+ url: "chrome://browser/content/devtools/framework/toolbox-options.xul",
+ icon: "chrome://browser/skin/devtools/tool-options.png",
+ tooltip: l10n("optionsButton.tooltip", toolboxStrings),
+ isTargetSupported: function(target) {
+ return true;
+ },
+ build: function(iframeWindow, toolbox) {
+ let panel = new OptionsPanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+}
+
+Tools.webConsole = {
+ id: "webconsole",
+ key: l10n("cmd.commandkey", webConsoleStrings),
+ accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
+ modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
+ ordinal: 1,
+ icon: "chrome://browser/skin/devtools/tool-webconsole.png",
+ url: "chrome://browser/content/devtools/webconsole.xul",
+ label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
+ menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
+ tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
+
+ isTargetSupported: function(target) {
+ return true;
+ },
+ build: function(iframeWindow, toolbox) {
+ let panel = new WebConsolePanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+};
+
+Tools.inspector = {
+ id: "inspector",
+ accesskey: l10n("inspector.accesskey", inspectorStrings),
+ key: l10n("inspector.commandkey", inspectorStrings),
+ ordinal: 2,
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ icon: "chrome://browser/skin/devtools/tool-inspector.png",
+ url: "chrome://browser/content/devtools/inspector/inspector.xul",
+ label: l10n("inspector.label", inspectorStrings),
+ tooltip: l10n("inspector.tooltip", inspectorStrings),
+
+ isTargetSupported: function(target) {
+ return !target.isRemote;
+ },
+
+ build: function(iframeWindow, toolbox) {
+ let panel = new InspectorPanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+};
+
+Tools.jsdebugger = {
+ id: "jsdebugger",
+ key: l10n("debuggerMenu.commandkey", debuggerStrings),
+ accesskey: l10n("debuggerMenu.accesskey", debuggerStrings),
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ ordinal: 3,
+ visibilityswitch: "devtools.debugger.enabled",
+ icon: "chrome://browser/skin/devtools/tool-debugger.png",
+ highlightedicon: "chrome://browser/skin/devtools/tool-debugger-paused.png",
+ url: "chrome://browser/content/devtools/debugger.xul",
+ label: l10n("ToolboxDebugger.label", debuggerStrings),
+ tooltip: l10n("ToolboxDebugger.tooltip", debuggerStrings),
+
+ isTargetSupported: function(target) {
+ return true;
+ },
+
+ build: function(iframeWindow, toolbox) {
+ let panel = new DebuggerPanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+};
+
+Tools.styleEditor = {
+ id: "styleeditor",
+ key: l10n("open.commandkey", styleEditorStrings),
+ ordinal: 4,
+ accesskey: l10n("open.accesskey", styleEditorStrings),
+ modifiers: "shift",
+ icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
+ url: "chrome://browser/content/devtools/styleeditor.xul",
+ label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
+ tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings),
+
+ isTargetSupported: function(target) {
+ return true;
+ },
+
+ build: function(iframeWindow, toolbox) {
+ let panel = new StyleEditorPanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+};
+
+Tools.jsprofiler = {
+ id: "jsprofiler",
+ accesskey: l10n("profiler.accesskey", profilerStrings),
+ key: l10n("profiler2.commandkey", profilerStrings),
+ ordinal: 5,
+ modifiers: "shift",
+ visibilityswitch: "devtools.profiler.enabled",
+ icon: "chrome://browser/skin/devtools/tool-profiler.png",
+ url: "chrome://browser/content/devtools/profiler.xul",
+ label: l10n("profiler.label", profilerStrings),
+ tooltip: l10n("profiler.tooltip2", profilerStrings),
+
+ isTargetSupported: function (target) {
+ return true;
+ },
+
+ build: function (frame, target) {
+ let panel = new ProfilerPanel(frame, target);
+ return panel.open();
+ }
+};
+
+Tools.netMonitor = {
+ id: "netmonitor",
+ accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
+ key: l10n("netmonitor.commandkey", netMonitorStrings),
+ ordinal: 6,
+ modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+ visibilityswitch: "devtools.netmonitor.enabled",
+ icon: "chrome://browser/skin/devtools/tool-network.png",
+ url: "chrome://browser/content/devtools/netmonitor.xul",
+ label: l10n("netmonitor.label", netMonitorStrings),
+ tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
+
+ isTargetSupported: function(target) {
+ return true;
+ },
+
+ build: function(iframeWindow, toolbox) {
+ let panel = new NetMonitorPanel(iframeWindow, toolbox);
+ return panel.open();
+ }
+};
+
+let defaultTools = [
+ Tools.options,
+ Tools.styleEditor,
+ Tools.webConsole,
+ Tools.jsdebugger,
+ Tools.inspector,
+ Tools.jsprofiler,
+ Tools.netMonitor
+];
+
+exports.defaultTools = defaultTools;
+
+for (let definition of defaultTools) {
+ gDevTools.registerTool(definition);
+}
+
+var unloadObserver = {
+ observe: function(subject, topic, data) {
+ if (subject.wrappedJSObject === require("@loader/unload")) {
+ Services.obs.removeObserver(unloadObserver, "sdk:loader:destroy");
+ for (let definition of gDevTools.getToolDefinitionArray()) {
+ gDevTools.unregisterTool(definition.id);
+ }
+ }
+ }
+};
+Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false);
+
+/**
+ * Lookup l10n string from a string bundle.
+ *
+ * @param {string} name
+ * The key to lookup.
+ * @param {StringBundle} bundle
+ * The key to lookup.
+ * @returns A localized version of the given key.
+ */
+function l10n(name, bundle)
+{
+ try {
+ return bundle.GetStringFromName(name);
+ } catch (ex) {
+ Services.console.logStringMessage("Error reading '" + name + "'");
+ throw new Error("l10n error with " + name);
+ }
+}
diff --git a/browser/devtools/markupview/Makefile.in b/browser/devtools/markupview/Makefile.in
new file mode 100644
index 000000000..83e6e11a1
--- /dev/null
+++ b/browser/devtools/markupview/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/markupview
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/markupview/markup-view.css b/browser/devtools/markupview/markup-view.css
new file mode 100644
index 000000000..356acce7e
--- /dev/null
+++ b/browser/devtools/markupview/markup-view.css
@@ -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/. */
+
+ul {
+ list-style: none;
+}
+
+ul.children:not([expanded]) {
+ display: none;
+}
+
+.codebox {
+ display: inline-block;
+}
+
+.newattr {
+ display: inline-block;
+ width: 1em;
+ height: 1ex;
+}
+
+.summary {
+ cursor: pointer;
+}
+
+.summary[expanded] {
+ display: none;
+}
+
+#root {
+ margin-right: 80px;
+}
+
+/* Preview */
+
+#previewbar {
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 90px;
+ background: black;
+ border-left: 1px solid #333;
+ border-bottom: 1px solid #333;
+ overflow: hidden;
+}
+
+#preview {
+ position: absolute;
+ top: 0;
+ right: 5px;
+ width: 80px;
+ height: 100%;
+ background-image: -moz-element(#root);
+ background-repeat: no-repeat;
+}
+
+#previewbar.hide,
+#previewbar.disabled {
+ display: none;
+}
+
+#viewbox {
+ position: absolute;
+ top: 0;
+ right: 5px;
+ width: 80px;
+ border: 1px dashed #888;
+ background: rgba(205,205,255,0.2);
+ outline: 1px solid transparent;
+}
diff --git a/browser/devtools/markupview/markup-view.js b/browser/devtools/markupview/markup-view.js
new file mode 100644
index 000000000..6f1888d3b
--- /dev/null
+++ b/browser/devtools/markupview/markup-view.js
@@ -0,0 +1,1529 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const {Cc, Cu, Ci} = require("chrome");
+
+// Page size for pageup/pagedown
+const PAGE_SIZE = 10;
+
+const PREVIEW_AREA = 700;
+const DEFAULT_MAX_CHILDREN = 100;
+
+let {UndoStack} = require("devtools/shared/undo");
+let EventEmitter = require("devtools/shared/event-emitter");
+let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
+
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/Templater.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Vocabulary for the purposes of this file:
+ *
+ * MarkupContainer - the structure that holds an editor and its
+ * immediate children in the markup panel.
+ * 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 aInspector
+ * The inspector we're watching.
+ * @param iframe aFrame
+ * An iframe in which the caller has kindly loaded markup-view.xhtml.
+ */
+function MarkupView(aInspector, aFrame, aControllerWindow)
+{
+ this._inspector = aInspector;
+ this._frame = aFrame;
+ this.doc = this._frame.contentDocument;
+ this._elt = this.doc.querySelector("#root");
+
+ try {
+ this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
+ } catch(ex) {
+ this.maxChildren = DEFAULT_MAX_CHILDREN;
+ }
+
+ this.undo = new UndoStack();
+ this.undo.installController(aControllerWindow);
+
+ this._containers = new WeakMap();
+
+ this._observer = new this.doc.defaultView.MutationObserver(this._mutationObserver.bind(this));
+
+ this._boundOnNewSelection = this._onNewSelection.bind(this);
+ this._inspector.selection.on("new-node", this._boundOnNewSelection);
+ this._onNewSelection();
+
+ this._boundKeyDown = this._onKeyDown.bind(this);
+ this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
+
+ this._boundFocus = this._onFocus.bind(this);
+ this._frame.addEventListener("focus", this._boundFocus, false);
+
+ this._initPreview();
+}
+
+exports.MarkupView = MarkupView;
+
+MarkupView.prototype = {
+ _selectedContainer: null,
+
+ template: function MT_template(aName, aDest, aOptions={stack: "markup-view.xhtml"})
+ {
+ let node = this.doc.getElementById("template-" + aName).cloneNode(true);
+ node.removeAttribute("id");
+ template(node, aDest, aOptions);
+ return node;
+ },
+
+ /**
+ * Get the MarkupContainer object for a given node, or undefined if
+ * none exists.
+ */
+ getContainer: function MT_getContainer(aNode)
+ {
+ return this._containers.get(aNode);
+ },
+
+ /**
+ * Highlight the inspector selected node.
+ */
+ _onNewSelection: function MT__onNewSelection()
+ {
+ if (this._inspector.selection.isNode()) {
+ this.showNode(this._inspector.selection.node, true);
+ this.markNodeAsSelected(this._inspector.selection.node);
+ } else {
+ this.unmarkSelectedNode();
+ }
+ },
+
+ /**
+ * Create a TreeWalker to find the next/previous
+ * node for selection.
+ */
+ _selectionWalker: function MT__seletionWalker(aStart)
+ {
+ let walker = this.doc.createTreeWalker(
+ aStart || this._elt,
+ Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
+ function(aElement) {
+ if (aElement.container && aElement.container.visible) {
+ return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+ }
+ return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+ }
+ );
+ walker.currentNode = this._selectedContainer.elt;
+ return walker;
+ },
+
+ /**
+ * Key handling.
+ */
+ _onKeyDown: function MT__KeyDown(aEvent)
+ {
+ let handled = true;
+
+ // Ignore keystrokes that originated in editors.
+ if (aEvent.target.tagName.toLowerCase() === "input" ||
+ aEvent.target.tagName.toLowerCase() === "textarea") {
+ return;
+ }
+
+ switch(aEvent.keyCode) {
+ case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
+ case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
+ this.deleteNode(this._selectedContainer.node);
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
+ this.navigate(this._containers.get(this._rootNode.firstChild));
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
+ if (this._selectedContainer.expanded) {
+ this.collapseNode(this._selectedContainer.node);
+ } else {
+ let parent = this._selectionWalker().parentNode();
+ if (parent) {
+ this.navigate(parent.container);
+ }
+ }
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
+ if (!this._selectedContainer.expanded) {
+ this.expandNode(this._selectedContainer.node);
+ } else {
+ let next = this._selectionWalker().nextNode();
+ if (next) {
+ this.navigate(next.container);
+ }
+ }
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_UP:
+ let prev = this._selectionWalker().previousNode();
+ if (prev) {
+ this.navigate(prev.container);
+ }
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
+ let next = this._selectionWalker().nextNode();
+ if (next) {
+ this.navigate(next.container);
+ }
+ break;
+ case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
+ let walker = this._selectionWalker();
+ let selection = this._selectedContainer;
+ for (let i = 0; i < PAGE_SIZE; i++) {
+ let prev = walker.previousNode();
+ if (!prev) {
+ break;
+ }
+ selection = prev.container;
+ }
+ this.navigate(selection);
+ break;
+ }
+ case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: {
+ let walker = this._selectionWalker();
+ let selection = this._selectedContainer;
+ for (let i = 0; i < PAGE_SIZE; i++) {
+ let next = walker.nextNode();
+ if (!next) {
+ break;
+ }
+ selection = next.container;
+ }
+ this.navigate(selection);
+ break;
+ }
+ default:
+ handled = false;
+ }
+ if (handled) {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+ },
+
+ /**
+ * Delete a node from the DOM.
+ * This is an undoable action.
+ */
+ deleteNode: function MC__deleteNode(aNode)
+ {
+ let doc = nodeDocument(aNode);
+ if (aNode === doc ||
+ aNode === doc.documentElement ||
+ aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
+ return;
+ }
+
+ let parentNode = aNode.parentNode;
+ let sibling = aNode.nextSibling;
+
+ this.undo.do(function() {
+ if (aNode.selected) {
+ this.navigate(this._containers.get(parentNode));
+ }
+ parentNode.removeChild(aNode);
+ }.bind(this), function() {
+ parentNode.insertBefore(aNode, sibling);
+ });
+ },
+
+ /**
+ * If an editable item is focused, select its container.
+ */
+ _onFocus: function MC__onFocus(aEvent) {
+ let parent = aEvent.target;
+ while (!parent.container) {
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ this.navigate(parent.container, true);
+ }
+ },
+
+ /**
+ * Handle a user-requested navigation to a given MarkupContainer,
+ * updating the inspector's currently-selected node.
+ *
+ * @param MarkupContainer aContainer
+ * The container we're navigating to.
+ * @param aIgnoreFocus aIgnoreFocus
+ * If falsy, keyboard focus will be moved to the container too.
+ */
+ navigate: function MT__navigate(aContainer, aIgnoreFocus)
+ {
+ if (!aContainer) {
+ return;
+ }
+
+ let node = aContainer.node;
+ this.showNode(node, false);
+
+ this._inspector.selection.setNode(node, "treepanel");
+ // This event won't be fired if the node is the same. But the highlighter
+ // need to lock the node if it wasn't.
+ this._inspector.selection.emit("new-node");
+
+ if (!aIgnoreFocus) {
+ aContainer.focus();
+ }
+ },
+
+ /**
+ * Make sure a node is included in the markup tool.
+ *
+ * @param DOMNode aNode
+ * The node in the content document.
+ *
+ * @returns MarkupContainer The MarkupContainer object for this element.
+ */
+ importNode: function MT_importNode(aNode, aExpand)
+ {
+ if (!aNode) {
+ return null;
+ }
+
+ if (this._containers.has(aNode)) {
+ return this._containers.get(aNode);
+ }
+
+ this._observer.observe(aNode, {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ });
+
+ let walker = documentWalker(aNode);
+ let parent = walker.parentNode();
+ if (parent) {
+ var container = new MarkupContainer(this, aNode);
+ } else {
+ var container = new RootContainer(this, aNode);
+ this._elt.appendChild(container.elt);
+ this._rootNode = aNode;
+ aNode.addEventListener("load", function MP_watch_contentLoaded(aEvent) {
+ // Fake a childList mutation here.
+ this._mutationObserver([{target: aEvent.target, type: "childList"}]);
+ }.bind(this), true);
+ }
+
+ this._containers.set(aNode, container);
+ // FIXME: set an expando to prevent the the wrapper from disappearing
+ // See bug 819131 for details.
+ aNode.__preserveHack = true;
+ container.expanded = aExpand;
+
+ container.childrenDirty = true;
+ this._updateChildren(container);
+
+ if (parent) {
+ this.importNode(parent, true);
+ }
+ return container;
+ },
+
+ /**
+ * Mutation observer used for included nodes.
+ */
+ _mutationObserver: function MT__mutationObserver(aMutations)
+ {
+ for (let mutation of aMutations) {
+ let container = this._containers.get(mutation.target);
+ if (!container) {
+ // Container might not exist if this came from a load event for an iframe
+ // we're not viewing.
+ continue;
+ }
+ if (mutation.type === "attributes" || mutation.type === "characterData") {
+ container.update();
+ } else if (mutation.type === "childList") {
+ container.childrenDirty = true;
+ this._updateChildren(container);
+ }
+ }
+ this._inspector.emit("markupmutation");
+ },
+
+ /**
+ * Make sure the given node's parents are expanded and the
+ * node is scrolled on to screen.
+ */
+ showNode: function MT_showNode(aNode, centered)
+ {
+ let container = this.importNode(aNode);
+ this._updateChildren(container);
+ let walker = documentWalker(aNode);
+ let parent;
+ while (parent = walker.parentNode()) {
+ this._updateChildren(this.getContainer(parent));
+ this.expandNode(parent);
+ }
+ LayoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
+ },
+
+ /**
+ * Expand the container's children.
+ */
+ _expandContainer: function MT__expandContainer(aContainer)
+ {
+ if (aContainer.hasChildren && !aContainer.expanded) {
+ aContainer.expanded = true;
+ this._updateChildren(aContainer);
+ }
+ },
+
+ /**
+ * Expand the node's children.
+ */
+ expandNode: function MT_expandNode(aNode)
+ {
+ let container = this._containers.get(aNode);
+ this._expandContainer(container);
+ },
+
+ /**
+ * Expand the entire tree beneath a container.
+ *
+ * @param aContainer The container to expand.
+ */
+ _expandAll: function MT_expandAll(aContainer)
+ {
+ this._expandContainer(aContainer);
+ let child = aContainer.children.firstChild;
+ while (child) {
+ this._expandAll(child.container);
+ child = child.nextSibling;
+ }
+ },
+
+ /**
+ * Expand the entire tree beneath a node.
+ *
+ * @param aContainer The node to expand, or null
+ * to start from the top.
+ */
+ expandAll: function MT_expandAll(aNode)
+ {
+ aNode = aNode || this._rootNode;
+ this._expandAll(this._containers.get(aNode));
+ },
+
+ /**
+ * Collapse the node's children.
+ */
+ collapseNode: function MT_collapseNode(aNode)
+ {
+ let container = this._containers.get(aNode);
+ container.expanded = false;
+ },
+
+ /**
+ * Mark the given node selected.
+ */
+ markNodeAsSelected: function MT_markNodeAsSelected(aNode)
+ {
+ let container = this._containers.get(aNode);
+ if (this._selectedContainer === container) {
+ return false;
+ }
+ if (this._selectedContainer) {
+ this._selectedContainer.selected = false;
+ }
+ this._selectedContainer = container;
+ if (aNode) {
+ this._selectedContainer.selected = true;
+ }
+
+ this._ensureSelectionVisible();
+
+ return true;
+ },
+
+ /**
+ * Make sure that every ancestor of the selection are updated
+ * and included in the list of visible children.
+ */
+ _ensureSelectionVisible: function MT_ensureSelectionVisible()
+ {
+ let node = this._selectedContainer.node;
+ let walker = documentWalker(node);
+ while (node) {
+ let container = this._containers.get(node);
+ let parent = walker.parentNode();
+ if (!container.elt.parentNode) {
+ let parentContainer = this._containers.get(parent);
+ parentContainer.childrenDirty = true;
+ this._updateChildren(parentContainer, node);
+ }
+
+ node = parent;
+ }
+ },
+
+ /**
+ * Unmark selected node (no node selected).
+ */
+ unmarkSelectedNode: function MT_unmarkSelectedNode()
+ {
+ if (this._selectedContainer) {
+ this._selectedContainer.selected = false;
+ this._selectedContainer = null;
+ }
+ },
+
+ /**
+ * Called when the markup panel initiates a change on a node.
+ */
+ nodeChanged: function MT_nodeChanged(aNode)
+ {
+ if (aNode === this._inspector.selection) {
+ this._inspector.change("markupview");
+ }
+ },
+
+ /**
+ * Make sure all children of the given container's node are
+ * imported and attached to the container in the right order.
+ * @param aCentered If provided, this child will be included
+ * in the visible subset, and will be roughly centered
+ * in that list.
+ */
+ _updateChildren: function MT__updateChildren(aContainer, aCentered)
+ {
+ if (!aContainer.childrenDirty) {
+ return false;
+ }
+
+ // Get a tree walker pointing at the first child of the node.
+ let treeWalker = documentWalker(aContainer.node);
+ let child = treeWalker.firstChild();
+ aContainer.hasChildren = !!child;
+
+ if (!aContainer.expanded) {
+ return;
+ }
+
+ aContainer.childrenDirty = false;
+
+ let children = this._getVisibleChildren(aContainer, aCentered);
+ let fragment = this.doc.createDocumentFragment();
+
+ for (child of children.children) {
+ let container = this.importNode(child, false);
+ fragment.appendChild(container.elt);
+ }
+
+ while (aContainer.children.firstChild) {
+ aContainer.children.removeChild(aContainer.children.firstChild);
+ }
+
+ if (!(children.hasFirst && children.hasLast)) {
+ let data = {
+ showing: this.strings.GetStringFromName("markupView.more.showing"),
+ showAll: this.strings.formatStringFromName(
+ "markupView.more.showAll",
+ [aContainer.node.children.length.toString()], 1),
+ allButtonClick: function() {
+ aContainer.maxChildren = -1;
+ aContainer.childrenDirty = true;
+ this._updateChildren(aContainer);
+ }.bind(this)
+ };
+
+ 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);
+ }
+ }
+
+ aContainer.children.appendChild(fragment);
+
+ return true;
+ },
+
+ /**
+ * Return a list of the children to display for this container.
+ */
+ _getVisibleChildren: function MV__getVisibleChildren(aContainer, aCentered)
+ {
+ let maxChildren = aContainer.maxChildren || this.maxChildren;
+ if (maxChildren == -1) {
+ maxChildren = Number.MAX_VALUE;
+ }
+ let firstChild = documentWalker(aContainer.node).firstChild();
+ let lastChild = documentWalker(aContainer.node).lastChild();
+
+ if (!firstChild) {
+ // No children, we're done.
+ return { hasFirst: true, hasLast: true, children: [] };
+ }
+
+ // By default try to put the selected child in the middle of the list.
+ let start = aCentered || firstChild;
+
+ // Start by reading backward from the starting point....
+ let nodes = [];
+ let backwardWalker = documentWalker(start);
+ if (backwardWalker.previousSibling()) {
+ let backwardCount = Math.floor(maxChildren / 2);
+ let backwardNodes = this._readBackward(backwardWalker, backwardCount);
+ nodes = backwardNodes;
+ }
+
+ // Then read forward by any slack left in the max children...
+ let forwardWalker = documentWalker(start);
+ let forwardCount = maxChildren - nodes.length;
+ nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
+
+ // If there's any room left, it means we've run all the way to the end.
+ // In that case, there might still be more items at the front.
+ let remaining = maxChildren - nodes.length;
+ if (remaining > 0 && nodes[0] != firstChild) {
+ let firstNodes = this._readBackward(backwardWalker, remaining);
+
+ // Then put it all back together.
+ nodes = firstNodes.concat(nodes);
+ }
+
+ return {
+ hasFirst: nodes[0] == firstChild,
+ hasLast: nodes[nodes.length - 1] == lastChild,
+ children: nodes
+ };
+ },
+
+ _readForward: function MV__readForward(aWalker, aCount)
+ {
+ let ret = [];
+ let node = aWalker.currentNode;
+ do {
+ ret.push(node);
+ node = aWalker.nextSibling();
+ } while (node && --aCount);
+ return ret;
+ },
+
+ _readBackward: function MV__readBackward(aWalker, aCount)
+ {
+ let ret = [];
+ let node = aWalker.currentNode;
+ do {
+ ret.push(node);
+ node = aWalker.previousSibling();
+ } while(node && --aCount);
+ ret.reverse();
+ return ret;
+ },
+
+ /**
+ * Tear down the markup panel.
+ */
+ destroy: function MT_destroy()
+ {
+ this.undo.destroy();
+ delete this.undo;
+
+ this._frame.removeEventListener("focus", this._boundFocus, false);
+ delete this._boundFocus;
+
+ this._frame.contentWindow.removeEventListener("scroll", this._boundUpdatePreview, true);
+ this._frame.contentWindow.removeEventListener("resize", this._boundResizePreview, true);
+ this._frame.contentWindow.removeEventListener("overflow", this._boundResizePreview, true);
+ this._frame.contentWindow.removeEventListener("underflow", this._boundResizePreview, true);
+ delete this._boundUpdatePreview;
+
+ this._frame.contentWindow.removeEventListener("keydown", this._boundKeyDown, true);
+ delete this._boundKeyDown;
+
+ this._inspector.selection.off("new-node", this._boundOnNewSelection);
+ delete this._boundOnNewSelection;
+
+ delete this._elt;
+
+ delete this._containers;
+ this._observer.disconnect();
+ delete this._observer;
+ },
+
+ /**
+ * Initialize the preview panel.
+ */
+ _initPreview: function MT_initPreview()
+ {
+ if (!Services.prefs.getBoolPref("devtools.inspector.markupPreview")) {
+ return;
+ }
+
+ this._previewBar = this.doc.querySelector("#previewbar");
+ this._preview = this.doc.querySelector("#preview");
+ this._viewbox = this.doc.querySelector("#viewbox");
+
+ this._previewBar.classList.remove("disabled");
+
+ this._previewWidth = this._preview.getBoundingClientRect().width;
+
+ this._boundResizePreview = this._resizePreview.bind(this);
+ this._frame.contentWindow.addEventListener("resize", this._boundResizePreview, true);
+ this._frame.contentWindow.addEventListener("overflow", this._boundResizePreview, true);
+ this._frame.contentWindow.addEventListener("underflow", this._boundResizePreview, true);
+
+ this._boundUpdatePreview = this._updatePreview.bind(this);
+ this._frame.contentWindow.addEventListener("scroll", this._boundUpdatePreview, true);
+ this._updatePreview();
+ },
+
+
+ /**
+ * Move the preview viewbox.
+ */
+ _updatePreview: function MT_updatePreview()
+ {
+ let win = this._frame.contentWindow;
+
+ if (win.scrollMaxY == 0) {
+ this._previewBar.classList.add("disabled");
+ return;
+ }
+
+ this._previewBar.classList.remove("disabled");
+
+ let ratio = this._previewWidth / PREVIEW_AREA;
+ let width = ratio * win.innerWidth;
+
+ let height = ratio * (win.scrollMaxY + win.innerHeight);
+ let scrollTo
+ if (height >= win.innerHeight) {
+ scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY);
+ this._previewBar.setAttribute("style", "height:" + height + "px;transform:translateY(" + scrollTo + "px)");
+ } else {
+ this._previewBar.setAttribute("style", "height:100%");
+ }
+
+ let bgSize = ~~width + "px " + ~~height + "px";
+ this._preview.setAttribute("style", "background-size:" + bgSize);
+
+ let height = ~~(win.innerHeight * ratio) + "px";
+ let top = ~~(win.scrollY * ratio) + "px";
+ this._viewbox.setAttribute("style", "height:" + height + ";transform: translateY(" + top + ")");
+ },
+
+ /**
+ * Hide the preview while resizing, to avoid slowness.
+ */
+ _resizePreview: function MT_resizePreview()
+ {
+ let win = this._frame.contentWindow;
+ this._previewBar.classList.add("hide");
+ win.clearTimeout(this._resizePreviewTimeout);
+
+ win.setTimeout(function() {
+ this._updatePreview();
+ this._previewBar.classList.remove("hide");
+ }.bind(this), 1000);
+ },
+
+};
+
+
+/**
+ * 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.
+ *
+ * @param MarkupView aMarkupView
+ * The markup view that owns this container.
+ * @param DOMNode aNode
+ * The node to display.
+ */
+function MarkupContainer(aMarkupView, aNode)
+{
+ this.markup = aMarkupView;
+ this.doc = this.markup.doc;
+ this.undo = this.markup.undo;
+ this.node = aNode;
+
+ if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+ this.editor = new TextEditor(this, aNode, "text");
+ } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
+ this.editor = new TextEditor(this, aNode, "comment");
+ } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+ this.editor = new ElementEditor(this, aNode);
+ } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
+ this.editor = new DoctypeEditor(this, aNode);
+ } else {
+ this.editor = new GenericEditor(this.markup, aNode);
+ }
+
+ // The template will fill the following properties
+ this.elt = null;
+ this.expander = null;
+ this.codeBox = null;
+ this.children = null;
+ this.markup.template("container", this);
+ this.elt.container = this;
+
+ this.expander.addEventListener("click", function() {
+ this.markup.navigate(this);
+
+ if (this.expanded) {
+ this.markup.collapseNode(this.node);
+ } else {
+ this.markup.expandNode(this.node);
+ }
+ }.bind(this));
+
+ this.codeBox.insertBefore(this.editor.elt, this.children);
+
+ this.editor.elt.addEventListener("mousedown", function(evt) {
+ this.markup.navigate(this);
+ }.bind(this), false);
+
+ if (this.editor.summaryElt) {
+ this.editor.summaryElt.addEventListener("click", function(evt) {
+ this.markup.navigate(this);
+ this.markup.expandNode(this.node);
+ }.bind(this), false);
+ this.codeBox.appendChild(this.editor.summaryElt);
+ }
+
+ if (this.editor.closeElt) {
+ this.editor.closeElt.addEventListener("mousedown", function(evt) {
+ this.markup.navigate(this);
+ }.bind(this), false);
+ this.codeBox.appendChild(this.editor.closeElt);
+ }
+
+}
+
+MarkupContainer.prototype = {
+ /**
+ * 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(aValue) {
+ this._hasChildren = aValue;
+ if (aValue) {
+ this.expander.style.visibility = "visible";
+ } else {
+ this.expander.style.visibility = "hidden";
+ }
+ },
+
+ /**
+ * True if the node has been visually expanded in the tree.
+ */
+ get expanded() {
+ return this.children.hasAttribute("expanded");
+ },
+
+ set expanded(aValue) {
+ if (aValue) {
+ this.expander.setAttribute("open", "");
+ this.children.setAttribute("expanded", "");
+ if (this.editor.summaryElt) {
+ this.editor.summaryElt.setAttribute("expanded", "");
+ }
+ } else {
+ this.expander.removeAttribute("open");
+ this.children.removeAttribute("expanded");
+ if (this.editor.summaryElt) {
+ this.editor.summaryElt.removeAttribute("expanded");
+ }
+ }
+ },
+
+ /**
+ * 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(aValue) {
+ this._selected = aValue;
+ if (this._selected) {
+ this.editor.elt.classList.add("theme-selected");
+ if (this.editor.closeElt) {
+ this.editor.closeElt.classList.add("theme-selected");
+ }
+ } else {
+ this.editor.elt.classList.remove("theme-selected");
+ if (this.editor.closeElt) {
+ this.editor.closeElt.classList.remove("theme-selected");
+ }
+ }
+ },
+
+ /**
+ * Update the container's editor to the current state of the
+ * viewed node.
+ */
+ update: function MC_update()
+ {
+ if (this.editor.update) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Try to put keyboard focus on the current editor.
+ */
+ focus: function MC_focus()
+ {
+ let focusable = this.editor.elt.querySelector("[tabindex]");
+ if (focusable) {
+ focusable.focus();
+ }
+ },
+}
+
+/**
+ * Dummy container node used for the root document element.
+ */
+function RootContainer(aMarkupView, aNode)
+{
+ this.doc = aMarkupView.doc;
+ this.elt = this.doc.createElement("ul");
+ this.children = this.elt;
+ this.node = aNode;
+}
+
+/**
+ * Creates an editor for simple nodes.
+ */
+function GenericEditor(aContainer, aNode)
+{
+ this.elt = aContainer.doc.createElement("span");
+ this.elt.className = "editor";
+ this.elt.textContent = aNode.nodeName;
+}
+
+/**
+ * Creates an editor for a DOCTYPE node.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param DOMNode aNode The node being edited.
+ */
+function DoctypeEditor(aContainer, aNode)
+{
+ this.elt = aContainer.doc.createElement("span");
+ this.elt.className = "editor comment";
+ this.elt.textContent = '<!DOCTYPE ' + aNode.name +
+ (aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
+ (aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
+ '>';
+}
+
+/**
+ * Creates a simple text editor node, used for TEXT and COMMENT
+ * nodes.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param DOMNode aNode The node being edited.
+ * @param string aTemplate The template id to use to build the editor.
+ */
+function TextEditor(aContainer, aNode, aTemplate)
+{
+ this.node = aNode;
+
+ aContainer.markup.template(aTemplate, this);
+
+ editableField({
+ element: this.value,
+ stopOnReturn: true,
+ trigger: "dblclick",
+ multiline: true,
+ done: function TE_done(aVal, aCommit) {
+ if (!aCommit) {
+ return;
+ }
+ let oldValue = this.node.nodeValue;
+ aContainer.undo.do(function() {
+ this.node.nodeValue = aVal;
+ aContainer.markup.nodeChanged(this.node);
+ }.bind(this), function() {
+ this.node.nodeValue = oldValue;
+ aContainer.markup.nodeChanged(this.node);
+ }.bind(this));
+ }.bind(this)
+ });
+
+ this.update();
+}
+
+TextEditor.prototype = {
+ update: function TE_update()
+ {
+ this.value.textContent = this.node.nodeValue;
+ }
+};
+
+/**
+ * Creates an editor for an Element node.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param Element aNode The node being edited.
+ */
+function ElementEditor(aContainer, aNode)
+{
+ this.doc = aContainer.doc;
+ this.undo = aContainer.undo;
+ this.template = aContainer.markup.template.bind(aContainer.markup);
+ this.container = aContainer;
+ this.markup = this.container.markup;
+ this.node = aNode;
+
+ this.attrs = [];
+
+ // The templates will fill the following properties
+ this.elt = null;
+ this.tag = null;
+ this.attrList = null;
+ this.newAttr = null;
+ this.summaryElt = null;
+ this.closeElt = null;
+
+ // Create the main editor
+ this.template("element", this);
+
+ if (this.node.firstChild || this.node.textContent.length > 0) {
+ // Create the summary placeholder
+ this.template("elementContentSummary", this);
+ }
+
+ // Create the closing tag
+ this.template("elementClose", this);
+
+ // Make the tag name editable (unless this is a document element)
+ if (aNode != aNode.ownerDocument.documentElement) {
+ this.tag.setAttribute("tabindex", "0");
+ editableField({
+ element: this.tag,
+ trigger: "dblclick",
+ stopOnReturn: true,
+ done: this.onTagEdit.bind(this),
+ });
+ }
+
+ // Make the new attribute space editable.
+ editableField({
+ element: this.newAttr,
+ trigger: "dblclick",
+ stopOnReturn: true,
+ done: function EE_onNew(aVal, aCommit) {
+ if (!aCommit) {
+ return;
+ }
+
+ try {
+ this._applyAttributes(aVal);
+ } catch (x) {
+ return;
+ }
+ }.bind(this)
+ });
+
+ let tagName = this.node.nodeName.toLowerCase();
+ this.tag.textContent = tagName;
+ this.closeTag.textContent = tagName;
+
+ this.update();
+}
+
+ElementEditor.prototype = {
+ /**
+ * Update the state of the editor from the node.
+ */
+ update: function EE_update()
+ {
+ let attrs = this.node.attributes;
+ if (!attrs) {
+ return;
+ }
+
+ // Hide all the attribute editors, they'll be re-shown if they're
+ // still applicable. Don't update attributes that are being
+ // actively edited.
+ let attrEditors = this.attrList.querySelectorAll(".attreditor");
+ for (let i = 0; i < attrEditors.length; i++) {
+ if (!attrEditors[i].inplaceEditor) {
+ attrEditors[i].style.display = "none";
+ }
+ }
+
+ // Get the attribute editor for each attribute that exists on
+ // the node and show it.
+ for (let i = 0; i < attrs.length; i++) {
+ let attr = this._createAttribute(attrs[i]);
+ if (!attr.inplaceEditor) {
+ attr.style.removeProperty("display");
+ }
+ }
+ },
+
+ _createAttribute: function EE_createAttribute(aAttr, aBefore)
+ {
+ if (this.attrs.indexOf(aAttr.name) !== -1) {
+ var attr = this.attrs[aAttr.name];
+ var name = attr.querySelector(".attrname");
+ var val = attr.querySelector(".attrvalue");
+ } else {
+ // Create the template editor, which will save some variables here.
+ let data = {
+ attrName: aAttr.name,
+ };
+ this.template("attribute", data);
+ var {attr, inner, name, val} = data;
+
+ // Figure out where we should place the attribute.
+ let before = aBefore || null;
+ if (aAttr.name == "id") {
+ before = this.attrList.firstChild;
+ } else if (aAttr.name == "class") {
+ let idNode = this.attrs["id"];
+ before = idNode ? idNode.nextSibling : this.attrList.firstChild;
+ }
+ this.attrList.insertBefore(attr, before);
+
+ // Make the attribute editable.
+ editableField({
+ element: inner,
+ trigger: "dblclick",
+ stopOnReturn: true,
+ selectAll: false,
+ start: function EE_editAttribute_start(aEditor, aEvent) {
+ // If the editing was started inside the name or value areas,
+ // select accordingly.
+ if (aEvent && aEvent.target === name) {
+ aEditor.input.setSelectionRange(0, name.textContent.length);
+ } else if (aEvent && aEvent.target === val) {
+ let length = val.textContent.length;
+ let editorLength = aEditor.input.value.length;
+ let start = editorLength - (length + 1);
+ aEditor.input.setSelectionRange(start, start + length);
+ } else {
+ aEditor.input.select();
+ }
+ },
+ done: function EE_editAttribute_done(aVal, aCommit) {
+ if (!aCommit) {
+ return;
+ }
+
+ this.undo.startBatch();
+
+ // 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._removeAttribute(this.node, aAttr.name);
+ try {
+ this._applyAttributes(aVal, attr);
+ this.undo.endBatch();
+ } catch (e) {
+ this.undo.endBatch();
+ this.undo.undo();
+ }
+ }.bind(this)
+ });
+
+ this.attrs[aAttr.name] = attr;
+ }
+
+ name.textContent = aAttr.name;
+ val.textContent = aAttr.value;
+
+ return attr;
+ },
+
+ /**
+ * Parse a user-entered attribute string and apply the resulting
+ * attributes to the node. This operation is undoable.
+ *
+ * @param string aValue the user-entered value.
+ * @param Element aAttrNode the attribute editor that created this
+ * set of attributes, used to place new attributes where the
+ * user put them.
+ */
+ _applyAttributes: function EE__applyAttributes(aValue, aAttrNode)
+ {
+ let attrs = escapeAttributeValues(aValue);
+
+ this.undo.startBatch();
+
+ for (let attr of attrs) {
+ let attribute = {
+ name: attr.name,
+ value: attr.value
+ };
+ // Create an attribute editor next to the current attribute if needed.
+ this._createAttribute(attribute, aAttrNode ? aAttrNode.nextSibling : null);
+ this._setAttribute(this.node, attr.name, attr.value);
+ }
+
+ this.undo.endBatch();
+ },
+
+ /**
+ * Helper function for _setAttribute and _removeAttribute,
+ * returns a function that puts an attribute back the way it was.
+ */
+ _restoreAttribute: function EE_restoreAttribute(aNode, aName)
+ {
+ if (aNode.hasAttribute(aName)) {
+ let oldValue = aNode.getAttribute(aName);
+ return function() {
+ aNode.setAttribute(aName, oldValue);
+ this.markup.nodeChanged(aNode);
+ }.bind(this);
+ } else {
+ return function() {
+ aNode.removeAttribute(aName);
+ this.markup.nodeChanged(aNode);
+ }.bind(this);
+ }
+ },
+
+ /**
+ * Sets an attribute. This operation is undoable.
+ */
+ _setAttribute: function EE_setAttribute(aNode, aName, aValue)
+ {
+ this.undo.do(function() {
+ aNode.setAttribute(aName, aValue);
+ this.markup.nodeChanged(aNode);
+ }.bind(this), this._restoreAttribute(aNode, aName));
+ },
+
+ /**
+ * Removes an attribute. This operation is undoable.
+ */
+ _removeAttribute: function EE_removeAttribute(aNode, aName)
+ {
+ this.undo.do(function() {
+ aNode.removeAttribute(aName);
+ this.markup.nodeChanged(aNode);
+ }.bind(this), this._restoreAttribute(aNode, aName));
+ },
+
+ /**
+ * Handler for the new attribute editor.
+ */
+ _onNewAttribute: function EE_onNewAttribute(aValue, aCommit)
+ {
+ if (!aValue || !aCommit) {
+ return;
+ }
+
+ this._setAttribute(this.node, aValue, "");
+ let attr = this._createAttribute({ name: aValue, value: ""});
+ attr.style.removeAttribute("display");
+ attr.querySelector("attrvalue").click();
+ },
+
+
+ /**
+ * Called when the tag name editor has is done editing.
+ */
+ onTagEdit: function EE_onTagEdit(aVal, aCommit) {
+ if (!aCommit || aVal == this.node.tagName) {
+ return;
+ }
+
+ // Create a new element with the same attributes as the
+ // current element and prepare to replace the current node
+ // with it.
+ try {
+ var newElt = nodeDocument(this.node).createElement(aVal);
+ } catch(x) {
+ // Failed to create a new element with that tag name, ignore
+ // the change.
+ return;
+ }
+
+ let attrs = this.node.attributes;
+
+ for (let i = 0 ; i < attrs.length; i++) {
+ newElt.setAttribute(attrs[i].name, attrs[i].value);
+ }
+
+ function swapNodes(aOld, aNew) {
+ while (aOld.firstChild) {
+ aNew.appendChild(aOld.firstChild);
+ }
+ aOld.parentNode.insertBefore(aNew, aOld);
+ aOld.parentNode.removeChild(aOld);
+ }
+
+ let markup = this.container.markup;
+
+ // Queue an action to swap out the element.
+ this.undo.do(function() {
+ swapNodes(this.node, newElt);
+
+ // Make sure the new node is imported and is expanded/selected
+ // the same as the current node.
+ let newContainer = markup.importNode(newElt, this.container.expanded);
+ newContainer.expanded = this.container.expanded;
+ if (this.container.selected) {
+ markup.navigate(newContainer);
+ }
+ }.bind(this), function() {
+ swapNodes(newElt, this.node);
+
+ let newContainer = markup._containers.get(newElt);
+ this.container.expanded = newContainer.expanded;
+ if (newContainer.selected) {
+ markup.navigate(this.container);
+ }
+ }.bind(this));
+ },
+}
+
+
+
+RootContainer.prototype = {
+ hasChildren: true,
+ expanded: true,
+ update: function RC_update() {}
+};
+
+function documentWalker(node) {
+ return new DocumentWalker(node, Ci.nsIDOMNodeFilter.SHOW_ALL, whitespaceTextFilter);
+}
+
+function nodeDocument(node) {
+ return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
+}
+
+/**
+ * Similar to a TreeWalker, except will dig in to iframes and it doesn't
+ * implement the good methods like previousNode and nextNode.
+ *
+ * See TreeWalker documentation for explanations of the methods.
+ */
+function DocumentWalker(aNode, aShow, aFilter)
+{
+ let doc = nodeDocument(aNode);
+ this.walker = doc.createTreeWalker(nodeDocument(aNode), aShow, aFilter);
+ this.walker.currentNode = aNode;
+ this.filter = aFilter;
+}
+
+DocumentWalker.prototype = {
+ get node() this.walker.node,
+ get whatToShow() this.walker.whatToShow,
+ get expandEntityReferences() this.walker.expandEntityReferences,
+ get currentNode() this.walker.currentNode,
+ set currentNode(aVal) this.walker.currentNode = aVal,
+
+ /**
+ * Called when the new node is in a different document than
+ * the current node, creates a new treewalker for the document we've
+ * run in to.
+ */
+ _reparentWalker: function DW_reparentWalker(aNewNode) {
+ if (!aNewNode) {
+ return null;
+ }
+ let doc = nodeDocument(aNewNode);
+ let walker = doc.createTreeWalker(doc,
+ this.whatToShow, this.filter, this.expandEntityReferences);
+ walker.currentNode = aNewNode;
+ this.walker = walker;
+ return aNewNode;
+ },
+
+ parentNode: function DW_parentNode()
+ {
+ let currentNode = this.walker.currentNode;
+ let parentNode = this.walker.parentNode();
+
+ if (!parentNode) {
+ if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
+ && currentNode.defaultView) {
+ let embeddingFrame = currentNode.defaultView.frameElement;
+ if (embeddingFrame) {
+ return this._reparentWalker(embeddingFrame);
+ }
+ }
+ return null;
+ }
+
+ return parentNode;
+ },
+
+ firstChild: function DW_firstChild()
+ {
+ let node = this.walker.currentNode;
+ if (!node)
+ return;
+ if (node.contentDocument) {
+ return this._reparentWalker(node.contentDocument);
+ } else if (node.getSVGDocument) {
+ return this._reparentWalker(node.getSVGDocument());
+ }
+ return this.walker.firstChild();
+ },
+
+ lastChild: function DW_lastChild()
+ {
+ let node = this.walker.currentNode;
+ if (!node)
+ return;
+ if (node.contentDocument) {
+ return this._reparentWalker(node.contentDocument);
+ } else if (node.getSVGDocument) {
+ return this._reparentWalker(node.getSVGDocument());
+ }
+ return this.walker.lastChild();
+ },
+
+ previousSibling: function DW_previousSibling() this.walker.previousSibling(),
+ nextSibling: function DW_nextSibling() this.walker.nextSibling(),
+
+ // XXX bug 785143: not doing previousNode or nextNode, which would sure be useful.
+};
+
+/**
+ * Properly escape attribute values.
+ *
+ * @param {String} attr
+ * The attributes for which the values are to be escaped.
+ * @return {Array}
+ * An array of attribute names and their escaped values.
+ */
+function escapeAttributeValues(attr) {
+ let name = null;
+ let value = null;
+ let result = "";
+ let attributes = [];
+
+ while(attr.length > 0) {
+ let match;
+ let dirty = false;
+
+ // Trim quotes and spaces from attr start
+ match = attr.match(/^["\s]+/);
+ if (match && match.length == 1) {
+ attr = attr.substr(match[0].length);
+ }
+
+ // Name
+ if (!dirty) {
+ match = attr.match(/^([\w-]+)="/);
+ if (match && match.length == 2) {
+ if (name) {
+ // We had a name without a value e.g. disabled. Let's set the value to "";
+ value = "";
+ } else {
+ name = match[1];
+ attr = attr.substr(match[0].length);
+ }
+ dirty = true;
+ }
+ }
+
+ // Value (in the case of multiple attributes)
+ if (!dirty) {
+ match = attr.match(/^(.+?)"\s+[\w-]+="/);
+ if (match && match.length > 1) {
+ value = typeof match[1] == "undefined" ? match[2] : match[1];
+ attr = attr.substr(value.length);
+ value = simpleEscape(value);
+ dirty = true;
+ }
+ }
+
+ // Final value
+ if (!dirty && attr.indexOf("=\"") == -1) {
+ // No more attributes, get the remaining value minus it's ending quote.
+ if (attr.charAt(attr.length - 1) == '"') {
+ attr = attr.substr(0, attr.length - 1);
+ }
+
+ if (!name) {
+ name = attr;
+ value = "";
+ } else {
+ value = simpleEscape(attr);
+ }
+ attr = "";
+ dirty = true;
+ }
+
+ if (name !== null && value !== null) {
+ attributes.push({name: name, value: value});
+ name = value = null;
+ }
+
+ if (!dirty) {
+ // This should never happen but we exit here if it does.
+ return attributes;
+ }
+ }
+ return attributes;
+}
+
+/**
+ * Escape basic html entities <, >, " and '.
+ * @param {String} value
+ * Value to escape.
+ * @return {String}
+ * Escaped value.
+ */
+function simpleEscape(value) {
+ return value.replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&apos;");
+}
+
+/**
+ * A tree walker filter for avoiding empty whitespace text nodes.
+ */
+function whitespaceTextFilter(aNode)
+{
+ if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
+ !/[^\s]/.exec(aNode.nodeValue)) {
+ return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+ } else {
+ return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+ }
+}
+
+loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
+ "chrome://browser/locale/devtools/inspector.properties"
+));
diff --git a/browser/devtools/markupview/markup-view.xhtml b/browser/devtools/markupview/markup-view.xhtml
new file mode 100644
index 000000000..b26671ce3
--- /dev/null
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -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 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://browser/content/devtools/markup-view.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+
+</head>
+<body class="theme-body devtools-monospace" role="application">
+ <div id="root"></div>
+ <div id="templates" style="display:none">
+ <ul>
+ <li id="template-container" save="${elt}" class="container"><span save="${expander}" class="theme-twisty expander"></span><span save="${codeBox}" class="codebox"><ul save="${children}" class="children"></ul></span></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-element" save="${elt}" class="editor"><span>&lt;</span><span save="${tag}" class="tagname theme-fg-color3"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span>&gt;</span>
+
+ <span id="template-attribute" save="${attr}" data-attr="${attrName}" class="attreditor" style="display:none"> <span class="editable" save="${inner}" tabindex="0"><span save="${name}" class="attrname theme-fg-color2"></span>=&quot;<span save="${val}" class="attrvalue theme-fg-color6"></span>&quot;</span></span>
+
+ <span id="template-text" save="${elt}" class="editor text">
+ <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
+ </span>
+
+ <span id="template-comment" save="${elt}" class="editor comment theme-comment">
+ <span>&lt;!--</span><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><span>--&gt;</span>
+ </span>
+
+ <span id="template-elementContentSummary" save="${summaryElt}" class="summary"> … </span>
+
+ <span id="template-elementClose" save="${closeElt}">&lt;/<span save="${closeTag}" class="tagname theme-fg-color3"></span>&gt;</span>
+ </div>
+ <div id="previewbar" class="disabled">
+ <div id="preview"/>
+ <div id="viewbox"/>
+ </div>
+</body>
+</html>
diff --git a/browser/devtools/markupview/moz.build b/browser/devtools/markupview/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/markupview/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/markupview/test/Makefile.in b/browser/devtools/markupview/test/Makefile.in
new file mode 100644
index 000000000..2c0218d80
--- /dev/null
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_inspector_markup_navigation.html \
+ browser_inspector_markup_navigation.js \
+ browser_inspector_markup_mutation.html \
+ browser_inspector_markup_mutation.js \
+ browser_inspector_markup_edit.html \
+ browser_inspector_markup_edit.js \
+ browser_inspector_markup_subset.html \
+ browser_inspector_markup_subset.js \
+ head.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_edit.html b/browser/devtools/markupview/test/browser_inspector_markup_edit.html
new file mode 100644
index 000000000..b48d3535c
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.html
@@ -0,0 +1,44 @@
+<!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">line17</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>
+ </body>
+</html>
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_edit.js b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
new file mode 100644
index 000000000..89af69914
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -0,0 +1,443 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that various editors work as expected. Also checks
+ * that the various changes are properly undoable and redoable.
+ * For each step in the test, we:
+ * - Run the setup for that test (if any)
+ * - Check that the node we're editing is as we expect
+ * - 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.
+ *
+ * This test mostly tries to verify that the editor makes changes to the
+ * underlying DOM, not that the UI updates - UI updates are based on
+ * underlying DOM changes, and the mutation tests should cover those cases.
+ */
+
+function test() {
+ let inspector;
+ let {
+ getInplaceEditorForSpan: inplaceEditor
+ } = devtools.require("devtools/shared/inplace-editor");
+
+ waitForExplicitFinish();
+
+ // Will hold the doc we're viewing
+ let doc;
+
+ // Holds the MarkupTool object we're testing.
+ let markup;
+
+ /**
+ * Edit a given editableField
+ */
+ function editField(aField, aValue)
+ {
+ aField.focus();
+ EventUtils.sendKey("return", inspector.panelWin);
+ let input = inplaceEditor(aField).input;
+ input.value = aValue;
+ EventUtils.sendKey("return", inspector.panelWin);
+ }
+
+ /**
+ * Check that the appropriate attributes are assigned to a node.
+ *
+ * @param {HTMLNode} aElement
+ * The node to check.
+ * @param {Object} aAttributes
+ * An object containing the arguments to check.
+ * e.g. {id="id1",class="someclass"}
+ *
+ * NOTE: When checking attribute values bare in mind that node.getAttribute()
+ * returns attribute values provided by the HTML parser. The parser only
+ * provides unescaped entities so &amp; will return &.
+ */
+ function assertAttributes(aElement, aAttributes)
+ {
+ let attrs = Object.getOwnPropertyNames(aAttributes);
+ is(aElement.attributes.length, attrs.length,
+ "Node has the correct number of attributes");
+ for (let attr of attrs) {
+ is(aElement.getAttribute(attr), aAttributes[attr],
+ "Node has the correct " + attr + " attribute.");
+ }
+ }
+
+ // All the mutation types we want to test.
+ let edits = [
+ {
+ desc: "Change an attribute",
+ before: function() {
+ assertAttributes(doc.querySelector("#node1"), {
+ id: "node1",
+ class: "node1"
+ });
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let editor = markup.getContainer(doc.querySelector("#node1")).editor;
+ let attr = editor.attrs["class"].querySelector(".editable");
+ editField(attr, 'class="changednode1"');
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node1"), {
+ id: "node1",
+ class: "changednode1"
+ });
+ }
+ },
+
+ {
+ desc: 'Try changing an attribute to a quote (") - this should result ' +
+ 'in it being set to an empty string',
+ before: function() {
+ assertAttributes(doc.querySelector("#node22"), {
+ id: "node22",
+ class: "unchanged"
+ });
+ },
+ execute: function(after) {
+ let editor = markup.getContainer(doc.querySelector("#node22")).editor;
+ let attr = editor.attrs["class"].querySelector(".editable");
+ editField(attr, 'class="""');
+ executeSoon(after);
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node22"), {
+ id: "node22",
+ class: ""
+ });
+ }
+ },
+
+ {
+ desc: "Remove an attribute",
+ before: function() {
+ assertAttributes(doc.querySelector("#node4"), {
+ id: "node4",
+ class: "node4"
+ });
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let editor = markup.getContainer(doc.querySelector("#node4")).editor;
+ let attr = editor.attrs["class"].querySelector(".editable");
+ editField(attr, '');
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node4"), {
+ id: "node4",
+ });
+ }
+ },
+
+ {
+ desc: "Add an attribute by clicking the empty space after a node",
+ before: function() {
+ assertAttributes(doc.querySelector("#node14"), {
+ id: "node14",
+ });
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let editor = markup.getContainer(doc.querySelector("#node14")).editor;
+ let attr = editor.newAttr;
+ editField(attr, 'class="newclass" style="color:green"');
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node14"), {
+ id: "node14",
+ 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',
+ before: function() {
+ assertAttributes(doc.querySelector("#node23"), {
+ id: "node23",
+ });
+ },
+ execute: function(after) {
+ let editor = markup.getContainer(doc.querySelector("#node23")).editor;
+ let attr = editor.newAttr;
+ editField(attr, 'class="newclass" style="""');
+ executeSoon(after);
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node23"), {
+ id: "node23",
+ class: "newclass",
+ style: ""
+ });
+ }
+ },
+
+ {
+ desc: "Try add attributes by adding to an existing attribute's entry",
+ before: function() {
+ assertAttributes(doc.querySelector("#node24"), {
+ id: "node24",
+ });
+ },
+ execute: function(after) {
+ let editor = markup.getContainer(doc.querySelector("#node24")).editor;
+ let attr = editor.attrs["id"].querySelector(".editable");
+ editField(attr, attr.textContent + ' class="""');
+ executeSoon(after);
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node24"), {
+ id: "node24",
+ class: ""
+ });
+ }
+ },
+
+ {
+ desc: "Edit text",
+ before: function() {
+ let node = doc.querySelector('.node6').firstChild;
+ is(node.nodeValue, "line6", "Text should be unchanged");
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let node = doc.querySelector('.node6').firstChild;
+ let editor = markup.getContainer(node).editor;
+ let field = editor.elt.querySelector("pre");
+ editField(field, "New text");
+ },
+ after: function() {
+ let node = doc.querySelector('.node6').firstChild;
+ is(node.nodeValue, "New text", "Text should be changed.");
+ },
+ },
+
+ {
+ desc: "Add an attribute value containing < > &uuml; \" & '",
+ before: function() {
+ assertAttributes(doc.querySelector("#node25"), {
+ id: "node25",
+ });
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let editor = markup.getContainer(doc.querySelector("#node25")).editor;
+ let attr = editor.newAttr;
+ editField(attr, 'src="somefile.html?param1=<a>&param2=&uuml;"bl\'ah"');
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node25"), {
+ id: "node25",
+ src: "somefile.html?param1=&lt;a&gt;&param2=&uuml;&quot;bl&apos;ah"
+ });
+ }
+ },
+ ];
+
+ // Create the helper tab for parsing...
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+ content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
+
+ function setupTest() {
+ var target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ inspector = toolbox.getCurrentPanel();
+ startTests();
+ });
+ }
+
+ function startTests() {
+ let startNode = doc.documentElement.cloneNode();
+ markup = inspector.markup;
+ markup.expandAll();
+
+ let cursor = 0;
+
+ function nextEditTest() {
+ executeSoon(function() {
+ if (cursor >= edits.length) {
+ addAttributes();
+ } else {
+ let step = edits[cursor++];
+ info("START " + step.desc);
+ if (step.setup) {
+ step.setup();
+ }
+ step.before();
+ info("before execute");
+ step.execute(function() {
+ info("after execute");
+ step.after();
+ ok(markup.undo.canUndo(), "Should be able to undo.");
+ markup.undo.undo();
+ step.before();
+ ok(markup.undo.canRedo(), "Should be able to redo.");
+ markup.undo.redo();
+ step.after();
+ info("END " + step.desc);
+ nextEditTest();
+ });
+ }
+ });
+ }
+ nextEditTest();
+ }
+
+ function addAttributes() {
+ let test = {
+ desc: "Add attributes by adding to an existing attribute's entry",
+ setup: function() {
+ inspector.selection.setNode(doc.querySelector("#node18"));
+ },
+ before: function() {
+ assertAttributes(doc.querySelector("#node18"), {
+ id: "node18",
+ });
+
+ is(inspector.highlighter.nodeInfo.classesBox.textContent, "",
+ "No classes in the infobar before edit.");
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", function() {
+ // needed because we need to make sure the infobar is updated
+ // not just the markupview (which happens in this event loop)
+ executeSoon(after);
+ });
+ let editor = markup.getContainer(doc.querySelector("#node18")).editor;
+ let attr = editor.attrs["id"].querySelector(".editable");
+ editField(attr, attr.textContent + ' class="newclass" style="color:green"');
+ },
+ after: function() {
+ assertAttributes(doc.querySelector("#node18"), {
+ id: "node18",
+ class: "newclass",
+ style: "color:green"
+ });
+ is(inspector.highlighter.nodeInfo.classesBox.textContent, ".newclass",
+ "Correct classes in the infobar after edit.");
+ }
+ };
+ testAsyncSetup(test, editTagName);
+ }
+
+ function editTagName() {
+ let test = {
+ desc: "Edit the tag name",
+ setup: function() {
+ inspector.selection.setNode(doc.querySelector("#retag-me"));
+ },
+ before: function() {
+ let node = doc.querySelector("#retag-me");
+ let container = markup.getContainer(node);
+
+ is(node.tagName, "DIV", "retag-me should be a div.");
+ ok(container.selected, "retag-me should be selected.");
+ ok(container.expanded, "retag-me should be expanded.");
+ is(doc.querySelector("#retag-me-2").parentNode, node,
+ "retag-me-2 should be a child of the old element.");
+ },
+ execute: function(after) {
+ inspector.once("markupmutation", after);
+ let node = doc.querySelector("#retag-me");
+ let editor = markup.getContainer(node).editor;
+ let field = editor.tag;
+ editField(field, "p");
+ },
+ after: function() {
+ let node = doc.querySelector("#retag-me");
+ let container = markup.getContainer(node);
+ is(node.tagName, "P", "retag-me should be a p.");
+ ok(container.selected, "retag-me should be selected.");
+ ok(container.expanded, "retag-me should be expanded.");
+ is(doc.querySelector("#retag-me-2").parentNode, node,
+ "retag-me-2 should be a child of the new element.");
+ }
+ };
+ testAsyncSetup(test, removeElementWithDelete);
+ }
+
+ function removeElementWithDelete() {
+ let test = {
+ desc: "Remove an element with the delete key",
+ before: function() {
+ ok(!!doc.querySelector("#node18"), "Node 18 should exist.");
+ },
+ execute: function() {
+ inspector.selection.setNode(doc.querySelector("#node18"));
+ },
+ executeCont: function() {
+ EventUtils.sendKey("delete", inspector.panelWin);
+ },
+ after: function() {
+ ok(!doc.querySelector("#node18"), "Node 18 should not exist.")
+ }
+ };
+ testAsyncExecute(test, finishUp);
+ }
+
+ function testAsyncExecute(test, callback) {
+ info("START " + test.desc);
+
+ test.before();
+ inspector.selection.once("new-node", function BIMET_testAsyncExecNewNode() {
+ test.executeCont();
+ test.after();
+ undoRedo(test, callback);
+ });
+ executeSoon(function BIMET_setNode1() {
+ test.execute();
+ });
+ }
+
+ function testAsyncSetup(test, callback) {
+ info("START " + test.desc);
+
+ inspector.selection.once("new-node", function BIMET_testAsyncSetupNewNode() {
+ test.before();
+ test.execute(function() {
+ test.after();
+ undoRedo(test, callback);
+ });
+ });
+ executeSoon(function BIMET_setNode2() {
+ test.setup();
+ });
+ }
+
+ function undoRedo(test, callback) {
+ ok(markup.undo.canUndo(), "Should be able to undo.");
+ markup.undo.undo();
+ executeSoon(function() {
+ test.before();
+ ok(markup.undo.canRedo(), "Should be able to redo.");
+ markup.undo.redo();
+ executeSoon(function() {
+ test.after();
+ info("END " + test.desc);
+ callback();
+ });
+ });
+ }
+
+ function finishUp() {
+ while (markup.undo.canUndo()) {
+ markup.undo.undo();
+ }
+ doc = inspector = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_mutation.html b/browser/devtools/markupview/test/browser_inspector_markup_mutation.html
new file mode 100644
index 000000000..65895a26c
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_mutation.html
@@ -0,0 +1,37 @@
+<!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">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/browser/devtools/markupview/test/browser_inspector_markup_mutation.js b/browser/devtools/markupview/test/browser_inspector_markup_mutation.js
new file mode 100644
index 000000000..0923262ce
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_mutation.js
@@ -0,0 +1,181 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that various mutations to the dom update the markup tool correctly.
+ * The test for comparing the markup tool to the real dom is a bit weird:
+ * - Select the text in the markup tool
+ * - Parse that as innerHTML in a document we've created for the purpose.
+ * - Remove extraneous whitespace in that tree
+ * - Compare it to the real dom with isEqualNode.
+ */
+
+function test() {
+ waitForExplicitFinish();
+
+ // Will hold the doc we're viewing
+ let contentTab;
+ let doc;
+
+ // Holds the MarkupTool object we're testing.
+ let markup;
+
+ // Holds the document we use to help re-parse the markup tool's output.
+ let parseTab;
+ let parseDoc;
+
+ let inspector;
+
+ // Strip whitespace from a node and its children.
+ function stripWhitespace(node)
+ {
+ node.normalize();
+ let iter = node.ownerDocument.createNodeIterator(node, NodeFilter.SHOW_TEXT + NodeFilter.SHOW_COMMENT,
+ null);
+
+ while ((node = iter.nextNode())) {
+ node.nodeValue = node.nodeValue.replace(/\s+/g, '');
+ if (node.nodeType == Node.TEXT_NODE &&
+ !/[^\s]/.exec(node.nodeValue)) {
+ node.parentNode.removeChild(node);
+ }
+ }
+ }
+
+ // Verify that the markup in the tool is the same as the markup in the document.
+ function checkMarkup()
+ {
+ markup.expandAll();
+
+ let contentNode = doc.querySelector("body");
+ let panelNode = markup._containers.get(contentNode).elt;
+ let parseNode = parseDoc.querySelector("body");
+
+ // Grab the text from the markup panel...
+ let sel = panelNode.ownerDocument.defaultView.getSelection();
+ sel.selectAllChildren(panelNode);
+
+ // Parse it
+ parseNode.outerHTML = sel;
+ parseNode = parseDoc.querySelector("body");
+
+ // Pull whitespace out of text and comment nodes, there will
+ // be minor unimportant differences.
+ stripWhitespace(parseNode);
+
+ ok(contentNode.isEqualNode(parseNode), "Markup panel should match document.");
+ }
+
+ // All the mutation types we want to test.
+ let mutations = [
+ // Add an attribute
+ function() {
+ let node1 = doc.querySelector("#node1");
+ node1.setAttribute("newattr", "newattrval");
+ },
+ function() {
+ let node1 = doc.querySelector("#node1");
+ node1.removeAttribute("newattr");
+ },
+ function() {
+ let node1 = doc.querySelector("#node1");
+ node1.textContent = "newtext";
+ },
+ function() {
+ let node2 = doc.querySelector("#node2");
+ node2.innerHTML = "<div><span>foo</span></div>";
+ },
+
+ function() {
+ let node4 = doc.querySelector("#node4");
+ while (node4.firstChild) {
+ node4.removeChild(node4.firstChild);
+ }
+ },
+ function() {
+ // Move a child to a new parent.
+ let node17 = doc.querySelector("#node17");
+ let node1 = doc.querySelector("#node2");
+ node1.appendChild(node17);
+ },
+
+ function() {
+ // Swap 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
+ let node18 = doc.querySelector("#node18");
+ let node20 = doc.querySelector("#node20");
+
+ let node1 = doc.querySelector("#node1");
+
+ node1.appendChild(node20);
+ node20.appendChild(node18);
+ },
+ ];
+
+ // Create the helper tab for parsing...
+ parseTab = gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ parseDoc = content.document;
+
+ // Then create the actual dom we're inspecting...
+ contentTab = gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload2() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload2, true);
+ doc = content.document;
+ // Strip whitespace from the doc for easier comparison.
+ stripWhitespace(doc.documentElement);
+ waitForFocus(setupTest, content);
+ }, true);
+ content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_mutation.html";
+ }, true);
+
+ content.location = "data:text/html,<html></html>";
+
+ function setupTest() {
+ var target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ inspector = toolbox.getCurrentPanel();
+ startTests();
+ });
+ }
+
+ function startTests() {
+ markup = inspector.markup;
+ checkMarkup();
+ nextStep(0);
+ }
+
+ function nextStep(cursor) {
+ if (cursor >= mutations.length) {
+ finishUp();
+ return;
+ }
+ mutations[cursor]();
+ inspector.once("markupmutation", function() {
+ executeSoon(function() {
+ checkMarkup();
+ nextStep(cursor + 1);
+ });
+ });
+ }
+
+ function finishUp() {
+ doc = inspector = null;
+ gBrowser.removeTab(contentTab);
+ gBrowser.removeTab(parseTab);
+ finish();
+ }
+}
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_navigation.html b/browser/devtools/markupview/test/browser_inspector_markup_navigation.html
new file mode 100644
index 000000000..9633052e1
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_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/browser/devtools/markupview/test/browser_inspector_markup_navigation.js b/browser/devtools/markupview/test/browser_inspector_markup_navigation.js
new file mode 100644
index 000000000..e36d3aa60
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_navigation.js
@@ -0,0 +1,151 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+ let inspector;
+
+ waitForExplicitFinish();
+
+ let doc;
+
+ let keySequences = [
+ ["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", "node6"],
+ ["down", "*comment*"],
+ ["down" , "node7"],
+ ["right", "node7"],
+ ["down", "*text*"],
+ ["down", "node8"],
+ ["left", "node7"],
+ ["left", "node7"],
+ ["right", "node7"],
+ ["right", "*text*"],
+ ["right", "*text*"],
+ ["down", "node8"],
+ ["right", "node8"],
+ ["left", "node8"],
+ ["down", "node9"],
+ ["down", "node10"],
+ ["down", "node11"],
+ ["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", "node6"],
+ ["down", "*comment*"],
+ ["down", "node7"],
+ ["left", "node7"],
+ ["down", "node9"],
+ ["down", "node10"],
+ ["pageup", "node2"],
+ ["pageup", "*doctype*"],
+ ["down", "html"],
+ ["left", "html"],
+ ["down", "html"]
+ ];
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+
+ content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_navigation.html";
+
+ function setupTest() {
+ var target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ inspector = toolbox.getCurrentPanel();
+ startNavigation();
+ });
+ }
+
+ function startNavigation() {
+ nextStep(0);
+ }
+
+ function nextStep(cursor) {
+ if (cursor >= keySequences.length) {
+ finishUp();
+ return;
+ }
+
+ let key = keySequences[cursor][0];
+ let className = keySequences[cursor][1];
+ inspector.markup._frame.focus();
+
+ 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;
+ }
+
+ executeSoon(function BIMNT_newNode() {
+ let node = inspector.selection.node;
+
+ if (className == "*comment*") {
+ is(node.nodeType, Node.COMMENT_NODE, "[" + cursor + "] should be a comment after moving " + key);
+ } else if (className == "*text*") {
+ is(node.nodeType, Node.TEXT_NODE, "[" + cursor + "] should be text after moving " + key);
+ } else if (className == "*doctype*") {
+ is(node.nodeType, Node.DOCUMENT_TYPE_NODE, "[" + cursor + "] should be doctype after moving " + key);
+ } else {
+ is(node.className, className, "[" + cursor + "] right node selected: " + className + " after moving " + key);
+ }
+
+ nextStep(cursor + 1);
+ });
+ }
+
+ function finishUp() {
+ doc = inspector = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_subset.html b/browser/devtools/markupview/test/browser_inspector_markup_subset.html
new file mode 100644
index 000000000..8323f0b2e
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_subset.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/browser/devtools/markupview/test/browser_inspector_markup_subset.js b/browser/devtools/markupview/test/browser_inspector_markup_subset.js
new file mode 100644
index 000000000..5b802c812
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_subset.js
@@ -0,0 +1,146 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the markup view loads only as many nodes as specified
+ * by the devtools.markup.pagesize preference.
+ */
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("devtools.markup.pagesize");
+});
+Services.prefs.setIntPref("devtools.markup.pagesize", 5);
+
+
+function test() {
+ waitForExplicitFinish();
+
+ // Will hold the doc we're viewing
+ let doc;
+
+ let inspector;
+
+ // Holds the MarkupTool object we're testing.
+ let markup;
+
+ function assertChildren(expected)
+ {
+ let container = markup.getContainer(doc.querySelector("body"));
+ 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(expected, found, "Got the expected children.");
+ }
+
+ function forceReload()
+ {
+ let container = markup.getContainer(doc.querySelector("body"));
+ container.childrenDirty = true;
+ }
+
+ let selections = [
+ {
+ desc: "Select the first item",
+ selector: "#a",
+ before: function() {
+ },
+ after: function() {
+ assertChildren("abcde*more*");
+ }
+ },
+ {
+ desc: "Select the last item",
+ selector: "#z",
+ before: function() {},
+ after: function() {
+ assertChildren("*more*vwxyz");
+ }
+ },
+ {
+ desc: "Select an already-visible item",
+ selector: "#v",
+ before: function() {},
+ after: function() {
+ // Because "v" was already visible, we shouldn't have loaded
+ // a different page.
+ assertChildren("*more*vwxyz");
+ },
+ },
+ {
+ desc: "Verify childrenDirty reloads the page",
+ selector: "#w",
+ before: function() {
+ forceReload();
+ },
+ after: function() {
+ // But now that we don't already have a loaded page, selecting
+ // w should center around w.
+ assertChildren("*more*uvwxy*more*");
+ },
+ },
+ ];
+
+ // Create the helper tab for parsing...
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ doc = content.document;
+ waitForFocus(setupTest, content);
+ }, true);
+ content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_subset.html";
+
+ function setupTest() {
+ var target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ toolbox.once("inspector-selected", function SE_selected(id, aInspector) {
+ inspector = aInspector;
+ markup = inspector.markup;
+ runNextSelection();
+ });
+ });
+ }
+
+ function runTests() {
+ inspector.selection.once("new-node", startTests);
+ executeSoon(function() {
+ inspector.selection.setNode(doc.body);
+ });
+ }
+
+ function runNextSelection() {
+ let selection = selections.shift();
+ if (!selection) {
+ clickMore();
+ return;
+ }
+
+ info(selection.desc);
+ selection.before();
+ inspector.selection.once("new-node", function() {
+ selection.after();
+ runNextSelection();
+ });
+ inspector.selection.setNode(doc.querySelector(selection.selector));
+ }
+
+ function clickMore() {
+ info("Check that clicking more loads the whole thing.");
+ // Make sure that clicking the "more" button loads all the nodes.
+ let container = markup.getContainer(doc.querySelector("body"));
+ let button = container.elt.querySelector("button");
+ button.click();
+ assertChildren("abcdefghijklmnopqrstuvwxyz");
+ finishUp();
+ }
+
+ function finishUp() {
+ doc = inspector = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/markupview/test/head.js b/browser/devtools/markupview/test/head.js
new file mode 100644
index 000000000..c1e1804bf
--- /dev/null
+++ b/browser/devtools/markupview/test/head.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/. */
+
+const Cu = Components.utils;
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+// Clear preferences that may be set during the course of tests.
+function clearUserPrefs()
+{
+ Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
+ Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+ Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
+}
+
+registerCleanupFunction(clearUserPrefs);
diff --git a/browser/devtools/markupview/test/moz.build b/browser/devtools/markupview/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/markupview/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/moz.build b/browser/devtools/moz.build
new file mode 100644
index 000000000..9cec3f5b0
--- /dev/null
+++ b/browser/devtools/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; c-basic-offset: 4; 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 += [
+ 'inspector',
+ 'markupview',
+ 'webconsole',
+ 'commandline',
+ 'sourceeditor',
+ 'styleeditor',
+ 'styleinspector',
+ 'tilt',
+ 'scratchpad',
+ 'debugger',
+ 'netmonitor',
+ 'layoutview',
+ 'shared',
+ 'responsivedesign',
+ 'framework',
+ 'profiler',
+ 'fontinspector',
+
+]
diff --git a/browser/devtools/netmonitor/Makefile.in b/browser/devtools/netmonitor/Makefile.in
new file mode 100644
index 000000000..42f17d7fe
--- /dev/null
+++ b/browser/devtools/netmonitor/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/netmonitor/NetMonitorPanel.jsm b/browser/devtools/netmonitor/NetMonitorPanel.jsm
new file mode 100644
index 000000000..4f778bae1
--- /dev/null
+++ b/browser/devtools/netmonitor/NetMonitorPanel.jsm
@@ -0,0 +1,66 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+this.EXPORTED_SYMBOLS = ["NetMonitorPanel"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+this.NetMonitorPanel = function NetMonitorPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ this._view = this.panelWin.NetMonitorView;
+ this._controller = this.panelWin.NetMonitorController;
+ this._controller._target = this.target;
+
+ EventEmitter.decorate(this);
+}
+
+NetMonitorPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A Promise that is resolved when the NetMonitor completes opening.
+ */
+ open: function() {
+ let promise;
+
+ // Local monitoring needs to make the target remote.
+ if (!this.target.isRemote) {
+ promise = this.target.makeRemote();
+ } else {
+ promise = Promise.resolve(this.target);
+ }
+
+ return promise
+ .then(() => this._controller.startupNetMonitor())
+ .then(() => this._controller.connect())
+ .then(() => {
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ })
+ .then(null, function onError(aReason) {
+ Cu.reportError("NetMonitorPanel open failed. " +
+ reason.error + ": " + reason.message);
+ });
+ },
+
+ // DevToolPanel API
+ get target() this._toolbox.target,
+
+ destroy: function() {
+ this._controller.shutdownNetMonitor().then(() => this.emit("destroyed"));
+ }
+};
diff --git a/browser/devtools/netmonitor/moz.build b/browser/devtools/netmonitor/moz.build
new file mode 100644
index 000000000..493f80dc6
--- /dev/null
+++ b/browser/devtools/netmonitor/moz.build
@@ -0,0 +1,6 @@
+# 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/netmonitor/netmonitor-controller.js b/browser/devtools/netmonitor/netmonitor-controller.js
new file mode 100644
index 000000000..72e7a8a70
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -0,0 +1,571 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
+const LISTENERS = [ "NetworkActivity" ];
+const NET_PREFS = { "NetworkMonitor.saveRequestAndResponseBodies": true };
+
+/**
+ * Object defining the network monitor controller components.
+ */
+let NetMonitorController = {
+ /**
+ * Initializes the view.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes startup.
+ */
+ startupNetMonitor: function() {
+ if (this._isInitialized) {
+ return this._startup.promise;
+ }
+ this._isInitialized = true;
+
+ let deferred = this._startup = Promise.defer();
+
+ NetMonitorView.initialize(() => {
+ NetMonitorView._isInitialized = true;
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Destroys the view and disconnects the monitor client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes shutdown.
+ */
+ shutdownNetMonitor: function() {
+ if (this._isDestroyed) {
+ return this._shutdown.promise;
+ }
+ this._isDestroyed = true;
+ this._startup = null;
+
+ let deferred = this._shutdown = Promise.defer();
+
+ NetMonitorView.destroy(() => {
+ NetMonitorView._isDestroyed = true;
+ this.TargetEventsHandler.disconnect();
+ this.NetworkEventsHandler.disconnect();
+
+ this.disconnect();
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Initiates remote or chrome network monitoring based on the current target,
+ * wiring event handlers as necessary.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes connecting.
+ */
+ connect: function() {
+ if (this._connection) {
+ return this._connection.promise;
+ }
+
+ let deferred = this._connection = Promise.defer();
+
+ let target = this._target;
+ let { client, form } = target;
+ if (target.chrome) {
+ this._startChromeMonitoring(client, form.consoleActor, deferred.resolve);
+ } else {
+ this._startMonitoringTab(client, form, deferred.resolve);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Disconnects the debugger client and removes event handlers as necessary.
+ */
+ disconnect: function() {
+ // When debugging local or a remote instance, the connection is closed by
+ // the RemoteTarget.
+ this._connection = null;
+ this.client = null;
+ this.tabClient = null;
+ this.webConsoleClient = null;
+ },
+
+ /**
+ * Sets up a monitoring session.
+ *
+ * @param DebuggerClient aClient
+ * The debugger client.
+ * @param object aTabGrip
+ * The remote protocol grip of the tab.
+ * @param function aCallback
+ * A function to invoke once the client attached to the console client.
+ */
+ _startMonitoringTab: function(aClient, aTabGrip, aCallback) {
+ if (!aClient) {
+ Cu.reportError("No client found!");
+ return;
+ }
+ this.client = aClient;
+
+ aClient.attachTab(aTabGrip.actor, (aResponse, aTabClient) => {
+ if (!aTabClient) {
+ Cu.reportError("No tab client found!");
+ return;
+ }
+ this.tabClient = aTabClient;
+
+ aClient.attachConsole(aTabGrip.consoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
+ if (!aWebConsoleClient) {
+ Cu.reportError("Couldn't attach to console: " + aResponse.error);
+ return;
+ }
+ this.webConsoleClient = aWebConsoleClient;
+ this.webConsoleClient.setPreferences(NET_PREFS, () => {
+ this.TargetEventsHandler.connect();
+ this.NetworkEventsHandler.connect();
+
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ });
+ });
+ },
+
+ /**
+ * Sets up a chrome monitoring session.
+ *
+ * @param DebuggerClient aClient
+ * The debugger client.
+ * @param object aConsoleActor
+ * The remote protocol grip of the chrome debugger.
+ * @param function aCallback
+ * A function to invoke once the client attached to the console client.
+ */
+ _startChromeMonitoring: function(aClient, aConsoleActor, aCallback) {
+ if (!aClient) {
+ Cu.reportError("No client found!");
+ return;
+ }
+ this.client = aClient;
+
+ aClient.attachConsole(aConsoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
+ if (!aWebConsoleClient) {
+ Cu.reportError("Couldn't attach to console: " + aResponse.error);
+ return;
+ }
+ this.webConsoleClient = aWebConsoleClient;
+ this.webConsoleClient.setPreferences(NET_PREFS, () => {
+ this.TargetEventsHandler.connect();
+ this.NetworkEventsHandler.connect();
+
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ });
+ },
+
+ _isInitialized: false,
+ _isDestroyed: false,
+ _startup: null,
+ _shutdown: null,
+ _connection: null,
+ client: null,
+ tabClient: null,
+ webConsoleClient: null
+};
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+function TargetEventsHandler() {
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+}
+
+TargetEventsHandler.prototype = {
+ get target() NetMonitorController._target,
+ get webConsoleClient() NetMonitorController.webConsoleClient,
+
+ /**
+ * 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 aType
+ * Packet type.
+ * @param object aPacket
+ * Packet received from the server.
+ */
+ _onTabNavigated: function(aType, aPacket) {
+ switch (aType) {
+ case "will-navigate": {
+ // Reset UI.
+ NetMonitorView.RequestsMenu.reset();
+ NetMonitorView.NetworkDetails.reset();
+
+ // Reset global helpers cache.
+ nsIURL.store.clear();
+ drain.store.clear();
+
+ window.emit("NetMonitor:TargetWillNavigate");
+ break;
+ }
+ case "navigate": {
+ window.emit("NetMonitor:TargetNavigate");
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when the monitored tab is closed.
+ */
+ _onTabDetached: function() {
+ NetMonitorController.shutdownNetMonitor();
+ }
+};
+
+/**
+ * Functions handling target network events.
+ */
+function NetworkEventsHandler() {
+ 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);
+}
+
+NetworkEventsHandler.prototype = {
+ get client() NetMonitorController._target.client,
+ get webConsoleClient() NetMonitorController.webConsoleClient,
+
+ /**
+ * Connect to the current target client.
+ */
+ connect: function() {
+ dumpn("NetworkEventsHandler is connecting...");
+ this.client.addListener("networkEvent", this._onNetworkEvent);
+ this.client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function() {
+ if (!this.client) {
+ return;
+ }
+ dumpn("NetworkEventsHandler is disconnecting...");
+ this.client.removeListener("networkEvent", this._onNetworkEvent);
+ this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
+ },
+
+ /**
+ * The "networkEvent" message type handler.
+ *
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEvent: function(aType, aPacket) {
+ let { actor, startedDateTime, method, url, isXHR } = aPacket.eventActor;
+ NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url, isXHR);
+
+ window.emit("NetMonitor:NetworkEvent");
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler.
+ *
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate: function(aType, aPacket) {
+ let actor = aPacket.from;
+
+ switch (aPacket.updateType) {
+ case "requestHeaders":
+ this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
+ window.emit("NetMonitor:NetworkEventUpdating:RequestHeaders");
+ break;
+ case "requestCookies":
+ this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
+ window.emit("NetMonitor:NetworkEventUpdating:RequestCookies");
+ break;
+ case "requestPostData":
+ this.webConsoleClient.getRequestPostData(actor, this._onRequestPostData);
+ window.emit("NetMonitor:NetworkEventUpdating:RequestPostData");
+ break;
+ case "responseHeaders":
+ this.webConsoleClient.getResponseHeaders(actor, this._onResponseHeaders);
+ window.emit("NetMonitor:NetworkEventUpdating:ResponseHeaders");
+ break;
+ case "responseCookies":
+ this.webConsoleClient.getResponseCookies(actor, this._onResponseCookies);
+ window.emit("NetMonitor:NetworkEventUpdating:ResponseCookies");
+ break;
+ case "responseStart":
+ NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+ httpVersion: aPacket.response.httpVersion,
+ status: aPacket.response.status,
+ statusText: aPacket.response.statusText,
+ headersSize: aPacket.response.headersSize
+ });
+ window.emit("NetMonitor:NetworkEventUpdating:ResponseStart");
+ break;
+ case "responseContent":
+ NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+ contentSize: aPacket.contentSize,
+ mimeType: aPacket.mimeType
+ });
+ this.webConsoleClient.getResponseContent(actor, this._onResponseContent);
+ window.emit("NetMonitor:NetworkEventUpdating:ResponseContent");
+ break;
+ case "eventTimings":
+ NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+ totalTime: aPacket.totalTime
+ });
+ this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
+ window.emit("NetMonitor:NetworkEventUpdating:EventTimings");
+ break;
+ }
+ },
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onRequestHeaders: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ requestHeaders: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:RequestHeaders");
+ },
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onRequestCookies: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ requestCookies: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:RequestCookies");
+ },
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onRequestPostData: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ requestPostData: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:RequestPostData");
+ },
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onResponseHeaders: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ responseHeaders: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:ResponseHeaders");
+ },
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onResponseCookies: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ responseCookies: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:ResponseCookies");
+ },
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onResponseContent: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ responseContent: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:ResponseContent");
+ },
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _onEventTimings: function(aResponse) {
+ NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+ eventTimings: aResponse
+ });
+ window.emit("NetMonitor:NetworkEventUpdated:EventTimings");
+ },
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param object | string aStringGrip
+ * 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(aStringGrip) {
+ // Make sure this is a long string.
+ if (typeof aStringGrip != "object" || aStringGrip.type != "longString") {
+ return Promise.resolve(aStringGrip); // Go home string, you're drunk.
+ }
+ // Fetch the long string only once.
+ if (aStringGrip._fullText) {
+ return aStringGrip._fullText.promise;
+ }
+
+ let deferred = aStringGrip._fullText = Promise.defer();
+ let { actor, initial, length } = aStringGrip;
+ let longStringClient = this.webConsoleClient.longString(aStringGrip);
+
+ longStringClient.substring(initial.length, length, (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError(aResponse.error + ": " + aResponse.message);
+ deferred.reject(aResponse);
+ return;
+ }
+ deferred.resolve(initial + aResponse.substring);
+ });
+
+ return deferred.promise;
+ }
+};
+
+/**
+ * Localization convenience methods.
+ */
+let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
+
+/**
+ * Shortcuts for accessing various network monitor preferences.
+ */
+let Prefs = new ViewHelpers.Prefs("devtools.netmonitor", {
+ networkDetailsWidth: ["Int", "panes-network-details-width"],
+ networkDetailsHeight: ["Int", "panes-network-details-height"]
+});
+
+/**
+ * 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() NetMonitorController.NetworkEventsHandler
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("NET-FRONTEND: " + str + "\n");
+ }
+}
+
+let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js
new file mode 100644
index 000000000..b8437dc6b
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -0,0 +1,1825 @@
+/* -*- 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+const EPSILON = 0.001;
+const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes
+const RESIZE_REFRESH_RATE = 50; // ms
+const REQUESTS_REFRESH_RATE = 50; // ms
+const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
+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; // byte
+const DEFAULT_HTTP_VERSION = "HTTP/1.1";
+const HEADERS_SIZE_DECIMALS = 3;
+const CONTENT_SIZE_DECIMALS = 2;
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+ "ecmascript": "js",
+ "javascript": "js",
+ "x-javascript": "js"
+};
+const CONTENT_MIME_TYPE_MAPPINGS = {
+ "/ecmascript": SourceEditor.MODES.JAVASCRIPT,
+ "/javascript": SourceEditor.MODES.JAVASCRIPT,
+ "/x-javascript": SourceEditor.MODES.JAVASCRIPT,
+ "/html": SourceEditor.MODES.HTML,
+ "/xhtml": SourceEditor.MODES.HTML,
+ "/xml": SourceEditor.MODES.HTML,
+ "/atom": SourceEditor.MODES.HTML,
+ "/soap": SourceEditor.MODES.HTML,
+ "/rdf": SourceEditor.MODES.HTML,
+ "/rss": SourceEditor.MODES.HTML,
+ "/css": SourceEditor.MODES.CSS
+};
+const DEFAULT_EDITOR_CONFIG = {
+ mode: SourceEditor.MODES.TEXT,
+ readOnly: true,
+ showLineNumbers: true
+};
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ lazyEmpty: true,
+ lazyEmptyDelay: 10, // ms
+ searchEnabled: true,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChage: true,
+ preventDescriptorModifiers: true,
+ eval: () => {},
+ switch: () => {}
+};
+
+/**
+ * Object defining the network monitor view components.
+ */
+let NetMonitorView = {
+ /**
+ * Initializes the network monitor view.
+ *
+ * @param function aCallback
+ * Called after the view finishes initializing.
+ */
+ initialize: function(aCallback) {
+ dumpn("Initializing the NetMonitorView");
+
+ this._initializePanes();
+
+ this.Toolbar.initialize();
+ this.RequestsMenu.initialize();
+ this.NetworkDetails.initialize();
+
+ aCallback();
+ },
+
+ /**
+ * Destroys the network monitor view.
+ *
+ * @param function aCallback
+ * Called after the view finishes destroying.
+ */
+ destroy: function(aCallback) {
+ dumpn("Destroying the NetMonitorView");
+
+ this.Toolbar.destroy();
+ this.RequestsMenu.destroy();
+ this.NetworkDetails.destroy();
+
+ this._destroyPanes();
+
+ aCallback();
+ },
+
+ /**
+ * Initializes the UI for all the displayed panes.
+ */
+ _initializePanes: function() {
+ dumpn("Initializing the NetMonitorView panes");
+
+ this._body = $("#body");
+ this._detailsPane = $("#details-pane");
+ this._detailsPaneToggleButton = $("#details-pane-toggle");
+
+ this._collapsePaneString = L10N.getStr("collapseDetailsPane");
+ this._expandPaneString = L10N.getStr("expandDetailsPane");
+
+ this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
+ this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
+ this.toggleDetailsPane({ visible: false });
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: function() {
+ dumpn("Destroying the NetMonitorView panes");
+
+ Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
+ Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
+
+ this._detailsPane = null;
+ this._detailsPaneToggleButton = null;
+ },
+
+ /**
+ * Gets the visibility state of the network details pane.
+ * @return boolean
+ */
+ get detailsPaneHidden()
+ this._detailsPane.hasAttribute("pane-collapsed"),
+
+ /**
+ * Sets the network details 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.
+ */
+ toggleDetailsPane: function(aFlags, aTabIndex) {
+ let pane = this._detailsPane;
+ let button = this._detailsPaneToggleButton;
+
+ ViewHelpers.togglePane(aFlags, pane);
+
+ if (aFlags.visible) {
+ this._body.removeAttribute("pane-collapsed");
+ button.removeAttribute("pane-collapsed");
+ button.setAttribute("tooltiptext", this._collapsePaneString);
+ } else {
+ this._body.setAttribute("pane-collapsed", "");
+ button.setAttribute("pane-collapsed", "");
+ button.setAttribute("tooltiptext", this._expandPaneString);
+ }
+
+ if (aTabIndex !== undefined) {
+ $("#details-pane").selectedIndex = aTabIndex;
+ }
+ },
+
+ /**
+ * Lazily initializes and returns a promise for a SourceEditor instance.
+ *
+ * @param string aId
+ * The id of the editor placeholder node.
+ * @return object
+ * A Promise that is resolved when the editor is available.
+ */
+ editor: function(aId) {
+ dumpn("Getting a NetMonitorView editor: " + aId);
+
+ if (this._editorPromises.has(aId)) {
+ return this._editorPromises.get(aId);
+ }
+
+ let deferred = Promise.defer();
+ this._editorPromises.set(aId, deferred.promise);
+
+ // Initialize the source editor and store the newly created instance
+ // in the ether of a resolved promise's value.
+ new SourceEditor().init($(aId), DEFAULT_EDITOR_CONFIG, deferred.resolve);
+
+ return deferred.promise;
+ },
+
+ _body: null,
+ _detailsPane: null,
+ _detailsPaneToggleButton: null,
+ _collapsePaneString: "",
+ _expandPaneString: "",
+ _editorPromises: new Map(),
+ _isInitialized: false,
+ _isDestroyed: false
+};
+
+/**
+ * Functions handling the toolbar view: expand/collapse button etc.
+ */
+function ToolbarView() {
+ dumpn("ToolbarView was instantiated");
+
+ this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this);
+}
+
+ToolbarView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the ToolbarView");
+
+ this._detailsPaneToggleButton = $("#details-pane-toggle");
+ this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the ToolbarView");
+
+ this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false);
+ },
+
+ /**
+ * Listener handling the toggle button click event.
+ */
+ _onTogglePanesPressed: function() {
+ 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;
+ }
+ },
+
+ _detailsPaneToggleButton: null
+};
+
+/**
+ * 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._onSelect = this._onSelect.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._byFile = this._byFile.bind(this);
+ this._byDomain = this._byDomain.bind(this);
+ this._byType = this._byType.bind(this);
+}
+
+RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the RequestsMenuView");
+
+ this.widget = new SideMenuWidget($("#requests-menu-contents"), false);
+ this._summary = $("#request-menu-network-summary");
+
+ this.widget.maintainSelectionVisible = false;
+ this.widget.autoscrollWithAppendedItems = true;
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ window.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function() {
+ dumpn("Destroying the SourcesView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ window.removeEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Resets this container (removes all the networking information).
+ */
+ reset: function() {
+ this.empty();
+ this._firstRequestStartedMillis = -1;
+ this._lastRequestEndedMillis = -1;
+ },
+
+ /**
+ * Specifies if this view may be updated lazily.
+ */
+ lazyUpdate: true,
+
+ /**
+ * Adds a network request to this container.
+ *
+ * @param string aId
+ * An identifier coming from the network monitor controller.
+ * @param string aStartedDateTime
+ * 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 aMethod
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string aUrl
+ * Specifies the request's url.
+ * @param boolean aIsXHR
+ * True if this request was initiated via XHR.
+ */
+ addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) {
+ // Convert the received date/time string to a unix timestamp.
+ let unixTime = Date.parse(aStartedDateTime);
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(aMethod, aUrl);
+
+ // 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, aId], {
+ attachment: {
+ startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
+ startedMillis: unixTime,
+ method: aMethod,
+ url: aUrl,
+ isXHR: aIsXHR
+ }
+ });
+
+ $("#details-pane-toggle").disabled = false;
+ $("#requests-menu-empty-notice").hidden = true;
+
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Filters all network requests in this container by a specified type.
+ *
+ * @param string aType
+ * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
+ * or "flash".
+ */
+ filterOn: function(aType = "all") {
+ let target = $("#requests-menu-filter-" + aType + "-button");
+ let buttons = document.querySelectorAll(".requests-menu-footer-button");
+
+ for (let button of buttons) {
+ if (button != target) {
+ button.removeAttribute("checked");
+ } else {
+ button.setAttribute("checked", "true");
+ }
+ }
+
+ // Filter on whatever was requested.
+ switch (aType) {
+ case "all":
+ this.filterContents(() => true);
+ break;
+ case "html":
+ this.filterContents(this._onHtml);
+ break;
+ case "css":
+ this.filterContents(this._onCss);
+ break;
+ case "js":
+ this.filterContents(this._onJs);
+ break;
+ case "xhr":
+ this.filterContents(this._onXhr);
+ break;
+ case "fonts":
+ this.filterContents(this._onFonts);
+ break;
+ case "images":
+ this.filterContents(this._onImages);
+ break;
+ case "media":
+ this.filterContents(this._onMedia);
+ break;
+ case "flash":
+ this.filterContents(this._onFlash);
+ break;
+ }
+
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Sorts all network requests in this container by a specified detail.
+ *
+ * @param string aType
+ * Either "status", "method", "file", "domain", "type", "size" or
+ * "waterfall".
+ */
+ sortBy: function(aType = "waterfall") {
+ let target = $("#requests-menu-" + aType + "-button");
+ let headers = document.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ header.removeAttribute("sorted");
+ header.removeAttribute("tooltiptext");
+ }
+ }
+
+ 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"));
+ }
+ }
+
+ // Sort by whatever was requested.
+ switch (aType) {
+ case "status":
+ if (direction == "ascending") {
+ this.sortContents(this._byStatus);
+ } else {
+ this.sortContents((a, b) => !this._byStatus(a, b));
+ }
+ break;
+ case "method":
+ if (direction == "ascending") {
+ this.sortContents(this._byMethod);
+ } else {
+ this.sortContents((a, b) => !this._byMethod(a, b));
+ }
+ break;
+ case "file":
+ if (direction == "ascending") {
+ this.sortContents(this._byFile);
+ } else {
+ this.sortContents((a, b) => !this._byFile(a, b));
+ }
+ break;
+ case "domain":
+ if (direction == "ascending") {
+ this.sortContents(this._byDomain);
+ } else {
+ this.sortContents((a, b) => !this._byDomain(a, b));
+ }
+ break;
+ case "type":
+ if (direction == "ascending") {
+ this.sortContents(this._byType);
+ } else {
+ this.sortContents((a, b) => !this._byType(a, b));
+ }
+ break;
+ case "size":
+ if (direction == "ascending") {
+ this.sortContents(this._bySize);
+ } else {
+ this.sortContents((a, b) => !this._bySize(a, b));
+ }
+ break;
+ case "waterfall":
+ if (direction == "ascending") {
+ this.sortContents(this._byTiming);
+ } else {
+ this.sortContents((a, b) => !this._byTiming(a, b));
+ }
+ break;
+ }
+
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Predicates used when filtering items.
+ *
+ * @param object aItem
+ * The filtered item.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+ _onHtml: function({ attachment: { mimeType } })
+ mimeType && mimeType.contains("/html"),
+
+ _onCss: function({ attachment: { mimeType } })
+ mimeType && mimeType.contains("/css"),
+
+ _onJs: function({ attachment: { mimeType } })
+ mimeType && (
+ mimeType.contains("/ecmascript") ||
+ mimeType.contains("/javascript") ||
+ mimeType.contains("/x-javascript")),
+
+ _onXhr: function({ attachment: { isXHR } })
+ isXHR,
+
+ _onFonts: function({ attachment: { url, mimeType } }) // Fonts are a mess.
+ (mimeType && (
+ mimeType.contains("font/") ||
+ mimeType.contains("/font"))) ||
+ url.contains(".eot") ||
+ url.contains(".ttf") ||
+ url.contains(".otf") ||
+ url.contains(".woff"),
+
+ _onImages: function({ attachment: { mimeType } })
+ mimeType && mimeType.contains("image/"),
+
+ _onMedia: function({ attachment: { mimeType } }) // Not including images.
+ mimeType && (
+ mimeType.contains("audio/") ||
+ mimeType.contains("video/") ||
+ mimeType.contains("model/")),
+
+ _onFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
+ (mimeType && (
+ mimeType.contains("/x-flv") ||
+ mimeType.contains("/x-shockwave-flash"))) ||
+ url.contains(".swf") ||
+ url.contains(".flv"),
+
+ /**
+ * Predicates used when sorting items.
+ *
+ * @param object aFirst
+ * The first item used in the comparison.
+ * @param object aSecond
+ * The second item used in the comparison.
+ * @return number
+ * -1 to sort aFirst to a lower index than aSecond
+ * 0 to leave aFirst and aSecond unchanged with respect to each other
+ * 1 to sort aSecond to a lower index than aFirst
+ */
+ _byTiming: function({ attachment: first }, { attachment: second })
+ first.startedMillis > second.startedMillis,
+
+ _byStatus: function({ attachment: first }, { attachment: second })
+ first.status > second.status,
+
+ _byMethod: function({ attachment: first }, { attachment: second })
+ first.method > second.method,
+
+ _byFile: function({ attachment: first }, { attachment: second })
+ this._getUriNameWithQuery(first.url).toLowerCase() >
+ this._getUriNameWithQuery(second.url).toLowerCase(),
+
+ _byDomain: function({ attachment: first }, { attachment: second })
+ this._getUriHostPort(first.url).toLowerCase() >
+ this._getUriHostPort(second.url).toLowerCase(),
+
+ _byType: function({ attachment: first }, { attachment: second })
+ this._getAbbreviatedMimeType(first.mimeType).toLowerCase() >
+ this._getAbbreviatedMimeType(second.mimeType).toLowerCase(),
+
+ _bySize: function({ attachment: first }, { attachment: second })
+ first.contentSize > second.contentSize,
+
+ /**
+ * 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("value", 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("value", str
+ .replace("#1", visibleRequestsCount)
+ .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
+ .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
+ );
+ },
+
+ /**
+ * Adds odd/even attributes to all the visible items in this container.
+ */
+ refreshZebra: function() {
+ let visibleItems = this.orderedVisibleItems;
+
+ 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");
+ }
+ }
+ },
+
+ /**
+ * Schedules adding additional information to a network request.
+ *
+ * @param string aId
+ * An identifier coming from the network monitor controller.
+ * @param object aData
+ * An object containing several { key: value } tuples of network info.
+ * Supported keys are "httpVersion", "status", "statusText" etc.
+ */
+ updateRequest: function(aId, aData) {
+ // Prevent interference from zombie updates received after target closed.
+ if (NetMonitorView._isDestroyed) {
+ return;
+ }
+ this._updateQueue.push([aId, aData]);
+
+ // Lazy updating is disabled in some tests.
+ if (!this.lazyUpdate) {
+ return void this._flushRequests();
+ }
+ // Allow requests to settle down first.
+ drain("update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
+ },
+
+ /**
+ * Starts adding all queued additional information about network requests.
+ */
+ _flushRequests: function() {
+ // 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] 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 value = data[key];
+ if (value === undefined) {
+ // The information in the packet is empty, it can be safely ignored.
+ continue;
+ }
+
+ switch (key) {
+ case "requestHeaders":
+ requestItem.attachment.requestHeaders = value;
+ break;
+ case "requestCookies":
+ requestItem.attachment.requestCookies = value;
+ break;
+ case "requestPostData":
+ requestItem.attachment.requestPostData = value;
+ break;
+ case "responseHeaders":
+ requestItem.attachment.responseHeaders = value;
+ break;
+ case "responseCookies":
+ requestItem.attachment.responseCookies = value;
+ break;
+ case "httpVersion":
+ requestItem.attachment.httpVersion = value;
+ break;
+ case "status":
+ requestItem.attachment.status = value;
+ this._updateMenuView(requestItem, key, value);
+ break;
+ case "statusText":
+ requestItem.attachment.statusText = value;
+ this._updateMenuView(requestItem, key,
+ requestItem.attachment.status + " " +
+ requestItem.attachment.statusText);
+ break;
+ case "headersSize":
+ requestItem.attachment.headersSize = value;
+ break;
+ case "contentSize":
+ requestItem.attachment.contentSize = value;
+ this._updateMenuView(requestItem, key, value);
+ break;
+ case "mimeType":
+ requestItem.attachment.mimeType = value;
+ this._updateMenuView(requestItem, key, value);
+ break;
+ case "responseContent":
+ requestItem.attachment.responseContent = value;
+ break;
+ case "totalTime":
+ requestItem.attachment.totalTime = value;
+ requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
+ this._updateMenuView(requestItem, key, value);
+ this._registerLastRequestEnd(requestItem.attachment.endedMillis);
+ break;
+ case "eventTimings":
+ requestItem.attachment.eventTimings = value;
+ this._createWaterfallView(requestItem, value.timings);
+ break;
+ }
+ }
+ // This update may have additional information about a request which
+ // isn't shown yet in the network details pane.
+ let selectedItem = this.selectedItem;
+ if (selectedItem && selectedItem.value == id) {
+ NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
+ }
+ }
+
+ // We're done flushing all the requests, clear the update queue.
+ this._updateQueue = [];
+
+ // 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();
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aMethod
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string aUrl
+ * Specifies the request's url.
+ * @return nsIDOMNode
+ * The network request view.
+ */
+ _createMenuView: function(aMethod, aUrl) {
+ let uri = nsIURL(aUrl);
+ let nameWithQuery = this._getUriNameWithQuery(uri);
+ let hostPort = this._getUriHostPort(uri);
+
+ let template = $("#requests-menu-item-template");
+ let fragment = document.createDocumentFragment();
+
+ let method = $(".requests-menu-method", template);
+ method.setAttribute("value", aMethod);
+
+ let file = $(".requests-menu-file", template);
+ file.setAttribute("value", nameWithQuery);
+ file.setAttribute("tooltiptext", nameWithQuery);
+
+ let domain = $(".requests-menu-domain", template);
+ domain.setAttribute("value", hostPort);
+ domain.setAttribute("tooltiptext", hostPort);
+
+ let waterfall = $(".requests-menu-waterfall", template);
+ waterfall.style.backgroundImage = this._cachedWaterfallBackground;
+
+ // Flatten the DOM by removing one redundant box (the template container).
+ for (let node of template.childNodes) {
+ fragment.appendChild(node.cloneNode(true));
+ }
+
+ return fragment;
+ },
+
+ /**
+ * Updates the information displayed in a network request item view.
+ *
+ * @param object aItem
+ * The network request item in this container.
+ * @param string aKey
+ * The type of information that is to be updated.
+ * @param any aValue
+ * The new value to be shown.
+ */
+ _updateMenuView: function(aItem, aKey, aValue) {
+ switch (aKey) {
+ case "status": {
+ let node = $(".requests-menu-status", aItem.target);
+ node.setAttribute("code", aValue);
+ break;
+ }
+ case "statusText": {
+ let node = $(".requests-menu-status-and-method", aItem.target);
+ node.setAttribute("tooltiptext", aValue);
+ break;
+ }
+ case "contentSize": {
+ let kb = aValue / 1024;
+ let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+ let node = $(".requests-menu-size", aItem.target);
+ let text = L10N.getFormatStr("networkMenu.sizeKB", size);
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ case "mimeType": {
+ let type = this._getAbbreviatedMimeType(aValue);
+ let node = $(".requests-menu-type", aItem.target);
+ let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", aValue);
+ break;
+ }
+ case "totalTime": {
+ let node = $(".requests-menu-timings-total", aItem.target);
+ let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Creates a waterfall representing timing information in a network request item view.
+ *
+ * @param object aItem
+ * The network request item in this container.
+ * @param object aTimings
+ * An object containing timing information.
+ */
+ _createWaterfallView: function(aItem, aTimings) {
+ let { target, attachment } = aItem;
+ let sections = ["dns", "connect", "send", "wait", "receive"];
+ // Skipping "blocked" because it doesn't work yet.
+
+ let timingsNode = $(".requests-menu-timings", target);
+ let startCapNode = $(".requests-menu-timings-cap.start", timingsNode);
+ let endCapNode = $(".requests-menu-timings-cap.end", timingsNode);
+ let firstBox;
+
+ // Add a set of boxes representing timing information.
+ for (let key of sections) {
+ let width = aTimings[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, endCapNode);
+
+ // Make the start cap inherit the aspect of the first timing box.
+ if (!firstBox) {
+ firstBox = timingBox;
+ startCapNode.classList.add(key);
+ }
+ // Same goes for the end cap, inherit the aspect of the last timing box.
+ endCapNode.classList.add(key);
+ }
+ }
+
+ // Since at least one timing box should've been rendered, unhide the
+ // start and end timing cap nodes.
+ startCapNode.hidden = false;
+ endCapNode.hidden = false;
+
+ // Rescale all the waterfalls so that everything is visible at once.
+ this._flushWaterfallViews();
+ },
+
+ /**
+ * Rescales and redraws all the waterfall views in this container.
+ *
+ * @param boolean aReset
+ * True if this container's width was changed.
+ */
+ _flushWaterfallViews: function(aReset) {
+ // 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 (aReset) {
+ this._cachedWaterfallWidth = 0;
+ this._hideOverflowingColumns();
+ }
+
+ // 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);
+ this._flushWaterfallBackgrounds();
+
+ // Apply CSS transforms to each waterfall in this container totalTime
+ // accurately translate and resize as needed.
+ for (let { target, attachment } in this) {
+ let timingsNode = $(".requests-menu-timings", target);
+ let startCapNode = $(".requests-menu-timings-cap.start", target);
+ let endCapNode = $(".requests-menu-timings-cap.end", 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;
+ startCapNode.style.transform = revScaleX + " translateX(" + (direction * 0.5) + "px)";
+ endCapNode.style.transform = revScaleX + " translateX(" + (direction * -0.5) + "px)";
+ totalNode.style.transform = revScaleX;
+ }
+ },
+
+ /**
+ * Creates the labels displayed on the waterfall header in this container.
+ *
+ * @param number aScale
+ * The current waterfall scale.
+ */
+ _showWaterfallDivisionLabels: function(aScale) {
+ let container = $("#requests-menu-waterfall-button");
+ 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 = aScale * 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 divisionMS = (x / aScale).toFixed(0);
+ let translateX = "translateX(" + ((direction * x) | 0) + "px)";
+
+ let node = document.createElement("label");
+ let text = L10N.getFormatStr("networkMenu.divisionMS", divisionMS);
+ node.className = "plain requests-menu-timings-division";
+ node.style.transform = translateX;
+
+ node.setAttribute("value", text);
+ fragment.appendChild(node);
+ }
+ container.appendChild(fragment);
+ }
+ },
+
+ /**
+ * Creates the background displayed on each waterfall view in this container.
+ *
+ * @param number aScale
+ * The current waterfall scale.
+ */
+ _drawWaterfallBackground: function(aScale) {
+ 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;
+ let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
+
+ // Start over.
+ let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+ let pixelArray = imageData.data;
+
+ let buf = new ArrayBuffer(pixelArray.length);
+ let buf8 = new Uint8ClampedArray(buf);
+ let data32 = 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 = aScale * 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;
+ data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+ }
+ alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+ }
+ }
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+ this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")";
+ },
+
+ /**
+ * Reapplies the current waterfall background on all request items.
+ */
+ _flushWaterfallBackgrounds: function() {
+ for (let { target } in this) {
+ let waterfallNode = $(".requests-menu-waterfall", target);
+ waterfallNode.style.backgroundImage = this._cachedWaterfallBackground;
+ }
+ },
+
+ /**
+ * Hides the overflowing columns in the requests table.
+ */
+ _hideOverflowingColumns: function() {
+ if (window.isRTL) {
+ return;
+ }
+ let table = $("#network-table");
+ let toolbar = $("#requests-menu-toolbar");
+ let columns = [
+ ["#requests-menu-waterfall-header-box", "waterfall-overflows"],
+ ["#requests-menu-size-header-box", "size-overflows"],
+ ["#requests-menu-type-header-box", "type-overflows"],
+ ["#requests-menu-domain-header-box", "domain-overflows"]
+ ];
+
+ // Flush headers.
+ columns.forEach(([, attribute]) => table.removeAttribute(attribute));
+ let availableWidth = toolbar.getBoundingClientRect().width;
+
+ // Hide the columns.
+ columns.forEach(([id, attribute]) => {
+ let bounds = $(id).getBoundingClientRect();
+ if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) {
+ table.setAttribute(attribute, "");
+ }
+ });
+ },
+
+ /**
+ * The selection listener for this container.
+ */
+ _onSelect: function({ detail: item }) {
+ if (item) {
+ NetMonitorView.NetworkDetails.populate(item.attachment);
+ NetMonitorView.NetworkDetails.toggle(true);
+ } else {
+ NetMonitorView.NetworkDetails.toggle(false);
+ }
+ },
+
+ /**
+ * The resize listener for this container's window.
+ */
+ _onResize: function(e) {
+ // Allow requests to settle down first.
+ drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
+ },
+
+ /**
+ * Checks if the specified unix time is the first one to be known of,
+ * and saves it if so.
+ *
+ * @param number aUnixTime
+ * The milliseconds to check and save.
+ */
+ _registerFirstRequestStart: function(aUnixTime) {
+ if (this._firstRequestStartedMillis == -1) {
+ this._firstRequestStartedMillis = aUnixTime;
+ }
+ },
+
+ /**
+ * Checks if the specified unix time is the last one to be known of,
+ * and saves it if so.
+ *
+ * @param number aUnixTime
+ * The milliseconds to check and save.
+ */
+ _registerLastRequestEnd: function(aUnixTime) {
+ if (this._lastRequestEndedMillis < aUnixTime) {
+ this._lastRequestEndedMillis = aUnixTime;
+ }
+ },
+
+ /**
+ * Helpers for getting details about an nsIURL.
+ *
+ * @param nsIURL | string aUrl
+ * @return string
+ */
+ _getUriNameWithQuery: function(aUrl) {
+ if (!(aUrl instanceof Ci.nsIURL)) {
+ aUrl = nsIURL(aUrl);
+ }
+ let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/";
+ let query = NetworkHelper.convertToUnicode(unescape(aUrl.query));
+ return name + (query ? "?" + query : "");
+ },
+ _getUriHostPort: function(aUrl) {
+ if (!(aUrl instanceof Ci.nsIURL)) {
+ aUrl = nsIURL(aUrl);
+ }
+ return NetworkHelper.convertToUnicode(unescape(aUrl.hostPort));
+ },
+
+ /**
+ * Helper for getting an abbreviated string for a mime type.
+ *
+ * @param string aMimeType
+ * @return string
+ */
+ _getAbbreviatedMimeType: function(aMimeType) {
+ if (!aMimeType) {
+ return "";
+ }
+ return (aMimeType.split(";")[0].split("/")[1] || "").split("+")[0];
+ },
+
+ /**
+ * Gets the total number of bytes representing the cumulated content size of
+ * a set of requests. Returns 0 for an empty set.
+ *
+ * @param array aItemsArray
+ * @return number
+ */
+ _getTotalBytesOfRequests: function(aItemsArray) {
+ if (!aItemsArray.length) {
+ return 0;
+ }
+ return aItemsArray.reduce((prev, curr) => prev + curr.attachment.contentSize || 0, 0);
+ },
+
+ /**
+ * Gets the oldest (first performed) request in a set. Returns null for an
+ * empty set.
+ *
+ * @param array aItemsArray
+ * @return object
+ */
+ _getOldestRequest: function(aItemsArray) {
+ if (!aItemsArray.length) {
+ return null;
+ }
+ return aItemsArray.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 aItemsArray
+ * @return object
+ */
+ _getNewestRequest: function(aItemsArray) {
+ if (!aItemsArray.length) {
+ return null;
+ }
+ return aItemsArray.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;
+ },
+
+ _summary: null,
+ _canvas: null,
+ _ctx: null,
+ _cachedWaterfallWidth: 0,
+ _cachedWaterfallBackground: "",
+ _firstRequestStartedMillis: -1,
+ _lastRequestEndedMillis: -1,
+ _updateQueue: [],
+ _updateTimeout: null,
+ _resizeTimeout: null
+});
+
+/**
+ * Functions handling the requests details view.
+ */
+function NetworkDetailsView() {
+ dumpn("NetworkDetailsView was instantiated");
+
+ this._onTabSelect = this._onTabSelect.bind(this);
+};
+
+NetworkDetailsView.prototype = {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function() {
+ dumpn("Initializing the RequestsMenuView");
+
+ this.widget = $("#details-pane");
+
+ 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, {
+ searchPlaceholder: L10N.getStr("jsonFilterText")
+ }));
+
+ this._paramsQueryString = L10N.getStr("paramsQueryString");
+ this._paramsFormData = L10N.getStr("paramsFormData");
+ this._paramsPostPayload = L10N.getStr("paramsPostPayload");
+ this._requestHeaders = L10N.getStr("requestHeaders");
+ 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 SourcesView");
+ },
+
+ /**
+ * Sets this view hidden or visible. It's visible by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ toggle: function(aVisibleFlag) {
+ NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
+ NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Hides and resets this container (removes all the networking information).
+ */
+ reset: function() {
+ this.toggle(false);
+ this._dataSrc = null;
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object aData
+ * The data source (this should be the attachment of a request item).
+ */
+ populate: function(aData) {
+ $("#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;
+ $("#response-content-image-box").hidden = true;
+
+ this._headers.empty();
+ this._cookies.empty();
+ this._params.empty();
+ this._json.empty();
+
+ this._dataSrc = { src: aData, populated: [] };
+ this._onTabSelect();
+ },
+
+ /**
+ * Listener handling the tab selection event.
+ */
+ _onTabSelect: function() {
+ let { src, populated } = this._dataSrc || {};
+ let tab = this.widget.selectedIndex;
+
+ // Make sure the data source is valid and don't populate the same tab twice.
+ if (!src || populated[tab]) {
+ return;
+ }
+
+ switch (tab) {
+ case 0: // "Headers"
+ this._setSummary(src);
+ this._setResponseHeaders(src.responseHeaders);
+ this._setRequestHeaders(src.requestHeaders);
+ break;
+ case 1: // "Cookies"
+ this._setResponseCookies(src.responseCookies);
+ this._setRequestCookies(src.requestCookies);
+ break;
+ case 2: // "Params"
+ this._setRequestGetParams(src.url);
+ this._setRequestPostParams(src.requestHeaders, src.requestPostData);
+ break;
+ case 3: // "Response"
+ this._setResponseBody(src.url, src.responseContent);
+ break;
+ case 4: // "Timings"
+ this._setTimingsInformation(src.eventTimings);
+ break;
+ }
+
+ populated[tab] = true;
+ },
+
+ /**
+ * Sets the network request summary shown in this view.
+ *
+ * @param object aData
+ * The data source (this should be the attachment of a request item).
+ */
+ _setSummary: function(aData) {
+ if (aData.url) {
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.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 (aData.method) {
+ $("#headers-summary-method-value").setAttribute("value", aData.method);
+ $("#headers-summary-method").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-method").setAttribute("hidden", "true");
+ }
+
+ if (aData.status) {
+ $("#headers-summary-status-circle").setAttribute("code", aData.status);
+ $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
+ $("#headers-summary-status").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-status").setAttribute("hidden", "true");
+ }
+
+ if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
+ $("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
+ $("#headers-summary-version").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-version").setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Sets the network request headers shown in this view.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setRequestHeaders: function(aResponse) {
+ if (aResponse && aResponse.headers.length) {
+ this._addHeaders(this._requestHeaders, aResponse);
+ }
+ },
+
+ /**
+ * Sets the network response headers shown in this view.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setResponseHeaders: function(aResponse) {
+ if (aResponse && aResponse.headers.length) {
+ aResponse.headers.sort((a, b) => a.name > b.name);
+ this._addHeaders(this._responseHeaders, aResponse);
+ }
+ },
+
+ /**
+ * Populates the headers container in this view with the specified data.
+ *
+ * @param string aName
+ * The type of headers to populate (request or response).
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _addHeaders: function(aName, aResponse) {
+ let kb = aResponse.headersSize / 1024;
+ let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
+ let text = L10N.getFormatStr("networkMenu.sizeKB", size);
+ let headersScope = this._headers.addScope(aName + " (" + text + ")");
+ headersScope.expanded = true;
+
+ for (let header of aResponse.headers) {
+ let headerVar = headersScope.addItem(header.name, {}, true);
+ gNetwork.getString(header.value).then((aString) => headerVar.setGrip(aString));
+ }
+ },
+
+ /**
+ * Sets the network request cookies shown in this view.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setRequestCookies: function(aResponse) {
+ if (aResponse && aResponse.cookies.length) {
+ aResponse.cookies.sort((a, b) => a.name > b.name);
+ this._addCookies(this._requestCookies, aResponse);
+ }
+ },
+
+ /**
+ * Sets the network response cookies shown in this view.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setResponseCookies: function(aResponse) {
+ if (aResponse && aResponse.cookies.length) {
+ this._addCookies(this._responseCookies, aResponse);
+ }
+ },
+
+ /**
+ * Populates the cookies container in this view with the specified data.
+ *
+ * @param string aName
+ * The type of cookies to populate (request or response).
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _addCookies: function(aName, aResponse) {
+ let cookiesScope = this._cookies.addScope(aName);
+ cookiesScope.expanded = true;
+
+ for (let cookie of aResponse.cookies) {
+ let cookieVar = cookiesScope.addItem(cookie.name, {}, true);
+ gNetwork.getString(cookie.value).then((aString) => cookieVar.setGrip(aString));
+
+ // 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 aUrl
+ * The request's url.
+ */
+ _setRequestGetParams: function(aUrl) {
+ let query = nsIURL(aUrl).query;
+ if (query) {
+ this._addParams(this._paramsQueryString, query);
+ }
+ },
+
+ /**
+ * Sets the network request post params shown in this view.
+ *
+ * @param object aHeadersResponse
+ * The "requestHeaders" message received from the server.
+ * @param object aPostDataResponse
+ * The "requestPostData" message received from the server.
+ */
+ _setRequestPostParams: function(aHeadersResponse, aPostDataResponse) {
+ if (!aHeadersResponse || !aPostDataResponse) {
+ return;
+ }
+ gNetwork.getString(aPostDataResponse.postData.text).then((aString) => {
+ // Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
+ let cType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
+ let cString = cType ? cType.value : "";
+ if (cString.contains("x-www-form-urlencoded") ||
+ aString.contains("x-www-form-urlencoded")) {
+ let formDataGroups = aString.split(/\r\n|\n|\r/);
+ for (let group of formDataGroups) {
+ this._addParams(this._paramsFormData, group);
+ }
+ }
+ // Handle actual forms ("multipart/form-data" content type).
+ 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;
+ NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+ aEditor.setText(aString);
+ });
+ }
+ window.emit("NetMonitor:ResponsePostParamsAvailable");
+ });
+ },
+
+ /**
+ * Populates the params container in this view with the specified data.
+ *
+ * @param string aName
+ * The type of params to populate (get or post).
+ * @param string aParams
+ * A query string of params (e.g. "?foo=bar&baz=42").
+ */
+ _addParams: function(aName, aParams) {
+ // Make sure there's at least one param available.
+ if (!aParams || !aParams.contains("=")) {
+ return;
+ }
+ // Turn the params string into an array containing { name: value } tuples.
+ let paramsArray = aParams.replace(/^[?&]/, "").split("&").map((e) =>
+ let (param = e.split("=")) {
+ name: NetworkHelper.convertToUnicode(unescape(param[0])),
+ value: NetworkHelper.convertToUnicode(unescape(param[1]))
+ });
+
+ let paramsScope = this._params.addScope(aName);
+ paramsScope.expanded = true;
+
+ for (let param of paramsArray) {
+ let headerVar = paramsScope.addItem(param.name, {}, true);
+ headerVar.setGrip(param.value);
+ }
+ },
+
+ /**
+ * Sets the network response body shown in this view.
+ *
+ * @param string aUrl
+ * The request's url.
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setResponseBody: function(aUrl, aResponse) {
+ if (!aResponse) {
+ return;
+ }
+ let { mimeType, text, encoding } = aResponse.content;
+
+ gNetwork.getString(text).then((aString) => {
+ // Handle json.
+ if (mimeType.contains("/json")) {
+ let jsonpRegex = /^[a-zA-Z0-9_$]+\(|\)$/g; // JSONP with callback.
+ let sanitizedJSON = aString.replace(jsonpRegex, "");
+ let callbackPadding = aString.match(jsonpRegex);
+
+ // Make sure this is an valid JSON object first. If so, nicely display
+ // the parsing results in a variables view. Otherwise, simply show
+ // the contents as plain text.
+ try {
+ var jsonObject = JSON.parse(sanitizedJSON);
+ } catch (e) {
+ var parsingError = e;
+ }
+
+ // Valid JSON.
+ if (jsonObject) {
+ $("#response-content-json-box").hidden = false;
+ let jsonScopeName = callbackPadding
+ ? L10N.getFormatStr("jsonpScopeName", callbackPadding[0].slice(0, -1))
+ : L10N.getStr("jsonScopeName");
+
+ let jsonScope = this._json.addScope(jsonScopeName);
+ jsonScope.addItem().populate(jsonObject, { expanded: true });
+ jsonScope.expanded = true;
+ }
+ // Malformed JSON.
+ else {
+ $("#response-content-textarea-box").hidden = false;
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ aEditor.setText(aString);
+ });
+ let infoHeader = $("#response-content-info-header");
+ infoHeader.setAttribute("value", parsingError);
+ infoHeader.setAttribute("tooltiptext", parsingError);
+ infoHeader.hidden = false;
+ }
+ }
+ // Handle images.
+ else if (mimeType.contains("image/")) {
+ $("#response-content-image-box").setAttribute("align", "center");
+ $("#response-content-image-box").setAttribute("pack", "center");
+ $("#response-content-image-box").hidden = false;
+ $("#response-content-image").src =
+ "data:" + mimeType + ";" + encoding + "," + aString;
+
+ // Immediately display additional information about the image:
+ // file name, mime type and encoding.
+ $("#response-content-image-name-value").setAttribute("value", nsIURL(aUrl).fileName);
+ $("#response-content-image-mime-value").setAttribute("value", mimeType);
+ $("#response-content-image-encoding-value").setAttribute("value", encoding);
+
+ // 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) + " x " + (height - 2);
+ $("#response-content-image-dimensions-value").setAttribute("value", dimensions);
+ };
+ }
+ // Handle anything else.
+ else {
+ $("#response-content-textarea-box").hidden = false;
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ aEditor.setMode(SourceEditor.MODES.TEXT);
+ aEditor.setText(aString);
+
+ // Maybe set a more appropriate mode in the Source Editor if possible,
+ // but avoid doing this for very large files.
+ if (aString.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
+ for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
+ if (mimeType.contains(key)) {
+ aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
+ break;
+ }
+ }
+ }
+ });
+ }
+ window.emit("NetMonitor:ResponseBodyAvailable");
+ });
+ },
+
+ /**
+ * Sets the timings information shown in this view.
+ *
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _setTimingsInformation: function(aResponse) {
+ if (!aResponse) {
+ return;
+ }
+ let { blocked, dns, connect, send, wait, receive } = aResponse.timings;
+
+ let tabboxWidth = $("#details-pane").getAttribute("width");
+ let availableWidth = tabboxWidth / 2; // Other nodes also take some space.
+ let scale = Math.max(availableWidth / aResponse.totalTime, 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)";
+ },
+
+ _dataSrc: null,
+ _headers: null,
+ _cookies: null,
+ _params: null,
+ _json: null,
+ _paramsQueryString: "",
+ _paramsFormData: "",
+ _paramsPostPayload: "",
+ _requestHeaders: "",
+ _responseHeaders: "",
+ _requestCookies: "",
+ _responseCookies: ""
+};
+
+/**
+ * DOM query helper.
+ */
+function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
+
+/**
+ * Helper for getting an nsIURL instance out of a string.
+ */
+function nsIURL(aUrl, aStore = nsIURL.store) {
+ if (aStore.has(aUrl)) {
+ return aStore.get(aUrl);
+ }
+ let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ aStore.set(aUrl, uri);
+ return uri;
+}
+nsIURL.store = new Map();
+
+/**
+ * Helper for draining a rapid succession of events and invoking a callback
+ * once everything settles down.
+ */
+function drain(aId, aWait, aCallback, aStore = drain.store) {
+ window.clearTimeout(aStore.get(aId));
+ aStore.set(aId, window.setTimeout(aCallback, aWait));
+}
+drain.store = new Map();
+
+/**
+ * Preliminary setup for the NetMonitorView object.
+ */
+NetMonitorView.Toolbar = new ToolbarView();
+NetMonitorView.RequestsMenu = new RequestsMenuView();
+NetMonitorView.NetworkDetails = new NetworkDetailsView();
diff --git a/browser/devtools/netmonitor/netmonitor.css b/browser/devtools/netmonitor/netmonitor.css
new file mode 100644
index 000000000..cae41f8aa
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -0,0 +1,68 @@
+/* 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;
+}
+
+#details-pane-toggle[disabled] {
+ visibility: hidden;
+}
+
+#response-content-image-box {
+ overflow: auto;
+}
+
+#timings-summary-blocked {
+ display: none; /* This doesn't work yet. */
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+ #toolbar-spacer,
+ #details-pane-toggle,
+ #details-pane[pane-collapsed],
+ .requests-menu-waterfall,
+ .requests-menu-footer-label {
+ display: none;
+ }
+}
+
+@media (min-width: 701px) and (max-width: 1024px) {
+ #body:not([pane-collapsed]) .requests-menu-footer-button,
+ #body:not([pane-collapsed]) .requests-menu-footer-spacer {
+ display: none;
+ }
+}
+
+@media (min-width: 701px) {
+ #requests-menu-spacer-start {
+ display: none;
+ }
+
+ #network-table[waterfall-overflows] .requests-menu-waterfall {
+ display: none;
+ }
+
+ #network-table[size-overflows] .requests-menu-size {
+ display: none;
+ }
+
+ #network-table[type-overflows] .requests-menu-type {
+ display: none;
+ }
+
+ #network-table[domain-overflows] .requests-menu-domain {
+ display: none;
+ }
+
+ #network-table[type-overflows] .requests-menu-domain {
+ -moz-box-flex: 1;
+ }
+
+ #network-table[domain-overflows] .requests-menu-file {
+ -moz-box-flex: 1;
+ }
+}
diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul
new file mode 100644
index 000000000..06f4f1e09
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -0,0 +1,377 @@
+<?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://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/netmonitor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/netmonitor.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % netmonitorDTD SYSTEM "chrome://browser/locale/devtools/netmonitor.dtd">
+ %netmonitorDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="text/javascript" src="netmonitor-controller.js"/>
+ <script type="text/javascript" src="netmonitor-view.js"/>
+
+ <box id="body"
+ class="devtools-responsive-container"
+ flex="1">
+ <vbox id="network-table" flex="1">
+ <toolbar id="requests-menu-toolbar"
+ class="devtools-toolbar"
+ align="center">
+ <hbox id="toolbar-labels" flex="1">
+ <hbox id="requests-menu-status-and-method-header-box"
+ class="requests-menu-header requests-menu-status-and-method"
+ align="center">
+ <button id="requests-menu-status-button"
+ class="requests-menu-header-button requests-menu-status"
+ onclick="NetMonitorView.RequestsMenu.sortBy('status')"
+ label="&netmonitorUI.toolbar.status;">
+ </button>
+ <button id="requests-menu-method-button"
+ class="requests-menu-header-button requests-menu-method"
+ onclick="NetMonitorView.RequestsMenu.sortBy('method')"
+ label="&netmonitorUI.toolbar.method;"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-file-header-box"
+ class="requests-menu-header requests-menu-file"
+ align="center">
+ <button id="requests-menu-file-button"
+ class="requests-menu-header-button requests-menu-file"
+ onclick="NetMonitorView.RequestsMenu.sortBy('file')"
+ label="&netmonitorUI.toolbar.file;"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-domain-header-box"
+ class="requests-menu-header requests-menu-domain"
+ align="center">
+ <button id="requests-menu-domain-button"
+ class="requests-menu-header-button requests-menu-domain"
+ onclick="NetMonitorView.RequestsMenu.sortBy('domain')"
+ label="&netmonitorUI.toolbar.domain;"
+ 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"
+ onclick="NetMonitorView.RequestsMenu.sortBy('type')"
+ label="&netmonitorUI.toolbar.type;"
+ 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"
+ onclick="NetMonitorView.RequestsMenu.sortBy('size')"
+ label="&netmonitorUI.toolbar.size;"
+ 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"
+ onclick="NetMonitorView.RequestsMenu.sortBy('waterfall')"
+ pack="start"
+ flex="1">
+ <label id="requests-menu-waterfall-label"
+ class="plain requests-menu-waterfall"
+ value="&netmonitorUI.toolbar.waterfall;"/>
+ </button>
+ </hbox>
+ </hbox>
+ <toolbarbutton id="details-pane-toggle"
+ class="devtools-toolbarbutton"
+ tooltiptext="&netmonitorUI.panesButton.tooltip;"
+ disabled="true"
+ tabindex="0"/>
+ </toolbar>
+ <label id="requests-menu-empty-notice"
+ value="&netmonitorUI.emptyNotice2;"/>
+ <vbox id="requests-menu-contents" flex="1">
+ <hbox id="requests-menu-item-template" hidden="true">
+ <hbox class="requests-menu-subitem requests-menu-status-and-method"
+ align="center">
+ <box class="requests-menu-status"/>
+ <label class="plain requests-menu-method"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <label class="plain requests-menu-subitem requests-menu-file"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-domain"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-type"
+ 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">
+ <hbox class="start requests-menu-timings-cap" hidden="true"/>
+ <hbox class="end requests-menu-timings-cap" hidden="true"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </hbox>
+ </hbox>
+ </vbox>
+ <hbox id="requests-menu-footer">
+ <spacer id="requests-menu-spacer-start"
+ class="requests-menu-footer-spacer"
+ flex="100"/>
+ <button id="requests-menu-filter-all-button"
+ class="requests-menu-footer-button"
+ checked="true"
+ onclick="NetMonitorView.RequestsMenu.filterOn('all')"
+ label="&netmonitorUI.footer.filterAll;">
+ </button>
+ <button id="requests-menu-filter-html-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('html')"
+ label="&netmonitorUI.footer.filterHTML;">
+ </button>
+ <button id="requests-menu-filter-css-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('css')"
+ label="&netmonitorUI.footer.filterCSS;">
+ </button>
+ <button id="requests-menu-filter-js-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('js')"
+ label="&netmonitorUI.footer.filterJS;">
+ </button>
+ <button id="requests-menu-filter-xhr-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('xhr')"
+ label="&netmonitorUI.footer.filterXHR;">
+ </button>
+ <button id="requests-menu-filter-fonts-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('fonts')"
+ label="&netmonitorUI.footer.filterFonts;">
+ </button>
+ <button id="requests-menu-filter-images-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('images')"
+ label="&netmonitorUI.footer.filterImages;">
+ </button>
+ <button id="requests-menu-filter-media-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('media')"
+ label="&netmonitorUI.footer.filterMedia;">
+ </button>
+ <button id="requests-menu-filter-flash-button"
+ class="requests-menu-footer-button"
+ onclick="NetMonitorView.RequestsMenu.filterOn('flash')"
+ label="&netmonitorUI.footer.filterFlash;">
+ </button>
+ <spacer id="requests-menu-spacer-end"
+ class="requests-menu-footer-spacer"
+ flex="100"/>
+ <label id="request-menu-network-summary"
+ class="plain requests-menu-footer-label"
+ flex="1"
+ crop="end"/>
+ </hbox>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <tabbox id="details-pane"
+ class="devtools-sidebar-tabs"
+ hidden="true">
+ <tabs>
+ <tab label="&netmonitorUI.tab.headers;"/>
+ <tab label="&netmonitorUI.tab.cookies;"/>
+ <tab label="&netmonitorUI.tab.params;"/>
+ <tab label="&netmonitorUI.tab.response;"/>
+ <tab label="&netmonitorUI.tab.timings;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="headers-tabppanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <hbox id="headers-summary-url"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.summary.url;"/>
+ <label id="headers-summary-url-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="headers-summary-method"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.summary.method;"/>
+ <label id="headers-summary-method-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="headers-summary-status"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.summary.status;"/>
+ <box id="headers-summary-status-circle"
+ class="requests-menu-status"/>
+ <label id="headers-summary-status-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="headers-summary-version"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.summary.version;"/>
+ <label id="headers-summary-version-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </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"/>
+ </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"
+ value="&netmonitorUI.response.name;"/>
+ <label id="response-content-image-name-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.response.dimensions;"/>
+ <label id="response-content-image-dimensions-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.response.mime;"/>
+ <label id="response-content-image-mime-value"
+ class="plain tabpanel-summary-value"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ value="&netmonitorUI.response.encoding;"/>
+ <label id="response-content-image-encoding-value"
+ class="plain tabpanel-summary-value"
+ 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"
+ value="&netmonitorUI.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"
+ value="&netmonitorUI.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"
+ value="&netmonitorUI.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"
+ value="&netmonitorUI.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"
+ value="&netmonitorUI.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"
+ value="&netmonitorUI.timings.receive;"/>
+ <hbox class="requests-menu-timings-box receive"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </box>
+
+</window>
diff --git a/browser/devtools/netmonitor/test/Makefile.in b/browser/devtools/netmonitor/test/Makefile.in
new file mode 100644
index 000000000..7ec624bac
--- /dev/null
+++ b/browser/devtools/netmonitor/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_TESTS = \
+ browser_net_aaa_leaktest.js \
+ browser_net_autoscroll.js \
+ browser_net_simple-init.js \
+ browser_net_page-nav.js \
+ browser_net_prefs-and-l10n.js \
+ browser_net_prefs-reload.js \
+ browser_net_pane-collapse.js \
+ browser_net_pane-toggle.js \
+ browser_net_simple-request.js \
+ browser_net_simple-request-data.js \
+ browser_net_simple-request-details.js \
+ browser_net_content-type.js \
+ browser_net_cyrillic-01.js \
+ browser_net_cyrillic-02.js \
+ browser_net_large-response.js \
+ browser_net_status-codes.js \
+ browser_net_post-data-01.js \
+ browser_net_post-data-02.js \
+ browser_net_jsonp.js \
+ browser_net_json-long.js \
+ browser_net_json-malformed.js \
+ browser_net_timeline_ticks.js \
+ browser_net_sort-01.js \
+ browser_net_sort-02.js \
+ browser_net_sort-03.js \
+ browser_net_filter-01.js \
+ browser_net_filter-02.js \
+ browser_net_filter-03.js \
+ browser_net_accessibility-01.js \
+ browser_net_accessibility-02.js \
+ browser_net_footer-summary.js \
+ browser_net_req-resp-bodies.js \
+ head.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_PAGES = \
+ test-image.png \
+ html_simple-test-page.html \
+ html_navigate-test-page.html \
+ html_content-type-test-page.html \
+ html_cyrillic-test-page.html \
+ html_status-codes-test-page.html \
+ html_post-data-test-page.html \
+ html_post-raw-test-page.html \
+ html_jsonp-test-page.html \
+ html_json-long-test-page.html \
+ html_json-malformed-test-page.html \
+ html_sorting-test-page.html \
+ html_filter-test-page.html \
+ html_infinite-get-page.html \
+ html_custom-get-page.html \
+ sjs_simple-test-server.sjs \
+ sjs_content-type-test-server.sjs \
+ sjs_status-codes-test-server.sjs \
+ sjs_sorting-test-server.sjs \
+ $(NULL)
+
+MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/netmonitor/test/browser_net_aaa_leaktest.js b/browser/devtools/netmonitor/test/browser_net_aaa_leaktest.js
new file mode 100644
index 000000000..a9cd6fea7
--- /dev/null
+++ b/browser/devtools/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/ */
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ ok(aTab, "Should have a tab available.");
+ ok(aDebuggee, "Should have a debuggee available.");
+ ok(aMonitor, "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.");
+
+ teardown(aMonitor).then(finish);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_accessibility-01.js b/browser/devtools/netmonitor/test/browser_net_accessibility-01.js
new file mode 100644
index 000000000..7c9f54391
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_accessibility-01.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if focus modifiers work for the SideMenuWidget.
+ */
+
+function test() {
+ initNetMonitor(CUSTOM_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 2).then(() => {
+ 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);
+
+ aDebuggee.performRequests(18);
+ return waitForNetworkEvents(aMonitor, 18);
+ })
+ .then(() => {
+ 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);
+
+ teardown(aMonitor).then(finish);
+ });
+
+ let count = 0;
+
+ function check(aSelectedIndex, aPaneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, aSelectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !aPaneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ aDebuggee.performRequests(2);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_accessibility-02.js b/browser/devtools/netmonitor/test/browser_net_accessibility-02.js
new file mode 100644
index 000000000..aac9162e1
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_accessibility-02.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if keyboard and mouse navigation works in the network requests menu.
+ */
+
+function test() {
+ initNetMonitor(CUSTOM_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { window, $, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 2).then(() => {
+ 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);
+
+ aDebuggee.performRequests(18);
+ return waitForNetworkEvents(aMonitor, 18);
+ })
+ .then(() => {
+ 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);
+
+ teardown(aMonitor).then(finish);
+ });
+
+ let count = 0;
+
+ function check(aSelectedIndex, aPaneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, aSelectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !aPaneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ aDebuggee.performRequests(2);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_autoscroll.js b/browser/devtools/netmonitor/test/browser_net_autoscroll.js
new file mode 100644
index 000000000..64f60badf
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_autoscroll.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 863102 - Automatically scroll down upon new network requests.
+ */
+
+function test() {
+ let monitor, debuggee, requestsContainer, scrollTop;
+
+ initNetMonitor(INFINITE_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ monitor = aMonitor;
+ debuggee = aDebuggee;
+ let win = monitor.panelWin;
+ let topNode = win.document.getElementById("requests-menu-contents");
+ 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.
+ .then(() => {
+ return waitForRequestsToOverflowContainer(monitor, requestsContainer);
+ })
+ .then(() => {
+ 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.
+ .then(() => {
+ let children = requestsContainer.childNodes;
+ let middleNode = children.item(children.length / 2);
+ middleNode.scrollIntoView();
+ ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
+ scrollTop = requestsContainer.scrollTop; // save for comparison later
+ return waitForNetworkEvents(monitor, 8);
+ })
+ .then(() => {
+ 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.
+ .then(() => {
+ requestsContainer.scrollTop = requestsContainer.scrollHeight;
+ ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
+ return waitForNetworkEvents(monitor, 8);
+ })
+ .then(() => {
+ 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.
+ .then(() => {
+ monitor.panelWin.NetMonitorView.RequestsMenu.selectedIndex = 0;
+ return waitForNetworkEvents(monitor, 8);
+ })
+ .then(() => {
+ is(requestsContainer.scrollTop, 0, "Did not scroll.");
+ })
+
+ // Done; clean up.
+ .then(() => {
+ return teardown(monitor).then(finish);
+ })
+
+ // Handle exceptions in the chain of promises.
+ .then(null, (err) => {
+ ok(false, err);
+ finish();
+ });
+
+ function waitForRequestsToOverflowContainer (aMonitor, aContainer) {
+ return waitForNetworkEvents(aMonitor, 1).then(() => {
+ if (aContainer.scrollHeight > aContainer.clientHeight) {
+ // Wait for some more just for good measure.
+ return waitForNetworkEvents(aMonitor, 8);
+ } else {
+ return waitForRequestsToOverflowContainer(aMonitor, aContainer);
+ }
+ });
+ }
+
+ function scrolledToBottom(aElement) {
+ return aElement.scrollTop + aElement.clientHeight >= aElement.scrollHeight;
+ }
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_content-type.js b/browser/devtools/netmonitor/test/browser_net_content-type.js
new file mode 100644
index 000000000..3c7ed2d62
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_content-type.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if different response content types are handled correctly.
+ */
+
+function test() {
+ initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 6).then(() => {
+ 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.sizeKB", 0.04),
+ 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.sizeKB", 0.03),
+ 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.sizeKB", 0.03),
+ 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.sizeKB", 0.02),
+ 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.sizeKB", 0.02),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+ "GET", TEST_IMAGE, {
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.75),
+ time: true
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ testResponseTab("xml")
+ .then(() => {
+ RequestsMenu.selectedIndex = 1;
+ return testResponseTab("css");
+ })
+ .then(() => {
+ RequestsMenu.selectedIndex = 2;
+ return testResponseTab("js");
+ })
+ .then(() => {
+ RequestsMenu.selectedIndex = 3;
+ return testResponseTab("json");
+ })
+ .then(() => {
+ RequestsMenu.selectedIndex = 4;
+ return testResponseTab("html");
+ })
+ .then(() => {
+ RequestsMenu.selectedIndex = 5;
+ return testResponseTab("png");
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+
+ function testResponseTab(aType) {
+ let tab = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tab.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ function checkVisibility(aBox) {
+ 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"), aBox != "json",
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), aBox != "textarea",
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), aBox != "image",
+ "The response content image box doesn't have the intended visibility.");
+ }
+
+ switch (aType) {
+ case "xml": {
+ checkVisibility("textarea");
+
+ return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "<label value='greeting'>Hello XML!</label>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(aEditor.getMode(), SourceEditor.MODES.HTML,
+ "The mode active in the source editor is incorrect for the xml request.");
+ });
+ }
+ case "css": {
+ checkVisibility("textarea");
+
+ return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "body:pre { content: 'Hello CSS!' }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(aEditor.getMode(), SourceEditor.MODES.CSS,
+ "The mode active in the source editor is incorrect for the xml request.");
+ });
+ }
+ case "js": {
+ checkVisibility("textarea");
+
+ return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "function() { return 'Hello JS!'; }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+ "The mode active in the source editor is incorrect for the xml request.");
+ });
+ }
+ 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 Object]", "The second json property value was incorrect.");
+
+ return Promise.resolve();
+ }
+ case "html": {
+ checkVisibility("textarea");
+
+ return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "<blink>Not Found</blink>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(aEditor.getMode(), SourceEditor.MODES.HTML,
+ "The mode active in the source editor is incorrect for the xml request.");
+ });
+ }
+ case "png": {
+ checkVisibility("image");
+
+ let imageNode = tabpanel.querySelector("#response-content-image");
+ let deferred = Promise.defer();
+
+ imageNode.addEventListener("load", function onLoad() {
+ imageNode.removeEventListener("load", onLoad);
+
+ 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-encoding-value")
+ .getAttribute("value"), "base64",
+ "The image encoding info isn't correct.");
+ is(tabpanel.querySelector("#response-content-image-dimensions-value")
+ .getAttribute("value"), "16 x 16",
+ "The image dimensions info isn't correct.");
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ }
+ }
+ }
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_cyrillic-01.js b/browser/devtools/netmonitor/test/browser_net_cyrillic-01.js
new file mode 100644
index 000000000..f9da54b31
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic-01.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor.
+ */
+
+function test() {
+ initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ 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]);
+
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText().indexOf("\u044F"), 26, // Ñ
+ "The text shown in the source editor is incorrect.");
+ is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+ "The mode active in the source editor is incorrect.");
+
+ teardown(aMonitor).then(finish);
+ });
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js b/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
new file mode 100644
index 000000000..d47d8e4af
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor
+ * when loaded directly from an HTML page.
+ */
+
+function test() {
+ initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ 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]);
+
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText().indexOf("\u044F"), 189, // Ñ
+ "The text shown in the source editor is incorrect.");
+ is(aEditor.getMode(), SourceEditor.MODES.HTML,
+ "The mode active in the source editor is incorrect.");
+
+ teardown(aMonitor).then(finish);
+ });
+ });
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_filter-01.js b/browser/devtools/netmonitor/test/browser_net_filter-01.js
new file mode 100644
index 000000000..c6d36e57f
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_filter-01.js
@@ -0,0 +1,184 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if filtering items in the network table works correctly.
+ */
+
+function test() {
+ initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 8).then(() => {
+ 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.");
+
+ testButtons("all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1])
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testButtons("html");
+ return testContents([1, 0, 0, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testButtons("css");
+ return testContents([0, 1, 0, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ testButtons("js");
+ return testContents([0, 0, 1, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
+ testButtons("xhr");
+ return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
+ testButtons("fonts");
+ return testContents([0, 0, 0, 1, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
+ testButtons("images");
+ return testContents([0, 0, 0, 0, 1, 0, 0, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
+ testButtons("media");
+ return testContents([0, 0, 0, 0, 0, 1, 1, 0]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testButtons("flash");
+ return testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+ })
+ .then(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testButtons("all");
+ return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testButtons(aFilterType) {
+ let doc = aMonitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-filter-" + aFilterType + "-button");
+ let buttons = doc.querySelectorAll(".requests-menu-footer-button");
+
+ for (let button of buttons) {
+ if (button != target) {
+ is(button.hasAttribute("checked"), false,
+ "The " + button.id + " button should not have a 'checked' attribute.");
+ } else {
+ is(button.hasAttribute("checked"), true,
+ "The " + button.id + " button should have a 'checked' attribute.");
+ }
+ }
+ }
+
+ function testContents(aVisibility) {
+ 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.orderedItems.length, aVisibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, aVisibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < aVisibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !aVisibility[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"
+ });
+
+ return Promise.resolve(null);
+ }
+
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_filter-02.js b/browser/devtools/netmonitor/test/browser_net_filter-02.js
new file mode 100644
index 000000000..5b8c6457a
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_filter-02.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if filtering items in the network table works correctly with new requests.
+ */
+
+function test() {
+ initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 8).then(() => {
+ 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.");
+
+ testButtons("all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1])
+ .then(() => {
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testButtons("html");
+ return testContents([1, 0, 0, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ return waitForNetworkEvents(aMonitor, 8);
+ })
+ .then(() => {
+ info("Testing html filtering again.");
+ testButtons("html");
+ return testContents([1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ return waitForNetworkEvents(aMonitor, 8);
+ })
+ .then(() => {
+ info("Testing html filtering again.");
+ testButtons("html");
+ return testContents([1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testButtons(aFilterType) {
+ let doc = aMonitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-filter-" + aFilterType + "-button");
+ let buttons = doc.querySelectorAll(".requests-menu-footer-button");
+
+ for (let button of buttons) {
+ if (button != target) {
+ is(button.hasAttribute("checked"), false,
+ "The " + button.id + " button should not have a 'checked' attribute.");
+ } else {
+ is(button.hasAttribute("checked"), true,
+ "The " + button.id + " button should have a 'checked' attribute.");
+ }
+ }
+ }
+
+ function testContents(aVisibility) {
+ 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.orderedItems.length, aVisibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, aVisibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < aVisibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !aVisibility[i],
+ "The item at index " + i + " doesn't have the correct hidden state.");
+ }
+
+ for (let i = 0; i < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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 < aVisibility.length; i += 8) {
+ 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"
+ });
+ }
+
+ return Promise.resolve(null);
+ }
+
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_filter-03.js b/browser/devtools/netmonitor/test/browser_net_filter-03.js
new file mode 100644
index 000000000..e874a02ad
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_filter-03.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if filtering items in the network table works correctly with new requests
+ * and while sorting is enabled.
+ */
+
+function test() {
+ initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 7).then(() => {
+ 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.");
+
+ testButtons("all");
+ testContents([0, 1, 2, 3, 4, 5, 6], 7, 0)
+ .then(() => {
+ info("Sorting by size, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testButtons("all");
+ return testContents([6, 4, 5, 0, 1, 2, 3], 7, 6);
+ })
+ .then(() => {
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testButtons("html");
+ return testContents([6, 4, 5, 0, 1, 2, 3], 1, 6);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests('{ "getMedia": true }');
+ return waitForNetworkEvents(aMonitor, 7);
+ })
+ .then(() => {
+ info("Testing html filtering again.");
+ resetSorting();
+ testButtons("html");
+ return testContents([8, 13, 9, 11, 10, 12, 0, 4, 1, 5, 2, 6, 3, 7], 2, 13);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests('{ "getMedia": true }');
+ return waitForNetworkEvents(aMonitor, 7);
+ })
+ .then(() => {
+ info("Testing html filtering again.");
+ resetSorting();
+ testButtons("html");
+ return testContents([12, 13, 20, 14, 16, 18, 15, 17, 19, 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11], 3, 20);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function resetSorting() {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ }
+
+ function testButtons(aFilterType) {
+ let doc = aMonitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-filter-" + aFilterType + "-button");
+ let buttons = doc.querySelectorAll(".requests-menu-footer-button");
+
+ for (let button of buttons) {
+ if (button != target) {
+ is(button.hasAttribute("checked"), false,
+ "The " + button.id + " button should not have a 'checked' attribute.");
+ } else {
+ is(button.hasAttribute("checked"), true,
+ "The " + button.id + " button should have a 'checked' attribute.");
+ }
+ }
+ }
+
+ function testContents(aOrder, aVisible, aSelection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, aSelection,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.orderedItems.length, aOrder.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, aVisible,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < aOrder.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.orderedItems[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[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 = aOrder.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 6]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ }
+
+ return Promise.resolve(null);
+ }
+
+ let str = "'<p>'" + new Array(10).join(Math.random(10)) + "'</p>'";
+ aDebuggee.performRequests('{ "htmlContent": "' + str + '", "getMedia": true }');
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_footer-summary.js b/browser/devtools/netmonitor/test/browser_net_footer-summary.js
new file mode 100644
index 000000000..4ef217f53
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_footer-summary.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if the summary text displayed in the network requests menu footer
+ * is correct.
+ */
+
+function test() {
+ let { PluralForm } = Cu.import("resource://gre/modules/PluralForm.jsm", {});
+
+ initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { $, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ testStatus();
+
+ waitForNetworkEvents(aMonitor, 8).then(() => {
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testStatus();
+
+ info("Performing more requests.");
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ return waitForNetworkEvents(aMonitor, 8);
+ })
+ .then(() => {
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
+ testStatus();
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testStatus();
+
+ teardown(aMonitor).then(finish);
+ })
+
+ function testStatus() {
+ let summary = $("#request-menu-network-summary");
+ let value = summary.getAttribute("value");
+ info("Current summary: " + value);
+
+ let visibleItems = RequestsMenu.visibleItems;
+ let visibleRequestsCount = visibleItems.length;
+ let totalRequestsCount = RequestsMenu.itemCount;
+ info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
+
+ if (!totalRequestsCount) {
+ is(value, "",
+ "The current summary text is incorrect, expected an empty string.");
+ return;
+ }
+
+ if (!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.")
+ }
+
+ aDebuggee.performRequests('{ "getMedia": true, "getFlash": true }');
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_json-long.js b/browser/devtools/netmonitor/test/browser_net_json-long.js
new file mode 100644
index 000000000..c683b8cda
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_json-long.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if very long JSON responses are handled correctly.
+ */
+
+function test() {
+ initNetMonitor(JSON_LONG_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ 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, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ 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
+ });
+
+ aMonitor.panelWin.once("NetMonitor:ResponseBodyAvailable", () => {
+ testResponseTab();
+ teardown(aMonitor).then(finish);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ function testResponseTab() {
+ let tab = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tab.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, 6057,
+ "There should be 6057 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 .name";
+ let values = ".variables-view-property .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 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.");
+
+ is(Array.slice(jsonScope.querySelectorAll(names), -1).shift().getAttribute("value"),
+ "__proto__", "The last json property name was incorrect.");
+ is(Array.slice(jsonScope.querySelectorAll(values), -1).shift().getAttribute("value"),
+ "[object Object]", "The last json property value was incorrect.");
+ }
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_json-malformed.js b/browser/devtools/netmonitor/test/browser_net_json-malformed.js
new file mode 100644
index 000000000..1ea3124d4
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_json-malformed.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if malformed JSON responses are handled correctly.
+ */
+
+function test() {
+ initNetMonitor(JSON_MALFORMED_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8"
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ let tab = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tab.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",
+ "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",
+ "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.");
+
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "{ \"greeting\": \"Hello malformed JSON!\" },",
+ "The text shown in the source editor is incorrect.");
+ is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+ "The mode active in the source editor is incorrect.");
+
+ teardown(aMonitor).then(finish);
+ });
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_jsonp.js b/browser/devtools/netmonitor/test/browser_net_jsonp.js
new file mode 100644
index 000000000..a3b3f03c9
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_jsonp.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if JSONP responses are handled correctly.
+ */
+
+function test() {
+ initNetMonitor(JSONP_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ 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.sizeKB", 0.04),
+ time: true
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ testResponseTab();
+ teardown(aMonitor).then(finish);
+
+ function testResponseTab() {
+ let tab = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tab.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", "$_0123Fun"),
+ "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 JSONP!\"", "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 Object]", "The second json property value was incorrect.");
+ }
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_large-response.js b/browser/devtools/netmonitor/test/browser_net_large-response.js
new file mode 100644
index 000000000..41561ed50
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_large-response.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if very large response contents are just displayed as plain text.
+ */
+
+function test() {
+ initNetMonitor(CUSTOM_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ 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, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
+ status: 200,
+ statusText: "OK"
+ });
+
+ aMonitor.panelWin.once("NetMonitor:ResponseBodyAvailable", () => {
+ NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ ok(aEditor.getText().match(/^<p>/),
+ "The text shown in the source editor is incorrect.");
+ is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+ "The mode active in the source editor is incorrect.");
+
+ teardown(aMonitor).then(finish);
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ });
+
+ aDebuggee.performRequests(1, CONTENT_TYPE_SJS + "?fmt=html-long");
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_page-nav.js b/browser/devtools/netmonitor/test/browser_net_page-nav.js
new file mode 100644
index 000000000..690a3b8c1
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_page-nav.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if page navigation ("close", "navigate", etc.) triggers an appropriate
+ * action in the network monitor.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ testNavigate(() => testNavigateBack(() => testClose(() => finish())));
+
+ function testNavigate(aCallback) {
+ info("Navigating forward...");
+
+ aMonitor.panelWin.once("NetMonitor:TargetWillNavigate", () => {
+ is(aDebuggee.location, SIMPLE_URL,
+ "Target started navigating to the correct location.");
+
+ aMonitor.panelWin.once("NetMonitor:TargetNavigate", () => {
+ is(aDebuggee.location, NAVIGATE_URL,
+ "Target finished navigating to the correct location.");
+
+ aCallback();
+ });
+ });
+
+ aDebuggee.location = NAVIGATE_URL;
+ }
+
+ function testNavigateBack(aCallback) {
+ info("Navigating backward...");
+
+ aMonitor.panelWin.once("NetMonitor:TargetWillNavigate", () => {
+ is(aDebuggee.location, NAVIGATE_URL,
+ "Target started navigating back to the previous location.");
+
+ aMonitor.panelWin.once("NetMonitor:TargetNavigate", () => {
+ is(aDebuggee.location, SIMPLE_URL,
+ "Target finished navigating back to the previous location.");
+
+ aCallback();
+ });
+ });
+
+ aDebuggee.location = SIMPLE_URL;
+ }
+
+ function testClose(aCallback) {
+ info("Closing...");
+
+ aMonitor.once("destroyed", () => {
+ ok(!aMonitor._controller.client,
+ "There shouldn't be a client available after destruction.");
+ ok(!aMonitor._controller.tabClient,
+ "There shouldn't be a tabClient available after destruction.");
+ ok(!aMonitor._controller.webConsoleClient,
+ "There shouldn't be a webConsoleClient available after destruction.");
+
+ aCallback();
+ });
+
+ removeTab(aTab);
+ }
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_pane-collapse.js b/browser/devtools/netmonitor/test/browser_net_pane-collapse.js
new file mode 100644
index 000000000..6434aa259
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_pane-collapse.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the network monitor panes collapse properly.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, Prefs, NetMonitorView } = aMonitor.panelWin;
+ let detailsPane = document.getElementById("details-pane");
+ let detailsPaneToggleButton = document.getElementById("details-pane-toggle");
+
+ ok(detailsPane.hasAttribute("pane-collapsed") &&
+ detailsPaneToggleButton.hasAttribute("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.hasAttribute("pane-collapsed") &&
+ !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The details pane should at this point be visible.");
+
+ NetMonitorView.toggleDetailsPane({ visible: false, animated: true });
+
+ 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.hasAttribute("pane-collapsed") &&
+ detailsPaneToggleButton.hasAttribute("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.hasAttribute("pane-collapsed") &&
+ !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+ "The details pane should be visible again after uncollapsing.");
+
+ teardown(aMonitor).then(finish);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_pane-toggle.js b/browser/devtools/netmonitor/test/browser_net_pane-toggle.js
new file mode 100644
index 000000000..552c39cda
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_pane-toggle.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if toggling the details pane works as expected.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = aMonitor.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("#details-pane-toggle")
+ .hasAttribute("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.");
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after the first request.");
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("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" },
+ document.getElementById("details-pane-toggle"));
+
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed.");
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("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" },
+ document.getElementById("details-pane-toggle"));
+
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed again.");
+ is(document.querySelector("#details-pane-toggle")
+ .hasAttribute("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.");
+
+ teardown(aMonitor).then(finish);
+ });
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_post-data-01.js b/browser/devtools/netmonitor/test/browser_net_post-data-01.js
new file mode 100644
index 000000000..1e23adeb3
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-01.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the POST requests display the correct information in the UI.
+ */
+
+function test() {
+ initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ waitForNetworkEvents(aMonitor, 0, 2).then(() => {
+ 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.sizeKB", 0.01),
+ 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.sizeKB", 0.01),
+ time: true
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+
+ testParamsTab("urlencoded")
+ .then(() => {
+ RequestsMenu.selectedIndex = 1;
+ return testParamsTab("multipart");
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+
+ function testParamsTab(aType) {
+ let tab = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tab.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ function checkVisibility(aBox) {
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), !aBox.contains("params"),
+ "The request params box doesn't have the indended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), !aBox.contains("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(aType == "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"),
+ "\"" + aType + "\"", "The third query param value was incorrect.");
+
+ if (aType == "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.");
+
+ return Promise.resolve();
+ }
+ 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.");
+
+ return NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+ ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"text\""),
+ "The text shown in the source editor is incorrect (1.1).");
+ ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"email\""),
+ "The text shown in the source editor is incorrect (2.1).");
+ ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"range\""),
+ "The text shown in the source editor is incorrect (3.1).");
+ ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"Custom field\""),
+ "The text shown in the source editor is incorrect (4.1).");
+ ok(aEditor.getText().contains("Some text..."),
+ "The text shown in the source editor is incorrect (2.2).");
+ ok(aEditor.getText().contains("42"),
+ "The text shown in the source editor is incorrect (3.2).");
+ ok(aEditor.getText().contains("Extra data"),
+ "The text shown in the source editor is incorrect (4.2).");
+ is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+ "The mode active in the source editor is incorrect.");
+ });
+ }
+ }
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_post-data-02.js b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
new file mode 100644
index 000000000..b3d200a9e
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * even for raw payloads without attached content-type headers.
+ */
+
+function test() {
+ initNetMonitor(POST_RAW_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ waitForNetworkEvents(aMonitor, 0, 1).then(() => {
+ NetMonitorView.toggleDetailsPane({ visible: true }, 2)
+ RequestsMenu.selectedIndex = 0;
+
+ let tab = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tab.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.");
+
+ teardown(aMonitor).then(finish);
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js b/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
new file mode 100644
index 000000000..ce0f31ff7
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the preferences and localization objects work correctly.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ ok(aMonitor.panelWin.L10N,
+ "Should have a localization object available on the panel window.");
+ ok(aMonitor.panelWin.Prefs,
+ "Should have a preferences object available on the panel window.");
+
+ function testL10N() {
+ let { L10N } = aMonitor.panelWin;
+
+ ok(L10N.stringBundle,
+ "The localization object should have a string bundle available.");
+
+ let bundleName = "chrome://browser/locale/devtools/netmonitor.properties";
+ let stringBundle = Services.strings.createBundle(bundleName);
+
+ is(L10N.getStr("netmonitor.label"),
+ stringBundle.GetStringFromName("netmonitor.label"),
+ "The getStr() method didn't return the expected string.");
+
+ is(L10N.getFormatStr("networkMenu.totalMS", "foo"),
+ stringBundle.formatStringFromName("networkMenu.totalMS", ["foo"], 1),
+ "The getFormatStr() method didn't return the expected string.");
+ }
+
+ function testPrefs() {
+ let { Prefs } = aMonitor.panelWin;
+
+ is(Prefs.root, "devtools.netmonitor",
+ "The preferences object should have a correct root path.");
+
+ 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.");
+ }
+
+ testL10N();
+ testPrefs();
+
+ teardown(aMonitor).then(finish);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_prefs-reload.js b/browser/devtools/netmonitor/test/browser_net_prefs-reload.js
new file mode 100644
index 000000000..d5fd61c43
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_prefs-reload.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the prefs that should survive across tool reloads work.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ 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);
+
+ let prefsToCheck = {
+ networkDetailsWidth: {
+ newValue: ~~(Math.random() * 200 + 100),
+ validate: ($) => ~~$("#details-pane").getAttribute("width"),
+ modifyFrontend: ($, aValue) => $("#details-pane").setAttribute("width", aValue)
+ },
+ networkDetailsHeight: {
+ newValue: ~~(Math.random() * 300 + 100),
+ validate: ($) => ~~$("#details-pane").getAttribute("height"),
+ modifyFrontend: ($, aValue) => $("#details-pane").setAttribute("height", aValue)
+ },
+ /* add more prefs here... */
+ };
+
+ function storeFirstPrefValues() {
+ info("Caching initial pref values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = aMonitor.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 = aMonitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let validate = prefsToCheck[name].validate;
+
+ is(currentValue, firstValue,
+ "Pref " + name + " should be equal to first value: " + firstValue);
+ is(currentValue, validate(aMonitor.panelWin.$),
+ "Pref " + name + " should validate: " + currentValue);
+ }
+ }
+
+ function modifyFrontend() {
+ info("Modifying UI elements to the specified new values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = aMonitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validate = prefsToCheck[name].validate;
+ let modifyFrontend = prefsToCheck[name].modifyFrontend;
+
+ modifyFrontend(aMonitor.panelWin.$, newValue);
+ info("Modified UI element affecting " + name + " to: " + newValue);
+
+ is(currentValue, firstValue,
+ "Pref " + name + " should still be equal to first value: " + firstValue);
+ isnot(currentValue, newValue,
+ "Pref " + name + " should't yet be equal to second value: " + newValue);
+ is(newValue, validate(aMonitor.panelWin.$),
+ "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 = aMonitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validate = prefsToCheck[name].validate;
+
+ isnot(currentValue, firstValue,
+ "Pref " + name + " should't be equal to first value: " + firstValue);
+ is(currentValue, newValue,
+ "Pref " + name + " should now be equal to second value: " + newValue);
+ is(newValue, validate(aMonitor.panelWin.$),
+ "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 = aMonitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validate = prefsToCheck[name].validate;
+ let modifyFrontend = prefsToCheck[name].modifyFrontend;
+
+ modifyFrontend(aMonitor.panelWin.$, firstValue);
+ info("Modified UI element affecting " + name + " to: " + firstValue);
+
+ isnot(currentValue, firstValue,
+ "Pref " + name + " should't yet be equal to first value: " + firstValue);
+ is(currentValue, newValue,
+ "Pref " + name + " should still be equal to second value: " + newValue);
+ is(firstValue, validate(aMonitor.panelWin.$),
+ "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();
+
+ return restartNetMonitor(aMonitor)
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate and reset frontend while toolbox is on the bottom.
+ validateNewPrefValues();
+ resetFrontend();
+
+ return restartNetMonitor(aMonitor);
+ })
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ });
+ }
+
+ function testSide() {
+ info("Moving toolbox to the side...");
+
+ return aMonitor._toolbox.switchHost(Toolbox.HostType.SIDE)
+ .then(() => {
+ info("Testing prefs reload for a side host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is on the side.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ return restartNetMonitor(aMonitor);
+ })
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate and reset frontend while toolbox is on the side.
+ validateNewPrefValues();
+ resetFrontend();
+
+ return restartNetMonitor(aMonitor);
+ })
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ });
+ }
+
+ function testWindow() {
+ info("Moving toolbox into a window...");
+
+ return aMonitor._toolbox.switchHost(Toolbox.HostType.WINDOW)
+ .then(() => {
+ info("Testing prefs reload for a window host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is in a window.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ return restartNetMonitor(aMonitor);
+ })
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate and reset frontend while toolbox is in a window.
+ validateNewPrefValues();
+ resetFrontend();
+
+ return restartNetMonitor(aMonitor);
+ })
+ .then(([,, aNewMonitor]) => {
+ aMonitor = aNewMonitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ });
+ }
+
+ function cleanupAndFinish() {
+ info("Moving toolbox back to the bottom...");
+
+ aMonitor._toolbox.switchHost(Toolbox.HostType.BOTTOM)
+ .then(() => teardown(aMonitor))
+ .then(finish);
+ }
+
+ testBottom()
+ .then(testSide)
+ .then(testWindow)
+ .then(cleanupAndFinish);
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_req-resp-bodies.js b/browser/devtools/netmonitor/test/browser_net_req-resp-bodies.js
new file mode 100644
index 000000000..1f1a55a16
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_req-resp-bodies.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if request and response body logging stays on after opening the console.
+ */
+
+function test() {
+ initNetMonitor(JSON_LONG_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ function verifyRequest(aOffset) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOffset),
+ "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
+ });
+ }
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ verifyRequest(0);
+
+ aMonitor._toolbox.once("webconsole-selected", () => {
+ aMonitor._toolbox.once("netmonitor-selected", () => {
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ verifyRequest(1);
+ teardown(aMonitor).then(finish);
+ });
+
+ // Perform another batch of requests.
+ aDebuggee.performRequests();
+ });
+
+ // Reload debugee.
+ aDebuggee.location.reload();
+ });
+
+ // Switch back to the netmonitor.
+ aMonitor._toolbox.selectTool("netmonitor");
+ });
+
+ // Switch to the webconsole.
+ aMonitor._toolbox.selectTool("webconsole");
+ });
+
+ // Perform first batch of requests.
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_simple-init.js b/browser/devtools/netmonitor/test/browser_net_simple-init.js
new file mode 100644
index 000000000..9f444bb49
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-init.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Simple check if the network monitor starts up and shuts down properly.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ is(aTab.linkedBrowser.contentWindow.wrappedJSObject.location, SIMPLE_URL,
+ "The current tab's location is the correct one.");
+ is(aDebuggee.location, SIMPLE_URL,
+ "The current debuggee's location is the correct one.");
+
+ function checkIfInitialized(aTag) {
+ info("Checking if initialization is ok (" + aTag + ").");
+
+ ok(aMonitor._view,
+ "The network monitor view object exists (" + aTag + ").");
+ ok(aMonitor._view._isInitialized,
+ "The network monitor view object exists and is initialized (" + aTag + ").");
+
+ ok(aMonitor._controller,
+ "The network monitor controller object exists (" + aTag + ").");
+ ok(aMonitor._controller._isInitialized,
+ "The network monitor controller object exists and is initialized (" + aTag + ").");
+
+ ok(aMonitor.isReady,
+ "The network monitor panel appears to be ready (" + aTag + ").");
+
+ ok(aMonitor._controller.client,
+ "There should be a client available at this point (" + aTag + ").");
+ ok(aMonitor._controller.tabClient,
+ "There should be a tabClient available at this point (" + aTag + ").");
+ ok(aMonitor._controller.webConsoleClient,
+ "There should be a webConsoleClient available at this point (" + aTag + ").");
+ }
+
+ function checkIfDestroyed(aTag) {
+ info("Checking if destruction is ok.");
+
+ ok(!aMonitor._controller.client,
+ "There shouldn't be a client available after destruction (" + aTag + ").");
+ ok(!aMonitor._controller.tabClient,
+ "There shouldn't be a tabClient available after destruction (" + aTag + ").");
+ ok(!aMonitor._controller.webConsoleClient,
+ "There shouldn't be a webConsoleClient available after destruction (" + aTag + ").");
+ }
+
+ executeSoon(() => {
+ checkIfInitialized(1);
+
+ aMonitor._controller.startupNetMonitor()
+ .then(() => {
+ info("Starting up again shouldn't do anything special.");
+ checkIfInitialized(2);
+ return aMonitor._controller.connect();
+ })
+ .then(() => {
+ info("Connecting again shouldn't do anything special.");
+ checkIfInitialized(3);
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ registerCleanupFunction(() => {
+ checkIfDestroyed(1);
+
+ aMonitor._controller.shutdownNetMonitor()
+ .then(() => {
+ info("Shutting down again shouldn't do anything special.");
+ checkIfDestroyed(2);
+ return aMonitor._controller.disconnect();
+ })
+ .then(() => {
+ info("Disconnecting again shouldn't do anything special.");
+ checkIfDestroyed(3);
+ });
+ });
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_simple-request-data.js b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
new file mode 100644
index 000000000..d9efa5eeb
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests render correct information in the menu UI.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1)
+ .then(() => teardown(aMonitor))
+ .then(finish);
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+ 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);
+ let target = requestItem.target;
+
+ 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.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);
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestHeaders", () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestHeaders,
+ "There should be a requestHeaders attachment available.");
+ ok(requestItem.attachment.requestHeaders.headers.length >= 6,
+ "The requestHeaders attachment has an incorrect |headers| property.");
+ // Can't test for an exact total number of headers, because it seems to
+ // vary across pgo/non-pgo builds.
+ 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);
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestCookies", () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestCookies,
+ "There should be a requestCookies attachment available.");
+ is(requestItem.attachment.requestCookies.cookies.length, 0,
+ "The requestCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestPostData", () => {
+ ok(false, "Trap listener: this request doesn't have any post data.")
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseHeaders", () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseHeaders,
+ "There should be a responseHeaders attachment available.");
+ is(requestItem.attachment.responseHeaders.headers.length, 6,
+ "The responseHeaders attachment has an incorrect |headers| property.");
+ is(requestItem.attachment.responseHeaders.headersSize, 173,
+ "The responseHeaders attachment has an incorrect |headersSize| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseCookies", () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseCookies,
+ "There should be a responseCookies attachment available.");
+ is(requestItem.attachment.responseCookies.cookies.length, 0,
+ "The responseCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:ResponseStart", () => {
+ 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, 173,
+ "The headersSize attachment has an incorrect value.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ status: "200",
+ statusText: "Och Aye"
+ });
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:ResponseContent", () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseContent", () => {
+ 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:EventTimings", () => {
+ 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
+ });
+ });
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:EventTimings", () => {
+ 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
+ });
+ });
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_simple-request-details.js b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
new file mode 100644
index 000000000..c43afb222
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests render correct information in the details UI.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ 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.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("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.");
+
+ testHeadersTab();
+ testCookiesTab();
+ testParamsTab();
+ testResponseTab()
+ .then(() => {
+ testTimingsTab();
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testHeadersTab() {
+ let tab = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ is(tab.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-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.");
+ ok(tabpanel.querySelectorAll(".variable-or-property").length >= 12,
+ "There should be at least 12 header values displayed in this tabpanel.");
+ // Can't test for an exact total number of headers, because it seems to
+ // vary across pgo/non-pgo builds.
+
+ 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(173/1024, 3)) + ")",
+ "The response headers scope doesn't have the correct title.");
+
+ ok(requestScope.querySelector(".name").getAttribute("value").contains(
+ 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"),
+ "Connection", "The first response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+ "\"close\"", "The first response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
+ "Content-Length", "The second response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
+ "\"12\"", "The second response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[2].getAttribute("value"),
+ "Content-Type", "The third response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[2].getAttribute("value"),
+ "\"text/plain; charset=utf-8\"", "The third response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[5].getAttribute("value"),
+ "foo-bar", "The last response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[5].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")[5].getAttribute("value"),
+ "Connection", "The penultimate request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[5].getAttribute("value"),
+ "\"keep-alive\"", "The penultimate request header value was incorrect.");
+
+ let lastReqHeaderName = requestScope.querySelectorAll(".variables-view-variable .name")[6];
+ let lastReqHeaderValue = requestScope.querySelectorAll(".variables-view-variable .value")[6];
+ if (lastReqHeaderName && lastReqHeaderValue) {
+ is(lastReqHeaderName.getAttribute("value"),
+ "Cache-Control", "The last request header name was incorrect.");
+ is(lastReqHeaderValue.getAttribute("value"),
+ "\"max-age=0\"", "The last request header value was incorrect.");
+ } else {
+ info("The number of request headers was 6 instead of 7. Technically, " +
+ "not a failure in this particular test, but needs investigation.");
+ }
+ }
+
+ function testCookiesTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[1]);
+
+ let tab = document.querySelectorAll("#details-pane tab")[1];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[1];
+
+ is(tab.getAttribute("selected"), "true",
+ "The cookies tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+ "There should be no cookie scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+ "There should be no cookie values displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+ "The empty notice should be displayed in this tabpanel.");
+ }
+
+ function testParamsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+
+ let tab = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tab.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() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ let tab = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tab.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.");
+
+ return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+ is(aEditor.getText(), "Hello world!",
+ "The text shown in the source editor is incorrect.");
+ is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+ "The mode active in the source editor is incorrect.");
+ });
+ }
+
+ function testTimingsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[4]);
+
+ let tab = document.querySelectorAll("#details-pane tab")[4];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[4];
+
+ is(tab.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.");
+ }
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_simple-request.js b/browser/devtools/netmonitor/test/browser_net_simple-request.js
new file mode 100644
index 000000000..2544faf6b
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests are handled correctly.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = aMonitor.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.");
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+ 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.");
+
+ aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+ 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.");
+
+ teardown(aMonitor).then(finish);
+ });
+
+ aDebuggee.location.reload();
+ });
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_sort-01.js b/browser/devtools/netmonitor/test/browser_net_sort-01.js
new file mode 100644
index 000000000..a57a5326d
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-01.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if the sorting mechanism works correctly.
+ */
+
+function test() {
+ initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 5).then(() => {
+ testContents([0, 1, 2, 3, 4])
+ .then(() => {
+ info("Testing swap(0, 0)");
+ RequestsMenu.swapItemsAtIndices(0, 0);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing swap(0, 1)");
+ RequestsMenu.swapItemsAtIndices(0, 1);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 0, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing swap(0, 2)");
+ RequestsMenu.swapItemsAtIndices(0, 2);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 2, 0, 3, 4]);
+ })
+ .then(() => {
+ info("Testing swap(0, 3)");
+ RequestsMenu.swapItemsAtIndices(0, 3);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 2, 3, 0, 4]);
+ })
+ .then(() => {
+ info("Testing swap(0, 4)");
+ RequestsMenu.swapItemsAtIndices(0, 4);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 2, 3, 4, 0]);
+ })
+ .then(() => {
+ info("Testing swap(1, 0)");
+ RequestsMenu.swapItemsAtIndices(1, 0);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 2, 3, 4, 1]);
+ })
+ .then(() => {
+ info("Testing swap(1, 1)");
+ RequestsMenu.swapItemsAtIndices(1, 1);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 2, 3, 4, 1]);
+ })
+ .then(() => {
+ info("Testing swap(1, 2)");
+ RequestsMenu.swapItemsAtIndices(1, 2);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 1, 3, 4, 2]);
+ })
+ .then(() => {
+ info("Testing swap(1, 3)");
+ RequestsMenu.swapItemsAtIndices(1, 3);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 3, 1, 4, 2]);
+ })
+ .then(() => {
+ info("Testing swap(1, 4)");
+ RequestsMenu.swapItemsAtIndices(1, 4);
+ RequestsMenu.refreshZebra();
+ return testContents([0, 3, 4, 1, 2]);
+ })
+ .then(() => {
+ info("Testing swap(2, 0)");
+ RequestsMenu.swapItemsAtIndices(2, 0);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 3, 4, 1, 0]);
+ })
+ .then(() => {
+ info("Testing swap(2, 1)");
+ RequestsMenu.swapItemsAtIndices(2, 1);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 3, 4, 2, 0]);
+ })
+ .then(() => {
+ info("Testing swap(2, 2)");
+ RequestsMenu.swapItemsAtIndices(2, 2);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 3, 4, 2, 0]);
+ })
+ .then(() => {
+ info("Testing swap(2, 3)");
+ RequestsMenu.swapItemsAtIndices(2, 3);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 2, 4, 3, 0]);
+ })
+ .then(() => {
+ info("Testing swap(2, 4)");
+ RequestsMenu.swapItemsAtIndices(2, 4);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 4, 2, 3, 0]);
+ })
+ .then(() => {
+ info("Testing swap(3, 0)");
+ RequestsMenu.swapItemsAtIndices(3, 0);
+ RequestsMenu.refreshZebra();
+ return testContents([1, 4, 2, 0, 3]);
+ })
+ .then(() => {
+ info("Testing swap(3, 1)");
+ RequestsMenu.swapItemsAtIndices(3, 1);
+ RequestsMenu.refreshZebra();
+ return testContents([3, 4, 2, 0, 1]);
+ })
+ .then(() => {
+ info("Testing swap(3, 2)");
+ RequestsMenu.swapItemsAtIndices(3, 2);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 4, 3, 0, 1]);
+ })
+ .then(() => {
+ info("Testing swap(3, 3)");
+ RequestsMenu.swapItemsAtIndices(3, 3);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 4, 3, 0, 1]);
+ })
+ .then(() => {
+ info("Testing swap(3, 4)");
+ RequestsMenu.swapItemsAtIndices(3, 4);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 3, 4, 0, 1]);
+ })
+ .then(() => {
+ info("Testing swap(4, 0)");
+ RequestsMenu.swapItemsAtIndices(4, 0);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 3, 0, 4, 1]);
+ })
+ .then(() => {
+ info("Testing swap(4, 1)");
+ RequestsMenu.swapItemsAtIndices(4, 1);
+ RequestsMenu.refreshZebra();
+ return testContents([2, 3, 0, 1, 4]);
+ })
+ .then(() => {
+ info("Testing swap(4, 2)");
+ RequestsMenu.swapItemsAtIndices(4, 2);
+ RequestsMenu.refreshZebra();
+ return testContents([4, 3, 0, 1, 2]);
+ })
+ .then(() => {
+ info("Testing swap(4, 3)");
+ RequestsMenu.swapItemsAtIndices(4, 3);
+ RequestsMenu.refreshZebra();
+ return testContents([3, 4, 0, 1, 2]);
+ })
+ .then(() => {
+ info("Testing swap(4, 4)");
+ RequestsMenu.swapItemsAtIndices(4, 4);
+ RequestsMenu.refreshZebra();
+ return testContents([3, 4, 0, 1, 2]);
+ })
+ .then(() => {
+ info("Clearing sort.");
+ RequestsMenu.sortBy();
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testContents([a, b, c, d, e]) {
+ is(RequestsMenu.orderedItems.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(RequestsMenu.getItemAtIndex(0), RequestsMenu.orderedItems[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.orderedItems[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.orderedItems[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.orderedItems[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.orderedItems[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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+ 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ 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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+
+ return Promise.resolve(null);
+ }
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_sort-02.js b/browser/devtools/netmonitor/test/browser_net_sort-02.js
new file mode 100644
index 000000000..b2e9cfe46
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-02.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if sorting columns in the network table works correctly.
+ */
+
+function test() {
+ initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ 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 { $, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 5).then(() => {
+ 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])
+ .then(() => {
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ return testContents([4, 3, 2, 1, 0]);
+ })
+ .then(() => {
+ info("Testing status sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing method sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing method sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "descending");
+ return testContents([4, 3, 2, 1, 0]);
+ })
+ .then(() => {
+ info("Testing method sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing file sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing file sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "descending");
+ return testContents([4, 3, 2, 1, 0]);
+ })
+ .then(() => {
+ info("Testing file sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing type sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing type sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "descending");
+ return testContents([4, 3, 2, 1, 0]);
+ })
+ .then(() => {
+ info("Testing type sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing size sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing size sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "descending");
+ return testContents([4, 3, 2, 1, 0]);
+ })
+ .then(() => {
+ info("Testing size sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ return testContents([0, 1, 2, 3, 4]);
+ })
+ .then(() => {
+ info("Testing waterfall sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ return testContents([0, 2, 4, 3, 1]);
+ })
+ .then(() => {
+ info("Testing waterfall sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "descending");
+ return testContents([4, 2, 0, 1, 3]);
+ })
+ .then(() => {
+ info("Testing waterfall sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ return testContents([0, 2, 4, 3, 1]);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testHeaders(aSortType, aDirection) {
+ let doc = aMonitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + aSortType + "-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"), aDirection,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), aDirection == "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.orderedItems.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(RequestsMenu.getItemAtIndex(0), RequestsMenu.orderedItems[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.orderedItems[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.orderedItems[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.orderedItems[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.orderedItems[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",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
+ time: true
+ });
+
+ return Promise.resolve(null);
+ }
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_sort-03.js b/browser/devtools/netmonitor/test/browser_net_sort-03.js
new file mode 100644
index 000000000..c6f9214bc
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-03.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if sorting columns in the network table works correctly with new requests.
+ */
+
+function test() {
+ initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ 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 { $, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 5).then(() => {
+ 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)
+ .then(() => {
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ return testContents([0, 1, 2, 3, 4], 0);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests();
+ return waitForNetworkEvents(aMonitor, 5);
+ })
+ .then(() => {
+ info("Testing status sort again, ascending.");
+ testHeaders("status", "ascending");
+ return testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+ })
+ .then(() => {
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ return testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+ })
+ .then(() => {
+ info("Performing more requests.");
+ aDebuggee.performRequests();
+ return waitForNetworkEvents(aMonitor, 5);
+ })
+ .then(() => {
+ info("Testing status sort again, descending.");
+ testHeaders("status", "descending");
+ return testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 12);
+ })
+ .then(() => {
+ return teardown(aMonitor);
+ })
+ .then(finish);
+ });
+
+ function testHeaders(aSortType, aDirection) {
+ let doc = aMonitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + aSortType + "-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"), aDirection,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), aDirection == "ascending"
+ ? L10N.getStr("networkMenu.sortedAsc")
+ : L10N.getStr("networkMenu.sortedDesc"),
+ "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+ }
+ }
+ }
+
+ function testContents(aOrder, aSelection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after sorting.");
+ is(RequestsMenu.selectedIndex, aSelection,
+ "The first item should be still selected after sorting.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after sorting.");
+
+ is(RequestsMenu.orderedItems.length, aOrder.length,
+ "There should be a specific number of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, aOrder.length,
+ "There should be a specific number of visbile items in the requests menu.");
+
+ for (let i = 0; i < aOrder.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.orderedItems[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i]),
+ "GET1", SORTING_SJS + "?index=1", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Meh",
+ type: "1",
+ fullMimeType: "text/1",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+ time: true
+ });
+ }
+ for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len]),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ time: true
+ });
+ }
+ for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 2]),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+ }
+ for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 3]),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
+ time: true
+ });
+ }
+ for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 4]),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
+ time: true
+ });
+ }
+
+ return Promise.resolve(null);
+ }
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_status-codes.js b/browser/devtools/netmonitor/test/browser_net_status-codes.js
new file mode 100644
index 000000000..0e62f8071
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_status-codes.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests display the correct status code and text in the UI.
+ */
+
+function test() {
+ initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ waitForNetworkEvents(aMonitor, 5).then(() => {
+ let requestItems = [];
+
+ verifyRequestItemTarget(requestItems[0] = RequestsMenu.getItemAtIndex(0),
+ "GET", STATUS_CODES_SJS + "?sts=100", {
+ status: 101,
+ statusText: "Switching Protocols",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(requestItems[1] = RequestsMenu.getItemAtIndex(1),
+ "GET", STATUS_CODES_SJS + "?sts=200", {
+ status: 202,
+ statusText: "Created",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+ verifyRequestItemTarget(requestItems[2] = RequestsMenu.getItemAtIndex(2),
+ "GET", STATUS_CODES_SJS + "?sts=300", {
+ status: 303,
+ statusText: "See Other",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(requestItems[3] = RequestsMenu.getItemAtIndex(3),
+ "GET", STATUS_CODES_SJS + "?sts=400", {
+ status: 404,
+ statusText: "Not Found",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+ verifyRequestItemTarget(requestItems[4] = RequestsMenu.getItemAtIndex(4),
+ "GET", STATUS_CODES_SJS + "?sts=500", {
+ status: 501,
+ statusText: "Not Implemented",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+ time: true
+ });
+
+ // Test summaries...
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[0]);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
+ testSummary("GET", STATUS_CODES_SJS + "?sts=100", "101", "Switching Protocols");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
+ testSummary("GET", STATUS_CODES_SJS + "?sts=200", "202", "Created");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
+ testSummary("GET", STATUS_CODES_SJS + "?sts=300", "303", "See Other");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
+ testSummary("GET", STATUS_CODES_SJS + "?sts=400", "404", "Not Found");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
+ testSummary("GET", STATUS_CODES_SJS + "?sts=500", "501", "Not Implemented");
+
+ // Test params...
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
+ testParamsTab("\"100\"");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
+ testParamsTab("\"200\"");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
+ testParamsTab("\"300\"");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
+ testParamsTab("\"400\"");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
+ testParamsTab("\"500\"");
+
+ // We're done here.
+ teardown(aMonitor).then(finish);
+
+ function testSummary(aMethod, aUrl, aStatus, aStatusText) {
+ let tab = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+ aUrl, "The url summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+ aMethod, "The method summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+ aStatus, "The status summary code is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+ aStatus + " " + aStatusText, "The status summary value is incorrect.");
+ }
+
+ function testParamsTab(aStatusParamValue) {
+ let tab = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ 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"),
+ aStatusParamValue, "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.");
+ }
+ });
+
+ aDebuggee.performRequests();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js b/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
new file mode 100644
index 000000000..a56bff992
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if timeline correctly displays interval divisions.
+ */
+
+function test() {
+ initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ let { document, L10N, NetMonitorView } = aMonitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ ok(document.querySelector("#requests-menu-waterfall-label"),
+ "An timeline label should be displayed when the frontend is opened.");
+ ok(document.querySelectorAll(".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.");
+
+ waitForNetworkEvents(aMonitor, 1).then(() => {
+ ok(!document.querySelector("#requests-menu-waterfall-label"),
+ "The timeline label should be hidden after the first request.");
+ ok(document.querySelectorAll(".requests-menu-timings-division").length >= 3,
+ "There should be at least 3 tick labels in the network requests header.");
+
+ is(document.querySelectorAll(".requests-menu-timings-division")[0]
+ .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 0),
+ "The first tick label has an incorrect value");
+ is(document.querySelectorAll(".requests-menu-timings-division")[1]
+ .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 80),
+ "The second tick label has an incorrect value");
+ is(document.querySelectorAll(".requests-menu-timings-division")[2]
+ .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 160),
+ "The third tick label has an incorrect value");
+
+ is(document.querySelectorAll(".requests-menu-timings-division")[0]
+ .style.transform, "translateX(0px)",
+ "The first tick label has an incorrect translation");
+ is(document.querySelectorAll(".requests-menu-timings-division")[1]
+ .style.transform, "translateX(80px)",
+ "The second tick label has an incorrect translation");
+ is(document.querySelectorAll(".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];
+ }
+
+ teardown(aMonitor).then(finish);
+ });
+
+ aDebuggee.location.reload();
+ });
+}
diff --git a/browser/devtools/netmonitor/test/head.js b/browser/devtools/netmonitor/test/head.js
new file mode 100644
index 000000000..dca1dc3cc
--- /dev/null
+++ b/browser/devtools/netmonitor/test/head.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/";
+
+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 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_RAW_URL = EXAMPLE_URL + "html_post-raw-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 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 SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
+const CONTENT_TYPE_SJS = 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 TEST_IMAGE = EXAMPLE_URL + "test-image.png";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// Enable logging for all the relevant tests.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+});
+
+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 browser = tab.linkedBrowser;
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve(tab);
+ }, true);
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetBrowser.removeTab(aTab);
+}
+
+function initNetMonitor(aUrl, aWindow) {
+ info("Initializing a network monitor pane.");
+
+ return addTab(aUrl).then((aTab) => {
+ info("Net tab added successfully: " + aUrl);
+
+ let deferred = Promise.defer();
+ let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
+ let target = TargetFactory.forTab(aTab);
+
+ gDevTools.showToolbox(target, "netmonitor").then((aToolbox) => {
+ info("Netork monitor pane shown successfully.");
+
+ let monitor = aToolbox.getCurrentPanel();
+ deferred.resolve([aTab, debuggee, monitor]);
+ });
+
+ return deferred.promise;
+ });
+}
+
+function restartNetMonitor(aMonitor, aNewUrl) {
+ info("Restarting the specified network monitor.");
+
+ let deferred = Promise.defer();
+ let tab = aMonitor.target.tab;
+ let url = aNewUrl || tab.linkedBrowser.contentWindow.wrappedJSObject.location.href;
+
+ aMonitor.once("destroyed", () => initNetMonitor(url).then(deferred.resolve));
+ removeTab(tab);
+
+ return deferred.promise;
+}
+
+function teardown(aMonitor) {
+ info("Destroying the specified network monitor.");
+
+ let deferred = Promise.defer();
+ let tab = aMonitor.target.tab;
+
+ aMonitor.once("destroyed", () => executeSoon(deferred.resolve));
+ removeTab(tab);
+
+ return deferred.promise;
+}
+
+function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
+ let deferred = Promise.defer();
+
+ let panel = aMonitor.panelWin;
+ let genericEvents = 0;
+ let postEvents = 0;
+
+ function onGenericEvent() {
+ genericEvents++;
+ maybeResolve();
+ }
+
+ function onPostEvent() {
+ postEvents++;
+ maybeResolve();
+ }
+
+ function maybeResolve() {
+ info("> Network events progress: " +
+ genericEvents + "/" + ((aGetRequests + aPostRequests) * 13) + ", " +
+ postEvents + "/" + (aPostRequests * 2));
+
+ // There are 15 updates which need to be fired for a request to be
+ // considered finished. RequestPostData isn't fired for non-POST requests.
+ if (genericEvents == (aGetRequests + aPostRequests) * 13 &&
+ postEvents == aPostRequests * 2) {
+
+ panel.off("NetMonitor:NetworkEventUpdating:RequestHeaders", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:RequestHeaders", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:RequestCookies", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:RequestPostData", onPostEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:RequestPostData", onPostEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:RequestCookies", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:ResponseHeaders", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:ResponseHeaders", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:ResponseCookies", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:ResponseCookies", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:ResponseStart", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:ResponseContent", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:ResponseContent", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdating:EventTimings", onGenericEvent);
+ panel.off("NetMonitor:NetworkEventUpdated:EventTimings", onGenericEvent);
+
+ executeSoon(deferred.resolve);
+ }
+ }
+
+ panel.on("NetMonitor:NetworkEventUpdating:RequestHeaders", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:RequestHeaders", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:RequestCookies", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:RequestPostData", onPostEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:RequestPostData", onPostEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:RequestCookies", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:ResponseHeaders", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:ResponseHeaders", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:ResponseCookies", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:ResponseCookies", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:ResponseStart", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:ResponseContent", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:ResponseContent", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdating:EventTimings", onGenericEvent);
+ panel.on("NetMonitor:NetworkEventUpdated:EventTimings", onGenericEvent);
+
+ return deferred.promise;
+}
+
+function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
+ info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
+ info("> Request: " + aRequestItem.attachment.toSource());
+
+ let requestsMenu = aRequestItem.ownerView;
+ let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
+ let visibleIndex = requestsMenu.orderedVisibleItems.indexOf(aRequestItem);
+
+ info("Widget index of item: " + widgetIndex);
+ info("Visible index of item: " + visibleIndex);
+
+ let { fuzzyUrl, status, statusText, type, fullMimeType, size, time } = aData;
+ let { attachment, target } = aRequestItem
+
+ let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ let name = uri.fileName || "/";
+ let query = uri.query;
+ let hostPort = uri.hostPort;
+
+ if (fuzzyUrl) {
+ ok(attachment.method.startsWith(aMethod), "The attached method is incorrect.");
+ ok(attachment.url.startsWith(aUrl), "The attached url is incorrect.");
+ } else {
+ is(attachment.method, aMethod, "The attached method is incorrect.");
+ is(attachment.url, aUrl, "The attached url is incorrect.");
+ }
+
+ is(target.querySelector(".requests-menu-method").getAttribute("value"),
+ aMethod, "The displayed method is incorrect.");
+
+ if (fuzzyUrl) {
+ ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
+ name + (query ? "?" + query : "")), "The displayed file is incorrect.");
+ ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(
+ name + (query ? "?" + query : "")), "The tooltip file is incorrect.");
+ } else {
+ is(target.querySelector(".requests-menu-file").getAttribute("value"),
+ name + (query ? "?" + query : ""), "The displayed file is incorrect.");
+ is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
+ name + (query ? "?" + query : ""), "The tooltip file is incorrect.");
+ }
+
+ is(target.querySelector(".requests-menu-domain").getAttribute("value"),
+ hostPort, "The displayed domain is incorrect.");
+ is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
+ hostPort, "The tooltip domain is incorrect.");
+
+ if (status !== undefined) {
+ let value = target.querySelector(".requests-menu-status").getAttribute("code");
+ let tooltip = target.querySelector(".requests-menu-status-and-method").getAttribute("tooltiptext");
+ info("Displayed status: " + value);
+ info("Tooltip status: " + tooltip);
+ is(value, status, "The displayed status is incorrect.");
+ is(tooltip, status + " " + statusText, "The tooltip status is incorrect.");
+ }
+ 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 incorrect.");
+ is(tooltip, fullMimeType, "The tooltip type is incorrect.");
+ }
+ 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 incorrect.");
+ is(tooltip, size, "The tooltip size is incorrect.");
+ }
+ 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 incorrect.");
+ ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is incorrect.");
+ }
+
+ if (visibleIndex != -1) {
+ if (visibleIndex % 2 == 0) {
+ ok(aRequestItem.target.hasAttribute("even"),
+ "Unexpected 'even' attribute for " + aRequestItem.value);
+ ok(!aRequestItem.target.hasAttribute("odd"),
+ "Unexpected 'odd' attribute for " + aRequestItem.value);
+ } else {
+ ok(!aRequestItem.target.hasAttribute("even"),
+ "Unexpected 'even' attribute for " + aRequestItem.value);
+ ok(aRequestItem.target.hasAttribute("odd"),
+ "Unexpected 'odd' attribute for " + aRequestItem.value);
+ }
+ }
+}
diff --git a/browser/devtools/netmonitor/test/html_content-type-test-page.html b/browser/devtools/netmonitor/test/html_content-type-test-page.html
new file mode 100644
index 000000000..506ba21c1
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_content-type-test-page.html
@@ -0,0 +1,43 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_custom-get-page.html b/browser/devtools/netmonitor/test/html_custom-get-page.html
new file mode 100644
index 000000000..b01ae09eb
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_custom-get-page.html
@@ -0,0 +1,39 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_cyrillic-test-page.html b/browser/devtools/netmonitor/test/html_cyrillic-test-page.html
new file mode 100644
index 000000000..b9a0dc9d0
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_cyrillic-test-page.html
@@ -0,0 +1,34 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_filter-test-page.html b/browser/devtools/netmonitor/test/html_filter-test-page.html
new file mode 100644
index 000000000..6719991d9
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_filter-test-page.html
@@ -0,0 +1,55 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_infinite-get-page.html b/browser/devtools/netmonitor/test/html_infinite-get-page.html
new file mode 100644
index 000000000..882dc57de
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_infinite-get-page.html
@@ -0,0 +1,36 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_json-long-test-page.html b/browser/devtools/netmonitor/test/html_json-long-test-page.html
new file mode 100644
index 000000000..4889d76c7
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_json-long-test-page.html
@@ -0,0 +1,33 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_json-malformed-test-page.html b/browser/devtools/netmonitor/test/html_json-malformed-test-page.html
new file mode 100644
index 000000000..3c47c797f
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_json-malformed-test-page.html
@@ -0,0 +1,33 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_jsonp-test-page.html b/browser/devtools/netmonitor/test/html_jsonp-test-page.html
new file mode 100644
index 000000000..7cf0c6bbb
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_jsonp-test-page.html
@@ -0,0 +1,33 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/html_navigate-test-page.html b/browser/devtools/netmonitor/test/html_navigate-test-page.html
new file mode 100644
index 000000000..69284120a
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_navigate-test-page.html
@@ -0,0 +1,13 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Navigation test</p>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/html_post-data-test-page.html b/browser/devtools/netmonitor/test/html_post-data-test-page.html
new file mode 100644
index 000000000..4be478226
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_post-data-test-page.html
@@ -0,0 +1,72 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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/browser/devtools/netmonitor/test/html_post-raw-test-page.html b/browser/devtools/netmonitor/test/html_post-raw-test-page.html
new file mode 100644
index 000000000..b3148a77e
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_post-raw-test-page.html
@@ -0,0 +1,34 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(aMessage);
+ }
+
+ function performRequests() {
+ var rawData = "Content-Type: application/x-www-form-urlencoded\r\n\r\nfoo=bar&baz=123";
+ post("sjs_simple-test-server.sjs", rawData, function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/html_simple-test-page.html b/browser/devtools/netmonitor/test/html_simple-test-page.html
new file mode 100644
index 000000000..a8eae55bf
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_simple-test-page.html
@@ -0,0 +1,13 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Simple test</p>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/html_sorting-test-page.html b/browser/devtools/netmonitor/test/html_sorting-test-page.html
new file mode 100644
index 000000000..9326ded2d
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_sorting-test-page.html
@@ -0,0 +1,42 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Sorting test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aIndex, aCallback) {
+ var xhr = new XMLHttpRequest();
+ // Use a random parameter to defeat caching.
+ xhr.open("GET" + aIndex, aAddress + "?index=" + aIndex + "&" + Math.random(), true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_sorting-test-server.sjs", 1, function() {
+ get("sjs_sorting-test-server.sjs", 5, function() {
+ get("sjs_sorting-test-server.sjs", 2, function() {
+ get("sjs_sorting-test-server.sjs", 4, function() {
+ get("sjs_sorting-test-server.sjs", 3, function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/html_status-codes-test-page.html b/browser/devtools/netmonitor/test/html_status-codes-test-page.html
new file mode 100644
index 000000000..f19570e71
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_status-codes-test-page.html
@@ -0,0 +1,41 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <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.
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/browser/devtools/netmonitor/test/moz.build b/browser/devtools/netmonitor/test/moz.build
new file mode 100644
index 000000000..58ce5e273
--- /dev/null
+++ b/browser/devtools/netmonitor/test/moz.build
@@ -0,0 +1,5 @@
+# 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/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
new file mode 100644
index 000000000..a0cb4ec65
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
@@ -0,0 +1,127 @@
+/* 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 format = params.filter((s) => s.contains("fmt="))[0].split("=")[1];
+
+ Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
+ switch (format) {
+ case "txt": {
+ response.setStatusLine(request.httpVersion, 200, "DA DA DA");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("Братан, Ñ‚Ñ‹ вообще качаешьÑÑ?");
+ response.finish();
+ break;
+ }
+ case "xml": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+ response.write("<label value='greeting'>Hello XML!</label>");
+ response.finish();
+ break;
+ }
+ case "html": {
+ let content = params.filter((s) => s.contains("res="))[0].split("=")[1];
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ 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, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write("<p>" + str + "</p>");
+ response.finish();
+ break;
+ }
+ case "css": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+ response.write("body:pre { content: 'Hello CSS!' }");
+ response.finish();
+ break;
+ }
+ case "js": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+ response.write("function() { return 'Hello JS!'; }");
+ response.finish();
+ break;
+ }
+ case "json": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+ response.write("{ \"greeting\": \"Hello JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "jsonp": {
+ let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
+ response.finish();
+ break;
+ }
+ case "json-long": {
+ let str = "{ \"greeting\": \"Hello long string JSON!\" },";
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
+ response.finish();
+ break;
+ }
+ case "json-malformed": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
+ response.finish();
+ break;
+ }
+ case "font": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "font/woff", false);
+ response.finish();
+ break;
+ }
+ case "image": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ response.finish();
+ break;
+ }
+ case "audio": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "audio/ogg", false);
+ response.finish();
+ break;
+ }
+ case "video": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "video/webm", false);
+ response.finish();
+ break;
+ }
+ case "flash": {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/x-shockwave-flash", false);
+ response.finish();
+ break;
+ }
+ default: {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ 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/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs b/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
new file mode 100644
index 000000000..15fe9f8ae
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
@@ -0,0 +1,9 @@
+/* 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("Content-Type", "text/plain; charset=utf-8", false);
+ response.setHeader("Foo-Bar", "baz", false);
+ response.write("Hello world!");
+}
diff --git a/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs b/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs
new file mode 100644
index 000000000..fd31b3266
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs
@@ -0,0 +1,18 @@
+/* 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.contains("index="))[0].split("=")[1];
+
+ Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
+ response.setStatusLine(request.httpVersion, index == 1 ? 101 : index * 100, "Meh");
+ 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/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs b/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
new file mode 100644
index 000000000..bc07336ee
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -0,0 +1,34 @@
+/* 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.contains("sts="))[0].split("=")[1];
+
+ Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
+ 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;
+ }
+ 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/browser/devtools/netmonitor/test/test-image.png b/browser/devtools/netmonitor/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/devtools/netmonitor/test/test-image.png
Binary files differ
diff --git a/browser/devtools/profiler/Makefile.in b/browser/devtools/profiler/Makefile.in
new file mode 100644
index 000000000..42f17d7fe
--- /dev/null
+++ b/browser/devtools/profiler/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/profiler/ProfilerController.jsm b/browser/devtools/profiler/ProfilerController.jsm
new file mode 100644
index 000000000..a89a90b6c
--- /dev/null
+++ b/browser/devtools/profiler/ProfilerController.jsm
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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 = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+let EXPORTED_SYMBOLS = ["ProfilerController"];
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
+ "resource://gre/modules/devtools/dbg-server.jsm");
+
+/**
+ * Data structure that contains information that has
+ * to be shared between separate ProfilerController
+ * instances.
+ */
+const sharedData = {
+ data: new WeakMap(),
+ controllers: new WeakMap(),
+};
+
+/**
+ * Makes a structure representing an individual profile.
+ */
+function makeProfile(name, def={}) {
+ if (def.timeStarted == null)
+ def.timeStarted = null;
+
+ if (def.timeEnded == null)
+ def.timeEnded = null;
+
+ return {
+ name: name,
+ timeStarted: def.timeStarted,
+ timeEnded: def.timeEnded
+ };
+}
+
+// Three functions below all operate with sharedData
+// structure defined above. They should be self-explanatory.
+
+function addTarget(target) {
+ sharedData.data.set(target, new Map());
+}
+
+function getProfiles(target) {
+ return sharedData.data.get(target);
+}
+
+/**
+ * Object to control the JavaScript Profiler over the remote
+ * debugging protocol.
+ *
+ * @param Target target
+ * A target object as defined in Target.jsm
+ */
+function ProfilerController(target) {
+ if (sharedData.controllers.has(target)) {
+ return sharedData.controllers.get(target);
+ }
+
+ this.target = target;
+ this.client = target.client;
+ this.isConnected = false;
+ this.consoleProfiles = [];
+
+ addTarget(target);
+
+ // Chrome debugging targets have already obtained a reference
+ // to the profiler actor.
+ if (target.chrome) {
+ this.isConnected = true;
+ this.actor = target.form.profilerActor;
+ }
+
+ sharedData.controllers.set(target, this);
+};
+
+ProfilerController.prototype = {
+ /**
+ * Return a map of profile results for the current target.
+ *
+ * @return Map
+ */
+ get profiles() {
+ return getProfiles(this.target);
+ },
+
+ /**
+ * Checks whether the profile is currently recording.
+ *
+ * @param object profile
+ * An object made by calling makeProfile function.
+ * @return boolean
+ */
+ isProfileRecording: function PC_isProfileRecording(profile) {
+ return profile.timeStarted !== null && profile.timeEnded === null;
+ },
+
+ /**
+ * A listener that fires whenever console.profile or console.profileEnd
+ * is called.
+ *
+ * @param string type
+ * Type of a call. Either 'profile' or 'profileEnd'.
+ * @param object data
+ * Event data.
+ * @param object panel
+ * A reference to the ProfilerPanel in the current tab.
+ */
+ onConsoleEvent: function (type, data, panel) {
+ let name = data.extra.name;
+
+ let profileStart = () => {
+ if (name && this.profiles.has(name))
+ return;
+
+ // Add profile to the UI (createProfile will return
+ // an automatically generated name if 'name' is falsey).
+ let profile = panel.createProfile(name);
+ profile.start((name, cb) => cb());
+
+ // Add profile structure to shared data.
+ this.profiles.set(profile.name, makeProfile(profile.name, {
+ timeStarted: data.extra.currentTime
+ }));
+ this.consoleProfiles.push(profile.name);
+ };
+
+ let profileEnd = () => {
+ if (!name && !this.consoleProfiles.length)
+ return;
+
+ if (!name)
+ name = this.consoleProfiles.pop();
+ else
+ this.consoleProfiles.filter((n) => n !== name);
+
+ if (!this.profiles.has(name))
+ return;
+
+ let profile = this.profiles.get(name);
+ if (!this.isProfileRecording(profile))
+ return;
+
+ let profileData = data.extra.profile;
+ profile.timeEnded = data.extra.currentTime;
+
+ profileData.threads = profileData.threads.map((thread) => {
+ let samples = thread.samples.filter((sample) => {
+ return sample.time >= profile.timeStarted;
+ });
+
+ return { samples: samples };
+ });
+
+ let ui = panel.getProfileByName(name);
+ ui.data = profileData;
+ ui.parse(profileData, () => panel.emit("parsed"));
+ ui.stop((name, cb) => cb());
+ };
+
+ if (type === "profile")
+ profileStart();
+
+ if (type === "profileEnd")
+ profileEnd();
+ },
+
+ /**
+ * Connects to the client unless we're already connected.
+ *
+ * @param function cb
+ * Function to be called once we're connected. If
+ * the controller is already connected, this function
+ * will be called immediately (synchronously).
+ */
+ connect: function (cb=function(){}) {
+ if (this.isConnected) {
+ return void cb();
+ }
+
+ // Check if we already have a grip to the listTabs response object
+ // and, if we do, use it to get to the profilerActor. Otherwise,
+ // call listTabs. The problem is that if we call listTabs twice
+ // webconsole tests fail (see bug 872826).
+
+ let register = () => {
+ let data = { events: ["console-api-profiler"] };
+
+ // Check if Gecko Profiler Addon [1] is installed and, if it is,
+ // don't register our own console event listeners. Gecko Profiler
+ // Addon takes care of console.profile and console.profileEnd methods
+ // and we don't want to break it.
+ //
+ // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
+
+ AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
+ if (addon && !addon.userDisabled && !addon.softDisabled)
+ return void cb();
+
+ this.request("registerEventNotifications", data, (resp) => {
+ this.client.addListener("eventNotification", (type, resp) => {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (toolbox == null)
+ return;
+
+ let panel = toolbox.getPanel("jsprofiler");
+ if (panel)
+ return void this.onConsoleEvent(resp.subject.action, resp.data, panel);
+
+ // Can't use a promise here because of a race condition when the promise
+ // is resolved only after -ready event is fired when creating a new panel
+ // and during the -ready event when waiting for a panel to be created:
+ //
+ // console.profile(); // creates a new panel, waits for the promise
+ // console.profileEnd(); // panel is not created yet but loading
+ //
+ // -> jsprofiler-ready event is fired which triggers a promise for profileEnd
+ // -> a promise for profile is triggered.
+ //
+ // And it should be the other way around. Hence the event.
+
+ toolbox.once("jsprofiler-ready", (_, panel) => {
+ this.onConsoleEvent(resp.subject.action, resp.data, panel);
+ });
+
+ toolbox.loadTool("jsprofiler");
+ });
+ });
+
+ cb();
+ });
+ };
+
+ if (this.target.root) {
+ this.actor = this.target.root.profilerActor;
+ this.isConnected = true;
+ return void register();
+ }
+
+ this.client.listTabs((resp) => {
+ this.actor = resp.profilerActor;
+ this.isConnected = true;
+ register();
+ });
+ },
+
+ /**
+ * Adds actor and type information to data and sends the request over
+ * the remote debugging protocol.
+ *
+ * @param string type
+ * Method to call on the other side
+ * @param object data
+ * Data to send with the request
+ * @param function cb
+ * A callback function
+ */
+ request: function (type, data, cb) {
+ data.to = this.actor;
+ data.type = type;
+ this.client.request(data, cb);
+ },
+
+ /**
+ * Checks whether the profiler is active.
+ *
+ * @param function cb
+ * Function to be called with a response from the
+ * client. It will be called with two arguments:
+ * an error object (may be null) and a boolean
+ * value indicating if the profiler is active or not.
+ */
+ isActive: function (cb) {
+ this.request("isActive", {}, (resp) => {
+ cb(resp.error, resp.isActive, resp.currentTime);
+ });
+ },
+
+ /**
+ * Creates a new profile and starts the profiler, if needed.
+ *
+ * @param string name
+ * Name of the profile.
+ * @param function cb
+ * Function to be called once the profiler is started
+ * or we get an error. It will be called with a single
+ * argument: an error object (may be null).
+ */
+ start: function PC_start(name, cb) {
+ if (this.profiles.has(name)) {
+ return;
+ }
+
+ let profile = makeProfile(name);
+ this.consoleProfiles.push(name);
+ this.profiles.set(name, profile);
+
+ // If profile is already running, no need to do anything.
+ if (this.isProfileRecording(profile)) {
+ return void cb();
+ }
+
+ this.isActive((err, isActive, currentTime) => {
+ if (isActive) {
+ profile.timeStarted = currentTime;
+ return void cb();
+ }
+
+ let params = {
+ entries: 1000000,
+ interval: 1,
+ features: ["js"],
+ };
+
+ this.request("startProfiler", params, (resp) => {
+ if (resp.error) {
+ return void cb(resp.error);
+ }
+
+ profile.timeStarted = 0;
+ cb();
+ });
+ });
+ },
+
+ /**
+ * Stops the profiler. NOTE, that we don't stop the actual
+ * SPS Profiler here. It will be stopped as soon as all
+ * clients disconnect from the profiler actor.
+ *
+ * @param string name
+ * Name of the profile that needs to be stopped.
+ * @param function cb
+ * Function to be called once the profiler is stopped
+ * or we get an error. It will be called with a single
+ * argument: an error object (may be null).
+ */
+ stop: function PC_stop(name, cb) {
+ if (!this.profiles.has(name)) {
+ return;
+ }
+
+ let profile = this.profiles.get(name);
+ if (!this.isProfileRecording(profile)) {
+ return;
+ }
+
+ this.request("getProfile", {}, (resp) => {
+ if (resp.error) {
+ Cu.reportError("Failed to fetch profile data.");
+ return void cb(resp.error, null);
+ }
+
+ let data = resp.profile;
+ profile.timeEnded = resp.currentTime;
+
+ // Filter out all samples that fall out of current
+ // profile's range.
+
+ data.threads = data.threads.map((thread) => {
+ let samples = thread.samples.filter((sample) => {
+ return sample.time >= profile.timeStarted;
+ });
+
+ return { samples: samples };
+ });
+
+ cb(null, data);
+ });
+ },
+
+ /**
+ * Cleanup.
+ */
+ destroy: function PC_destroy() {
+ this.client = null;
+ this.target = null;
+ this.actor = null;
+ }
+};
diff --git a/browser/devtools/profiler/ProfilerHelpers.jsm b/browser/devtools/profiler/ProfilerHelpers.jsm
new file mode 100644
index 000000000..cfd03ba18
--- /dev/null
+++ b/browser/devtools/profiler/ProfilerHelpers.jsm
@@ -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 Cu = Components.utils;
+const ProfilerProps = "chrome://browser/locale/devtools/profiler.properties";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["L10N"];
+
+/**
+ * Localization helper methods.
+ */
+let L10N = {
+ /**
+ * Returns a simple localized string.
+ *
+ * @param string name
+ * @return string
+ */
+ getStr: function L10N_getStr(name) {
+ return this.stringBundle.GetStringFromName(name);
+ },
+
+ /**
+ * Returns formatted localized string.
+ *
+ * @param string name
+ * @param array params
+ * @return string
+ */
+ getFormatStr: function L10N_getFormatStr(name, params) {
+ return this.stringBundle.formatStringFromName(name, params, params.length);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function () {
+ return Services.strings.createBundle(ProfilerProps);
+}); \ No newline at end of file
diff --git a/browser/devtools/profiler/ProfilerPanel.jsm b/browser/devtools/profiler/ProfilerPanel.jsm
new file mode 100644
index 000000000..0ff79fbb7
--- /dev/null
+++ b/browser/devtools/profiler/ProfilerPanel.jsm
@@ -0,0 +1,716 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a 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;
+
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource:///modules/devtools/ProfilerController.jsm");
+Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
+
+this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
+ "resource://gre/modules/devtools/dbg-server.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const PROFILE_IDLE = 0;
+const PROFILE_RUNNING = 1;
+const PROFILE_COMPLETED = 2;
+
+/**
+ * An instance of a profile UI. Profile UI consists of
+ * an iframe with Cleopatra loaded in it and some
+ * surrounding meta-data (such as uids).
+ *
+ * Its main function is to talk to the Cleopatra instance
+ * inside of the iframe.
+ *
+ * ProfileUI is also an event emitter. It emits the following events:
+ * - ready, when Cleopatra is done loading (you can also check the isReady
+ * property to see if a particular instance has been loaded yet.
+ * - disabled, when Cleopatra gets disabled. Happens when another ProfileUI
+ * instance starts the profiler.
+ * - enabled, when Cleopatra gets enabled. Happens when another ProfileUI
+ * instance stops the profiler.
+ *
+ * @param number uid
+ * Unique ID for this profile.
+ * @param ProfilerPanel panel
+ * A reference to the container panel.
+ */
+function ProfileUI(uid, name, panel) {
+ let doc = panel.document;
+ let win = panel.window;
+
+ EventEmitter.decorate(this);
+
+ this.isReady = false;
+ this.isStarted = false;
+ this.isFinished = false;
+
+ this.messages = [];
+ this.panel = panel;
+ this.uid = uid;
+ this.name = name;
+
+ this.iframe = doc.createElement("iframe");
+ this.iframe.setAttribute("flex", "1");
+ this.iframe.setAttribute("id", "profiler-cleo-" + uid);
+ this.iframe.setAttribute("src", "cleopatra.html?" + uid);
+ this.iframe.setAttribute("hidden", "true");
+
+ // Append our iframe and subscribe to postMessage events.
+ // They'll tell us when the underlying page is done loading
+ // or when user clicks on start/stop buttons.
+
+ doc.getElementById("profiler-report").appendChild(this.iframe);
+ win.addEventListener("message", function (event) {
+ if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
+ return;
+ }
+
+ switch (event.data.status) {
+ case "loaded":
+ this.isReady = true;
+ this.emit("ready");
+ break;
+ case "start":
+ this.start();
+ break;
+ case "stop":
+ this.stop();
+ break;
+ case "disabled":
+ this.emit("disabled");
+ break;
+ case "enabled":
+ this.emit("enabled");
+ break;
+ case "displaysource":
+ this.panel.displaySource(event.data.data);
+ }
+ }.bind(this));
+}
+
+ProfileUI.prototype = {
+ /**
+ * Returns a contentWindow of the iframe pointing to Cleopatra
+ * if it exists and can be accessed. Otherwise returns null.
+ */
+ get contentWindow() {
+ if (!this.iframe) {
+ return null;
+ }
+
+ try {
+ return this.iframe.contentWindow;
+ } catch (err) {
+ return null;
+ }
+ },
+
+ show: function PUI_show() {
+ this.iframe.removeAttribute("hidden");
+ },
+
+ hide: function PUI_hide() {
+ this.iframe.setAttribute("hidden", true);
+ },
+
+ /**
+ * Send raw profiling data to Cleopatra for parsing.
+ *
+ * @param object data
+ * Raw profiling data from the SPS Profiler.
+ * @param function onParsed
+ * A callback to be called when Cleopatra finishes
+ * parsing and displaying results.
+ *
+ */
+ parse: function PUI_parse(data, onParsed) {
+ if (!this.isReady) {
+ return void this.on("ready", this.parse.bind(this, data, onParsed));
+ }
+
+ this.message({ task: "receiveProfileData", rawProfile: data }).then(() => {
+ let poll = () => {
+ let wait = this.panel.window.setTimeout.bind(null, poll, 100);
+ let trail = this.contentWindow.gBreadcrumbTrail;
+
+ if (!trail) {
+ return wait();
+ }
+
+ if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
+ return wait();
+ }
+
+ onParsed();
+ };
+
+ poll();
+ });
+ },
+
+ /**
+ * Start profiling and, once started, notify the underlying page
+ * so that it could update the UI. Also, once started, we add a
+ * star to the profile name to indicate which profile is currently
+ * running.
+ *
+ * @param function startFn
+ * A function to use instead of the default
+ * this.panel.startProfiling. Useful when you
+ * need mark panel as started after the profiler
+ * has been started elsewhere. It must take two
+ * params and call the second one.
+ */
+ start: function PUI_start(startFn) {
+ if (this.isStarted || this.isFinished) {
+ return;
+ }
+
+ startFn = startFn || this.panel.startProfiling.bind(this.panel);
+ startFn(this.name, () => {
+ this.isStarted = true;
+ this.panel.sidebar.setProfileState(this, PROFILE_RUNNING);
+ this.panel.broadcast(this.uid, {task: "onStarted"}); // Do we really need this?
+ this.emit("started");
+ });
+ },
+
+ /**
+ * Stop profiling and, once stopped, notify the underlying page so
+ * that it could update the UI and remove a star from the profile
+ * name.
+ *
+ * @param function stopFn
+ * A function to use instead of the default
+ * this.panel.stopProfiling. Useful when you
+ * need mark panel as stopped after the profiler
+ * has been stopped elsewhere. It must take two
+ * params and call the second one.
+ */
+ stop: function PUI_stop(stopFn) {
+ if (!this.isStarted || this.isFinished) {
+ return;
+ }
+
+ stopFn = stopFn || this.panel.stopProfiling.bind(this.panel);
+ stopFn(this.name, () => {
+ this.isStarted = false;
+ this.isFinished = true;
+ this.panel.sidebar.setProfileState(this, PROFILE_COMPLETED);
+ this.panel.broadcast(this.uid, {task: "onStopped"});
+ this.emit("stopped");
+ });
+ },
+
+ /**
+ * Send a message to Cleopatra instance. If a message cannot be
+ * sent, this method queues it for later.
+ *
+ * @param object data JSON data to send (must be serializable)
+ * @return promise
+ */
+ message: function PIU_message(data) {
+ let deferred = Promise.defer();
+ let win = this.contentWindow;
+ data = JSON.stringify(data);
+
+ if (win) {
+ win.postMessage(data, "*");
+ deferred.resolve();
+ } else {
+ this.messages.push({ data: data, onSuccess: () => deferred.resolve() });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Send all queued messages (see this.message for more info)
+ */
+ flushMessages: function PIU_flushMessages() {
+ if (!this.contentWindow) {
+ return;
+ }
+
+ let msg;
+ while (msg = this.messages.shift()) {
+ this.contentWindow.postMessage(msg.data, "*");
+ msg.onSuccess();
+ }
+ },
+
+ /**
+ * Destroys the ProfileUI instance.
+ */
+ destroy: function PUI_destroy() {
+ this.isReady = null
+ this.panel = null;
+ this.uid = null;
+ this.iframe = null;
+ this.messages = null;
+ }
+};
+
+function SidebarView(el) {
+ EventEmitter.decorate(this);
+ this.widget = new SideMenuWidget(el);
+}
+
+SidebarView.prototype = Heritage.extend(WidgetMethods, {
+ getItemByProfile: function (profile) {
+ return this.getItemForPredicate(item => item.attachment.uid === profile.uid);
+ },
+
+ setProfileState: function (profile, state) {
+ let item = this.getItemByProfile(profile);
+ let label = item.target.querySelector(".profiler-sidebar-item > span");
+
+ switch (state) {
+ case PROFILE_IDLE:
+ label.textContent = L10N.getStr("profiler.stateIdle");
+ break;
+ case PROFILE_RUNNING:
+ label.textContent = L10N.getStr("profiler.stateRunning");
+ break;
+ case PROFILE_COMPLETED:
+ label.textContent = L10N.getStr("profiler.stateCompleted");
+ break;
+ default: // Wrong state, do nothing.
+ return;
+ }
+
+ item.attachment.state = state;
+ this.emit("stateChanged", item);
+ }
+});
+
+/**
+ * Profiler panel. It is responsible for creating and managing
+ * different profile instances (see ProfileUI).
+ *
+ * ProfilerPanel is an event emitter. It can emit the following
+ * events:
+ *
+ * - ready: after the panel is done loading everything,
+ * including the default profile instance.
+ * - started: after the panel successfuly starts our SPS
+ * profiler.
+ * - stopped: after the panel successfuly stops our SPS
+ * profiler and is ready to hand over profiling
+ * data
+ * - parsed: after Cleopatra finishes parsing profiling
+ * data.
+ * - destroyed: after the panel cleans up after itself and
+ * is ready to be destroyed.
+ *
+ * The following events are used mainly by tests to prevent
+ * accidential oranges:
+ *
+ * - profileCreated: after a new profile is created.
+ * - profileSwitched: after user switches to a different
+ * profile.
+ */
+function ProfilerPanel(frame, toolbox) {
+ this.isReady = false;
+ this.window = frame.window;
+ this.document = frame.document;
+ this.target = toolbox.target;
+
+ this.profiles = new Map();
+ this._uid = 0;
+ this._msgQueue = {};
+
+ EventEmitter.decorate(this);
+}
+
+ProfilerPanel.prototype = {
+ isReady: null,
+ window: null,
+ document: null,
+ target: null,
+ controller: null,
+ profiles: null,
+ sidebar: null,
+
+ _uid: null,
+ _activeUid: null,
+ _runningUid: null,
+ _browserWin: null,
+ _msgQueue: null,
+
+ get activeProfile() {
+ return this.profiles.get(this._activeUid);
+ },
+
+ set activeProfile(profile) {
+ if (this._activeUid === profile.uid)
+ return;
+
+ if (this.activeProfile)
+ this.activeProfile.hide();
+
+ this._activeUid = profile.uid;
+ profile.show();
+ },
+
+ get browserWindow() {
+ if (this._browserWin) {
+ return this._browserWin;
+ }
+
+ let win = this.window.top;
+ let type = win.document.documentElement.getAttribute("windowtype");
+
+ if (type !== "navigator:browser") {
+ win = Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ return this._browserWin = win;
+ },
+
+ /**
+ * Open a debug connection and, on success, switch to the newly created
+ * profile.
+ *
+ * @return Promise
+ */
+ open: function PP_open() {
+ // Local profiling needs to make the target remote.
+ let target = this.target;
+ let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
+
+ return promise
+ .then((target) => {
+ let deferred = Promise.defer();
+
+ this.controller = new ProfilerController(this.target);
+ this.sidebar = new SidebarView(this.document.querySelector("#profiles-list"));
+ this.sidebar.widget.addEventListener("select", (ev) => {
+ if (!ev.detail)
+ return;
+
+ let profile = this.profiles.get(ev.detail.attachment.uid);
+ this.activeProfile = profile;
+
+ if (profile.isReady) {
+ profile.flushMessages();
+ return void this.emit("profileSwitched", profile.uid);
+ }
+
+ profile.once("ready", () => {
+ profile.flushMessages();
+ this.emit("profileSwitched", profile.uid);
+ });
+ });
+
+ this.controller.connect(() => {
+ let create = this.document.getElementById("profiler-create");
+ create.addEventListener("click", () => this.createProfile(), false);
+ create.removeAttribute("disabled");
+
+ let profile = this.createProfile();
+ let onSwitch = (_, uid) => {
+ if (profile.uid !== uid)
+ return;
+
+ this.off("profileSwitched", onSwitch);
+ this.isReady = true;
+ this.emit("ready");
+
+ deferred.resolve(this);
+ };
+
+ this.on("profileSwitched", onSwitch);
+ this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
+ });
+
+ return deferred.promise;
+ })
+ .then(null, (reason) =>
+ Cu.reportError("ProfilePanel open failed: " + reason.message));
+ },
+
+ /**
+ * Creates a new profile instance (see ProfileUI) and
+ * adds an appropriate item to the sidebar. Note that
+ * this method doesn't automatically switch user to
+ * the newly created profile, they have do to switch
+ * explicitly.
+ *
+ * @param string name
+ * (optional) name of the new profile
+ *
+ * @return ProfilerPanel
+ */
+ createProfile: function PP_createProfile(name) {
+ if (name && this.getProfileByName(name)) {
+ return this.getProfileByName(name);
+ }
+
+ let uid = ++this._uid;
+
+ // If profile is anonymous, increase its UID until we get
+ // to the unused name. This way if someone manually creates
+ // a profile named say 'Profile 2' we won't create a dup
+ // with the same name. We will just skip over uid 2.
+
+ if (!name) {
+ name = L10N.getFormatStr("profiler.profileName", [uid]);
+ while (this.getProfileByName(name)) {
+ uid = ++this._uid;
+ name = L10N.getFormatStr("profiler.profileName", [uid]);
+ }
+ }
+
+ let box = this.document.createElement("vbox");
+ box.className = "profiler-sidebar-item";
+ box.id = "profile-" + uid;
+ let h3 = this.document.createElement("h3");
+ h3.textContent = name;
+ let span = this.document.createElement("span");
+ span.textContent = L10N.getStr("profiler.stateIdle");
+ box.appendChild(h3);
+ box.appendChild(span);
+
+ this.sidebar.push([box], { attachment: { uid: uid, name: name, state: PROFILE_IDLE } });
+
+ let profile = new ProfileUI(uid, name, this);
+ this.profiles.set(uid, profile);
+
+ this.emit("profileCreated", uid);
+ return profile;
+ },
+
+ /**
+ * Start collecting profile data.
+ *
+ * @param function onStart
+ * A function to call once we get the message
+ * that profiling had been successfuly started.
+ */
+ startProfiling: function PP_startProfiling(name, onStart) {
+ this.controller.start(name, (err) => {
+ if (err) {
+ return void Cu.reportError("ProfilerController.start: " + err.message);
+ }
+
+ onStart();
+ this.emit("started");
+ });
+ },
+
+ /**
+ * Stop collecting profile data and send it to Cleopatra
+ * for parsing.
+ *
+ * @param function onStop
+ * A function to call once we get the message
+ * that profiling had been successfuly stopped.
+ */
+ stopProfiling: function PP_stopProfiling(name, onStop) {
+ this.controller.isActive(function (err, isActive) {
+ if (err) {
+ Cu.reportError("ProfilerController.isActive: " + err.message);
+ return;
+ }
+
+ if (!isActive) {
+ return;
+ }
+
+ this.controller.stop(name, function (err, data) {
+ if (err) {
+ Cu.reportError("ProfilerController.stop: " + err.message);
+ return;
+ }
+
+ this.activeProfile.data = data;
+ this.activeProfile.parse(data, function onParsed() {
+ this.emit("parsed");
+ }.bind(this));
+
+ onStop();
+ this.emit("stopped", data);
+ }.bind(this));
+ }.bind(this));
+ },
+
+ /**
+ * Lookup an individual profile by its name.
+ *
+ * @param string name name of the profile
+ * @return profile object or null
+ */
+ getProfileByName: function PP_getProfileByName(name) {
+ if (!this.profiles) {
+ return null;
+ }
+
+ for (let [ uid, profile ] of this.profiles) {
+ if (profile.name === name) {
+ return profile;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Lookup an individual profile by its UID.
+ *
+ * @param number uid UID of the profile
+ * @return profile object or null
+ */
+ getProfileByUID: function PP_getProfileByUID(uid) {
+ if (!this.profiles) {
+ return null;
+ }
+
+ return this.profiles.get(uid) || null;
+ },
+
+ /**
+ * Iterates over each available profile and calls
+ * a callback with it as a parameter.
+ *
+ * @param function cb a callback to call
+ */
+ eachProfile: function PP_eachProfile(cb) {
+ let uid = this._uid;
+
+ if (!this.profiles) {
+ return;
+ }
+
+ while (uid >= 0) {
+ if (this.profiles.has(uid)) {
+ cb(this.profiles.get(uid));
+ }
+
+ uid -= 1;
+ }
+ },
+
+ /**
+ * Broadcast messages to all Cleopatra instances.
+ *
+ * @param number target
+ * UID of the recepient profile. All profiles will receive the message
+ * but the profile specified by 'target' will have a special property,
+ * isCurrent, set to true.
+ * @param object data
+ * An object with a property 'task' that will be sent over to Cleopatra.
+ */
+ broadcast: function PP_broadcast(target, data) {
+ if (!this.profiles) {
+ return;
+ }
+
+ if (data.task === "onStarted") {
+ this._runningUid = target;
+ } else {
+ this._runningUid = null;
+ }
+
+ this.eachProfile((profile) => {
+ profile.message({
+ uid: target,
+ isCurrent: target === profile.uid,
+ task: data.task
+ });
+ });
+ },
+
+ /**
+ * Open file specified in data in either a debugger or view-source.
+ *
+ * @param object data
+ * An object describing the file. It must have three properties:
+ * - uri
+ * - line
+ * - isChrome (chrome files are opened via view-source)
+ */
+ displaySource: function PP_displaySource(data, onOpen=function() {}) {
+ let win = this.window;
+ let panelWin, timeout;
+
+ function onSourceShown(event) {
+ if (event.detail.url !== data.uri) {
+ return;
+ }
+
+ panelWin.removeEventListener("Debugger:SourceShown", onSourceShown, false);
+ panelWin.editor.setCaretPosition(data.line - 1);
+ onOpen();
+ }
+
+ if (data.isChrome) {
+ return void this.browserWindow.gViewSourceUtils.
+ viewSource(data.uri, null, this.document, data.line);
+ }
+
+ gDevTools.showToolbox(this.target, "jsdebugger").then(function (toolbox) {
+ let dbg = toolbox.getCurrentPanel();
+ panelWin = dbg.panelWin;
+
+ let view = dbg.panelWin.DebuggerView;
+ if (view.Sources.selectedValue === data.uri) {
+ view.editor.setCaretPosition(data.line - 1);
+ onOpen();
+ return;
+ }
+
+ panelWin.addEventListener("Debugger:SourceShown", onSourceShown, false);
+ panelWin.DebuggerView.Sources.preferredSource = data.uri;
+ }.bind(this));
+ },
+
+ /**
+ * Cleanup.
+ */
+ destroy: function PP_destroy() {
+ if (this.profiles) {
+ let uid = this._uid;
+
+ while (uid >= 0) {
+ if (this.profiles.has(uid)) {
+ this.profiles.get(uid).destroy();
+ this.profiles.delete(uid);
+ }
+ uid -= 1;
+ }
+ }
+
+ if (this.controller) {
+ this.controller.destroy();
+ }
+
+ this.isReady = null;
+ this.window = null;
+ this.document = null;
+ this.target = null;
+ this.controller = null;
+ this.profiles = null;
+ this._uid = null;
+ this._activeUid = null;
+
+ this.emit("destroyed");
+ }
+};
diff --git a/browser/devtools/profiler/cleopatra/cleopatra.html b/browser/devtools/profiler/cleopatra/cleopatra.html
new file mode 100644
index 000000000..afc5bfa9f
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/cleopatra.html
@@ -0,0 +1,28 @@
+<!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>Firefox Profiler (SPS)</title>
+ <meta charset="utf-8">
+
+ <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/ui.css">
+ <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/tree.css">
+ <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/devtools.css">
+
+ <script src="profiler/cleopatra/js/strings.js"></script>
+ <script src="profiler/cleopatra/js/parser.js"></script>
+ <script src="profiler/cleopatra/js/tree.js"></script>
+ <script src="profiler/cleopatra/js/ui.js"></script>
+ <script src="profiler/cleopatra/js/ProgressReporter.js"></script>
+ <script src="profiler/cleopatra/js/devtools.js"></script>
+ </head>
+
+ <body onload="notifyParent('loaded');">
+ <script>
+ initUI();
+ </script>
+ </body>
+</html>
diff --git a/browser/devtools/profiler/cleopatra/css/devtools.css b/browser/devtools/profiler/cleopatra/css/devtools.css
new file mode 100644
index 000000000..8eb0bc119
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/devtools.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/. */
+
+#mainarea > .controlPane {
+ font-size: 120%;
+ padding-top: 75px;
+ text-align: center;
+}
+
+#stopWrapper {
+ display: none;
+}
+
+#profilerMessage {
+ color: #999;
+ display: none;
+}
+
+/* De-emphasize chrome functions */
+.resourceIcon[data-resource^=otherhost_] + .functionName {
+ color: #999;
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/cleopatra/css/tree.css b/browser/devtools/profiler/cleopatra/css/tree.css
new file mode 100644
index 000000000..3b092a976
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/tree.css
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.treeViewContainer {
+ -moz-user-select: none;
+ user-select: none;
+ cursor: default;
+ line-height: 16px;
+ height: 100%;
+ outline: none; /* override the browser's focus styling */
+ position: relative;
+}
+
+.treeHeader {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 16px;
+ margin: 0;
+ padding: 0;
+}
+
+.treeColumnHeader {
+ position: absolute;
+ display: block;
+ background: linear-gradient(#FFF 45%, #EEE 60%);
+ margin: 0;
+ padding: 0;
+ top: 0;
+ height: 15px;
+ line-height: 15px;
+ border: 0 solid #CCC;
+ border-bottom-width: 1px;
+ text-indent: 5px;
+}
+
+.treeColumnHeader:not(:last-child) {
+ border-right-width: 1px;
+}
+
+.treeColumnHeader0 {
+ left: 0;
+ width: 86px;
+}
+
+.treeColumnHeader1 {
+ left: 99px;
+ width: 35px;
+}
+
+.treeColumnHeader0,
+.treeColumnHeader1 {
+ text-align: right;
+ padding-right: 12px;
+}
+
+.treeColumnHeader2 {
+ left: 147px;
+ right: 0;
+}
+
+.treeViewVerticalScrollbox {
+ position: absolute;
+ top: 16px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.treeViewNode,
+.treeViewHorizontalScrollbox {
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.treeViewNode {
+ min-width: -moz-min-content;
+ white-space: nowrap;
+}
+
+.treeViewHorizontalScrollbox {
+ padding-left: 150px;
+ overflow: hidden;
+}
+
+.treeViewVerticalScrollbox,
+.treeViewHorizontalScrollbox {
+ background: linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+ background-size: 100px 32px;
+}
+
+.leftColumnBackground {
+ background: linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
+ linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+ background-size: auto, 100px 32px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 146px;
+ min-height: 100%;
+ border-right: 1px solid #CCC;
+}
+
+.sampleCount,
+.samplePercentage,
+.selfSampleCount {
+ position: absolute;
+ text-align: right;
+}
+
+.sampleCount {
+ left: 2px;
+ width: 50px;
+}
+
+.samplePercentage {
+ left: 55px;
+ width: 40px;
+}
+
+.selfSampleCount {
+ left: 98px;
+ width: 45px;
+ padding-right: 2px;
+ border: solid #CCC;
+ border-width: 0 1px;
+}
+
+.libraryName {
+ margin-left: 10px;
+ color: #999;
+}
+
+.treeViewNode > .treeViewNodeList {
+ margin-left: 1em;
+}
+
+.treeViewNode.collapsed > .treeViewNodeList {
+ display: none;
+}
+
+.treeLine {
+ /* extend the selection background almost infinitely to the left */
+ margin-left: -10000px;
+ padding-left: 10000px;
+}
+
+.treeLine.selected {
+ color: black;
+ background-color: -moz-dialog;
+}
+
+.treeLine.selected > .sampleCount {
+ background-color: inherit;
+ margin-left: -2px;
+ padding-left: 2px;
+ padding-right: 95px;
+ margin-right: -95px;
+}
+
+.treeViewContainer:focus .treeLine.selected {
+ color: highlighttext;
+ background-color: highlight;
+}
+
+.treeViewContainer:focus .treeLine.selected > .libraryName {
+ color: #CCC;
+}
+
+.expandCollapseButton,
+.focusCallstackButton {
+ background: none 0 0 no-repeat transparent;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ width: 16px;
+ height: 16px;
+ overflow: hidden;
+ vertical-align: top;
+ color: transparent;
+ font-size: 0;
+}
+
+.expandCollapseButton {
+ background-image: url(../images/treetwisty.svg);
+}
+
+.focusCallstackButton {
+ background-image: url(../images/circlearrow.svg);
+ margin-left: 5px;
+ visibility: hidden;
+}
+
+.expandCollapseButton:active:hover,
+.focusCallstackButton:active:hover {
+ background-position: -16px 0;
+}
+
+.treeViewNode.collapsed > .treeLine > .expandCollapseButton {
+ background-position: 0 -16px;
+}
+
+.treeViewNode.collapsed > .treeLine > .expandCollapseButton:active:hover {
+ background-position: -16px -16px;
+}
+
+.treeViewContainer:focus .treeLine.selected > .expandCollapseButton,
+.treeViewContainer:focus .treeLine.selected > .focusCallstackButton {
+ background-position: -32px 0;
+}
+
+.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton {
+ background-position: -32px -16px;
+}
+
+.treeViewContainer:focus .treeLine.selected > .expandCollapseButton:active:hover,
+.treeViewContainer:focus .treeLine.selected > .focusCallstackButton:active:hover {
+ background-position: -48px 0;
+}
+
+.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton:active:hover {
+ background-position: -48px -16px;
+}
+
+.treeViewNode.leaf > * > .expandCollapseButton {
+ visibility: hidden;
+}
+
+.treeLine:hover > .focusCallstackButton {
+ visibility: visible;
+}
diff --git a/browser/devtools/profiler/cleopatra/css/ui.css b/browser/devtools/profiler/cleopatra/css/ui.css
new file mode 100644
index 000000000..a0e5bb6be
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/ui.css
@@ -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/. */
+
+body {
+ margin: 0;
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 11px;
+}
+#mainarea {
+ position: absolute;
+ top: 0;
+ left: 0px;
+ bottom: 0;
+ right: 0;
+}
+.finishedProfilePane,
+.finishedProfilePaneBackgroundCover,
+.profileEntryPane,
+.profileProgressPane {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
+.profileEntryPane {
+ overflow: auto;
+}
+.profileEntryPane,
+.profileProgressPane {
+ padding: 20px;
+ background-color: rgb(229,229,229);
+ background-image: url(../images/noise.png),
+ linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
+ text-shadow: rgba(255, 255, 255, 0.4) 0 1px;
+}
+.profileEntryPane h1 {
+ margin-top: 0;
+ font-size: 13px;
+ font-weight: normal;
+}
+.profileEntryPane input[type="file"] {
+ margin-bottom: 1em;
+}
+.profileProgressPane a {
+ position: absolute;
+ top: 30%;
+ left: 30%;
+ width: 40%;
+ height: 16px;
+}
+.profileProgressPane progress {
+ position: absolute;
+ top: 40%;
+ left: 30%;
+ width: 40%;
+ height: 16px;
+}
+.finishedProfilePaneBackgroundCover {
+ animation: darken 300ms cubic-bezier(0, 0, 1, 0);
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.finishedProfilePane {
+ animation: appear 300ms ease-out;
+}
+
+@keyframes darken {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes appear {
+ from {
+ transform: scale(0.3);
+ opacity: 0;
+ pointer-events: none;
+ }
+ to {
+ transform: scale(1);
+ opacity: 1;
+ pointer-events: auto;
+ }
+}
+.breadcrumbTrail {
+ top: 0;
+ right: 0;
+ height: 29px;
+ left: 0;
+ background: linear-gradient(#FFF 50%, #F3F3F3 55%);
+ border-bottom: 1px solid #CCC;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
+.breadcrumbTrailItem {
+ background: linear-gradient(#FFF 50%, #F3F3F3 55%);
+ display: block;
+ margin: 0;
+ padding: 0;
+ float: left;
+ line-height: 29px;
+ padding: 0 10px;
+ font-size: 12px;
+ -moz-user-select: none;
+ user-select: none;
+ cursor: default;
+ border-right: 1px solid #CCC;
+ max-width: 250px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ position: relative;
+}
+@keyframes slide-out {
+ from {
+ margin-left: -270px;
+ opacity: 0;
+ }
+ to {
+ margin-left: 0;
+ opacity: 1;
+ }
+}
+@keyframes slide-out {
+ from {
+ margin-left: -270px;
+ opacity: 0;
+ }
+ to {
+ margin-left: 0;
+ opacity: 1;
+ }
+}
+.breadcrumbTrailItem:not(:first-child) {
+ animation: slide-out;
+ animation-duration: 400ms;
+ animation-timing-function: ease-out;
+}
+.breadcrumbTrailItem.selected {
+ background: linear-gradient(#E5E5E5 50%, #DADADA 55%);
+}
+.breadcrumbTrailItem:not(.selected):active:hover {
+ background: linear-gradient(#F2F2F2 50%, #E6E6E6 55%);
+}
+.breadcrumbTrailItem.deleted {
+ transition: 400ms ease-out;
+ transition-property: opacity, margin-left;
+ opacity: 0;
+ margin-left: -270px;
+}
+.treeContainer {
+ /*For asbolute position child*/
+ position: relative;
+}
+.tree {
+ height: 100%;
+}
+#sampleBar {
+ position: absolute;
+ float: right;
+ left: auto;
+ top: 0;
+ right: 0;
+ height: 100%;
+}
+#fileList {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 360px;
+ width: 199px;
+ overflow: auto;
+ padding: 0;
+ margin: 0;
+ background: #DBDFE7;
+ border-right: 1px solid #BBB;
+ cursor: pointer;
+}
+#infoBar dl {
+ margin: 0;
+}
+#infoBar dt,
+#infoBar dd {
+ display: inline;
+}
+#infoBar dt {
+ font-weight: bold;
+}
+#infoBar dt::after {
+ content: " ";
+ white-space: pre;
+}
+#infoBar dd {
+ margin-left: 0;
+}
+#infoBar dd::after {
+ content: "\a";
+ white-space:pre;
+}
+.sideBar {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 200px;
+ height: 480px;
+ overflow: auto;
+ padding: 3px;
+ background: #EEE;
+ border-top: 1px solid #BBB;
+ border-right: 1px solid #BBB;
+}
+.sideBar h2 {
+ font-size: 1em;
+ padding: 1px 3px;
+ margin: 3px -3px;
+ background: rgba(255, 255, 255, 0.6);
+ border: solid #CCC;
+ border-width: 1px 0;
+}
+.sideBar h2:first-child {
+ margin-top: -4px;
+}
+.sideBar ul {
+ margin: 2px 0;
+ padding-left: 18px;
+}
+.pluginview {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ background-color: white;
+}
+.pluginviewIFrame {
+ border-style: none;
+ width: 100%;
+ height: 100%;
+}
+.histogram {
+ position: relative;
+ height: 60px;
+ right: 0;
+ left: 0;
+ border-bottom: 1px solid #CCC;
+ background: linear-gradient(#EEE, #CCC);
+}
+.histogramHilite {
+ position: absolute;
+ pointer-events: none;
+}
+.histogramHilite:not(.collapsed) {
+ background: rgba(150, 150, 150, 0.5);
+}
+.histogramMouseMarker {
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ width: 1px;
+ height: 100%;
+}
+.histogramMouseMarker:not(.collapsed) {
+ background: rgba(0, 0, 150, 0.7);
+}
+#iconbox {
+ display: none;
+}
+#filter, #showall {
+ cursor: pointer;
+}
+.markers {
+ display: none;
+}
+.hidden {
+ display: none !important;
+}
+.fileListItem {
+ display: block;
+ margin: 0;
+ padding: 0;
+ height: 40px;
+ text-indent: 8px;
+}
+.fileListItem.selected {
+ background: linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
+ color: #FFF;
+ text-shadow: 0 1px rgba(0, 0, 0, 0.3);
+}
+.fileListItemTitle {
+ display: block;
+ padding-top: 6px;
+ font-size: 12px;
+}
+.fileListItemDescription {
+ display: block;
+ line-height: 15px;
+ font-size: 9px;
+}
+.busyCover {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+ background: rgba(120, 120, 120, 0.2);
+ transition: 200ms ease-in-out;
+ transition-property: visibility, opacity;
+}
+.busyCover.busy {
+ visibility: visible;
+ opacity: 1;
+}
+.busyCover::before {
+ content: url(../images/throbber.svg);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -12px;
+}
+label {
+ -moz-user-select: none;
+}
+.videoPane {
+ background-color: white;
+ width: 100%;
+}
+.video {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
diff --git a/browser/devtools/profiler/cleopatra/images/circlearrow.svg b/browser/devtools/profiler/cleopatra/images/circlearrow.svg
new file mode 100644
index 000000000..70a7c54ef
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/circlearrow.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="64" height="16" viewBox="0 0 64 16">
+ <defs>
+ <mask id="arrowInCircle" maskContentUnits="userSpaceOnUse">
+ <circle cx="8" cy="8" r="6" fill="white"/>
+ <rect x="4.5" y="7" width="3.5" height="2" fill="black"/>
+ <polyline points="8 4 12 8 8 12" fill="black"/>
+ </mask>
+ </defs>
+ <g fill="#888">
+ <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+ </g>
+ <g fill="#444" transform="translate(16,0)">
+ <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+ </g>
+ <g fill="#FFF" transform="translate(32,0)">
+ <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+ </g>
+ <g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
+ <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+ </g>
+</svg>
diff --git a/browser/devtools/profiler/cleopatra/images/noise.png b/browser/devtools/profiler/cleopatra/images/noise.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/noise.png
diff --git a/browser/devtools/profiler/cleopatra/images/throbber.svg b/browser/devtools/profiler/cleopatra/images/throbber.svg
new file mode 100644
index 000000000..d185ef8d5
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/throbber.svg
@@ -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/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ 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/browser/devtools/profiler/cleopatra/images/treetwisty.svg b/browser/devtools/profiler/cleopatra/images/treetwisty.svg
new file mode 100644
index 000000000..b51aa4bd5
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/treetwisty.svg
@@ -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/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="64" height="32" viewBox="0 0 64 32">
+ <g fill="#888">
+ <polyline points="3 4 12 4 7.5 12"/>
+ <g transform="translate(0,16)">
+ <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+ </g>
+ </g>
+ <g fill="#444" transform="translate(16,0)">
+ <polyline points="3 4 12 4 7.5 12"/>
+ <g transform="translate(0,16)">
+ <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+ </g>
+ </g>
+ <g fill="#FFF" transform="translate(32,0)">
+ <polyline points="3 4 12 4 7.5 12"/>
+ <g transform="translate(0,16)">
+ <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+ </g>
+ </g>
+ <g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
+ <polyline points="3 4 12 4 7.5 12"/>
+ <g transform="translate(0,16)">
+ <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+ </g>
+ </g>
+</svg>
diff --git a/browser/devtools/profiler/cleopatra/js/ProgressReporter.js b/browser/devtools/profiler/cleopatra/js/ProgressReporter.js
new file mode 100644
index 000000000..f885aecd5
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/ProgressReporter.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/. */
+
+/**
+ * ProgressReporter
+ *
+ * This class is used by long-winded tasks to report progress to observers.
+ * If a task has subtasks that want to report their own progress, these
+ * subtasks can have their own progress reporters which are hooked up to the
+ * parent progress reporter, resulting in a tree structure. A parent progress
+ * reporter will calculate its progress value as a weighted sum of its
+ * subreporters' progress values.
+ *
+ * A progress reporter has a state, an action, and a progress value.
+ *
+ * - state is one of STATE_WAITING, STATE_DOING and STATE_FINISHED.
+ * - action is a string that describes the current task.
+ * - progress is the progress value as a number between 0 and 1, or NaN if
+ * indeterminate.
+ *
+ * A progress reporter starts out in the WAITING state. The DOING state is
+ * entered with the begin method which also sets the action. While the task is
+ * executing, the progress value can be updated with the setProgress method.
+ * When a task has finished, it can call the finish method which is just a
+ * shorthand for setProgress(1); this will set the state to FINISHED.
+ *
+ * Progress observers can be added with the addListener method which takes a
+ * function callback. Whenever the progress value or state change, all
+ * listener callbacks will be called with the progress reporter object. The
+ * observer can get state, progress value and action by calling the getter
+ * methods getState(), getProgress() and getAction().
+ *
+ * Creating child progress reporters for subtasks can be done with the
+ * addSubreporter(s) methods. If a progress reporter has subreporters, normal
+ * progress report functions (setProgress and finish) can no longer be called.
+ * Instead, the parent reporter will listen to progress changes on its
+ * subreporters and update its state automatically, and then notify its own
+ * listeners.
+ * When adding a subreporter, you are expected to provide an estimated
+ * duration for the subtask. This value will be used as a weight when
+ * calculating the progress of the parent reporter.
+ */
+
+const gDebugExpectedDurations = false;
+
+function ProgressReporter() {
+ this._observers = [];
+ this._subreporters = [];
+ this._subreporterExpectedDurationsSum = 0;
+ this._progress = 0;
+ this._state = ProgressReporter.STATE_WAITING;
+ this._action = "";
+}
+
+ProgressReporter.STATE_WAITING = 0;
+ProgressReporter.STATE_DOING = 1;
+ProgressReporter.STATE_FINISHED = 2;
+
+ProgressReporter.prototype = {
+ getProgress: function () {
+ return this._progress;
+ },
+ getState: function () {
+ return this._state;
+ },
+ setAction: function (action) {
+ this._action = action;
+ this._reportProgress();
+ },
+ getAction: function () {
+ switch (this._state) {
+ case ProgressReporter.STATE_WAITING:
+ return "Waiting for preceding tasks to finish...";
+ case ProgressReporter.STATE_DOING:
+ return this._action;
+ case ProgressReporter.STATE_FINISHED:
+ return "Finished.";
+ default:
+ throw "Broken state";
+ }
+ },
+ addListener: function (callback) {
+ this._observers.push(callback);
+ },
+ addSubreporter: function (expectedDuration) {
+ this._subreporterExpectedDurationsSum += expectedDuration;
+ var subreporter = new ProgressReporter();
+ var self = this;
+ subreporter.addListener(function (progress) {
+ self._recalculateProgressFromSubreporters();
+ self._recalculateStateAndActionFromSubreporters();
+ self._reportProgress();
+ });
+ this._subreporters.push({ expectedDuration: expectedDuration, reporter: subreporter });
+ return subreporter;
+ },
+ addSubreporters: function (expectedDurations) {
+ var reporters = {};
+ for (var key in expectedDurations) {
+ reporters[key] = this.addSubreporter(expectedDurations[key]);
+ }
+ return reporters;
+ },
+ begin: function (action) {
+ this._startTime = Date.now();
+ this._state = ProgressReporter.STATE_DOING;
+ this._action = action;
+ this._reportProgress();
+ },
+ setProgress: function (progress) {
+ if (this._subreporters.length > 0)
+ throw "Can't call setProgress on a progress reporter with subreporters";
+ if (progress != this._progress &&
+ (progress == 1 ||
+ (isNaN(progress) != isNaN(this._progress)) ||
+ (progress - this._progress >= 0.01))) {
+ this._progress = progress;
+ if (progress == 1)
+ this._transitionToFinished();
+ this._reportProgress();
+ }
+ },
+ finish: function () {
+ this.setProgress(1);
+ },
+ _recalculateProgressFromSubreporters: function () {
+ if (this._subreporters.length == 0)
+ throw "Can't _recalculateProgressFromSubreporters on a progress reporter without any subreporters";
+ this._progress = 0;
+ for (var i = 0; i < this._subreporters.length; i++) {
+ var expectedDuration = this._subreporters[i].expectedDuration;
+ var reporter = this._subreporters[i].reporter;
+ this._progress += reporter.getProgress() * expectedDuration / this._subreporterExpectedDurationsSum;
+ }
+ },
+ _recalculateStateAndActionFromSubreporters: function () {
+ if (this._subreporters.length == 0)
+ throw "Can't _recalculateStateAndActionFromSubreporters on a progress reporter without any subreporters";
+ var actions = [];
+ var allWaiting = true;
+ var allFinished = true;
+ for (var i = 0; i < this._subreporters.length; i++) {
+ var expectedDuration = this._subreporters[i].expectedDuration;
+ var reporter = this._subreporters[i].reporter;
+ var state = reporter.getState();
+ if (state != ProgressReporter.STATE_WAITING)
+ allWaiting = false;
+ if (state != ProgressReporter.STATE_FINISHED)
+ allFinished = false;
+ if (state == ProgressReporter.STATE_DOING)
+ actions.push(reporter.getAction());
+ }
+ if (allFinished) {
+ this._transitionToFinished();
+ } else if (!allWaiting) {
+ this._state = ProgressReporter.STATE_DOING;
+ if (actions.length == 0) {
+ this._action = "About to start next task..."
+ } else {
+ this._action = actions.join("\n");
+ }
+ }
+ },
+ _transitionToFinished: function () {
+ this._state = ProgressReporter.STATE_FINISHED;
+
+ if (gDebugExpectedDurations) {
+ this._realDuration = Date.now() - this._startTime;
+ if (this._subreporters.length) {
+ for (var i = 0; i < this._subreporters.length; i++) {
+ var expectedDuration = this._subreporters[i].expectedDuration;
+ var reporter = this._subreporters[i].reporter;
+ var realDuration = reporter._realDuration;
+ dump("For reporter with expectedDuration " + expectedDuration + ", real duration was " + realDuration + "\n");
+ }
+ }
+ }
+ },
+ _reportProgress: function () {
+ for (var i = 0; i < this._observers.length; i++) {
+ this._observers[i](this);
+ }
+ },
+};
diff --git a/browser/devtools/profiler/cleopatra/js/devtools.js b/browser/devtools/profiler/cleopatra/js/devtools.js
new file mode 100644
index 000000000..7a80d517b
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/devtools.js
@@ -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/. */
+
+var gInstanceUID;
+
+/**
+ * Sends a message to the parent window with a status
+ * update.
+ *
+ * @param string status
+ * Status to send to the parent page:
+ * - loaded, when page is loaded.
+ * - start, when user wants to start profiling.
+ * - stop, when user wants to stop profiling.
+ * - disabled, when the profiler was disabled
+ * - enabled, when the profiler was enabled
+ * - displaysource, when user wants to display source
+ * @param object data (optional)
+ * Additional data to send to the parent page.
+ */
+function notifyParent(status, data={}) {
+ if (!gInstanceUID) {
+ gInstanceUID = window.location.search.substr(1);
+ }
+
+ window.parent.postMessage({
+ uid: gInstanceUID,
+ status: status,
+ data: data
+ }, "*");
+}
+
+/**
+ * A listener for incoming messages from the parent
+ * page. All incoming messages must be stringified
+ * JSON objects to be compatible with Cleopatra's
+ * format:
+ *
+ * {
+ * task: string,
+ * ...
+ * }
+ *
+ * This listener recognizes two tasks: onStarted and
+ * onStopped.
+ *
+ * @param object event
+ * PostMessage event object.
+ */
+function onParentMessage(event) {
+ var start = document.getElementById("startWrapper");
+ var stop = document.getElementById("stopWrapper");
+ var profilerMessage = document.getElementById("profilerMessage");
+ var msg = JSON.parse(event.data);
+
+ if (msg.task !== "receiveProfileData" && !msg.isCurrent) {
+ return;
+ }
+
+ switch (msg.task) {
+ case "onStarted":
+ start.style.display = "none";
+ start.querySelector("button").removeAttribute("disabled");
+ stop.style.display = "inline";
+ break;
+ case "onStopped":
+ stop.style.display = "none";
+ stop.querySelector("button").removeAttribute("disabled");
+ start.style.display = "inline";
+ break;
+ case "receiveProfileData":
+ loadProfile(JSON.stringify(msg.rawProfile));
+ }
+}
+
+window.addEventListener("message", onParentMessage);
+
+/**
+ * Main entry point. This function initializes Cleopatra
+ * in the light mode and creates all the UI we need.
+ */
+function initUI() {
+ gLightMode = true;
+
+ gFileList = { profileParsingFinished: function () {} };
+ gInfoBar = { display: function () {} };
+
+ var container = document.createElement("div");
+ container.id = "ui";
+
+ gMainArea = document.createElement("div");
+ gMainArea.id = "mainarea";
+
+ container.appendChild(gMainArea);
+ document.body.appendChild(container);
+
+ var startButton = document.createElement("button");
+ startButton.innerHTML = gStrings.getStr("profiler.start");
+ startButton.addEventListener("click", function (event) {
+ event.target.setAttribute("disabled", true);
+ notifyParent("start");
+ }, false);
+
+ var stopButton = document.createElement("button");
+ stopButton.innerHTML = gStrings.getStr("profiler.stop");
+ stopButton.addEventListener("click", function (event) {
+ event.target.setAttribute("disabled", true);
+ notifyParent("stop");
+ }, false);
+
+ var controlPane = document.createElement("div");
+ var startProfiling = gStrings.getFormatStr("profiler.startProfiling",
+ ["<span class='btn'></span>"]);
+ var stopProfiling = gStrings.getFormatStr("profiler.stopProfiling",
+ ["<span class='btn'></span>"]);
+
+ controlPane.className = "controlPane";
+ controlPane.innerHTML =
+ "<p id='startWrapper'>" + startProfiling + "</p>" +
+ "<p id='stopWrapper'>" + stopProfiling + "</p>" +
+ "<p id='profilerMessage'></p>";
+
+ controlPane.querySelector("#startWrapper > span.btn").appendChild(startButton);
+ controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton);
+
+ gMainArea.appendChild(controlPane);
+}
+
+/**
+ * Modified copy of Cleopatra's enterFinishedProfileUI.
+ * By overriding the function we don't need to modify ui.js which helps
+ * with updating from upstream.
+ */
+function enterFinishedProfileUI() {
+ var cover = document.createElement("div");
+ cover.className = "finishedProfilePaneBackgroundCover";
+
+ var pane = document.createElement("table");
+ var rowIndex = 0;
+ var currRow;
+
+ pane.style.width = "100%";
+ pane.style.height = "100%";
+ pane.border = "0";
+ pane.cellPadding = "0";
+ pane.cellSpacing = "0";
+ pane.borderCollapse = "collapse";
+ pane.className = "finishedProfilePane";
+
+ gBreadcrumbTrail = new BreadcrumbTrail();
+ currRow = pane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gBreadcrumbTrail.getContainer());
+
+ gHistogramView = new HistogramView();
+ currRow = pane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gHistogramView.getContainer());
+
+ if (gMeta && gMeta.videoCapture) {
+ gVideoPane = new VideoPane(gMeta.videoCapture);
+ gVideoPane.onTimeChange(videoPaneTimeChange);
+ currRow = pane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gVideoPane.getContainer());
+ }
+
+ var tree = document.createElement("div");
+ tree.className = "treeContainer";
+ tree.style.width = "100%";
+ tree.style.height = "100%";
+
+ gTreeManager = new ProfileTreeManager();
+ gTreeManager.treeView.setColumns([
+ { name: "sampleCount", title: gStrings["Running Time"] },
+ { name: "selfSampleCount", title: gStrings["Self"] },
+ { name: "resource", title: "" }
+ ]);
+
+ currRow = pane.insertRow(rowIndex++);
+ currRow.style.height = "100%";
+
+ var cell = currRow.insertCell(0);
+ cell.appendChild(tree);
+ tree.appendChild(gTreeManager.getContainer());
+
+ gPluginView = new PluginView();
+ tree.appendChild(gPluginView.getContainer());
+
+ gMainArea.appendChild(cover);
+ gMainArea.appendChild(pane);
+
+ var currentBreadcrumb = gSampleFilters;
+ gBreadcrumbTrail.add({
+ title: gStrings["Complete Profile"],
+ enterCallback: function () {
+ gSampleFilters = [];
+ filtersChanged();
+ }
+ });
+
+ if (currentBreadcrumb == null || currentBreadcrumb.length == 0) {
+ gTreeManager.restoreSerializedSelectionSnapshot(gRestoreSelection);
+ viewOptionsChanged();
+ }
+
+ for (var i = 0; i < currentBreadcrumb.length; i++) {
+ var filter = currentBreadcrumb[i];
+ var forceSelection = null;
+ if (gRestoreSelection != null && i == currentBreadcrumb.length - 1) {
+ forceSelection = gRestoreSelection;
+ }
+ switch (filter.type) {
+ case "FocusedFrameSampleFilter":
+ focusOnSymbol(filter.name, filter.symbolName);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "FocusedCallstackPrefixSampleFilter":
+ focusOnCallstack(filter.focusedCallstack, filter.name, false);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "FocusedCallstackPostfixSampleFilter":
+ focusOnCallstack(filter.focusedCallstack, filter.name, true);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "RangeSampleFilter":
+ gHistogramView.selectRange(filter.start, filter.end);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ }
+ }
+
+ toggleJavascriptOnly();
+}
+
+function enterProgressUI() {
+ var pane = document.createElement("div");
+ var label = document.createElement("a");
+ var bar = document.createElement("progress");
+ var string = gStrings.getStr("profiler.loading");
+
+ pane.className = "profileProgressPane";
+ pane.appendChild(label);
+ pane.appendChild(bar);
+
+ var reporter = new ProgressReporter();
+ reporter.addListener(function (rep) {
+ var progress = rep.getProgress();
+
+ if (label.textContent !== string) {
+ label.textContent = string;
+ }
+
+ if (isNaN(progress)) {
+ bar.removeAttribute("value");
+ } else {
+ bar.value = progress;
+ }
+ });
+
+ gMainArea.appendChild(pane);
+ Parser.updateLogSetting();
+
+ return reporter;
+}
diff --git a/browser/devtools/profiler/cleopatra/js/parser.js b/browser/devtools/profiler/cleopatra/js/parser.js
new file mode 100644
index 000000000..eeb9c03bc
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/parser.js
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Array.prototype.clone = function() { return this.slice(0); }
+
+function makeSample(frames, extraInfo, lines) {
+ return {
+ frames: frames,
+ extraInfo: extraInfo,
+ lines: lines
+ };
+}
+
+function cloneSample(sample) {
+ return makeSample(sample.frames.clone(), sample.extraInfo, sample.lines.clone());
+}
+
+function bucketsBySplittingArray(array, maxItemsPerBucket) {
+ var buckets = [];
+ while (buckets.length * maxItemsPerBucket < array.length) {
+ buckets.push(array.slice(buckets.length * maxItemsPerBucket,
+ (buckets.length + 1) * maxItemsPerBucket));
+ }
+ return buckets;
+}
+
+var gParserWorker = new Worker("profiler/cleopatra/js/parserWorker.js");
+gParserWorker.nextRequestID = 0;
+
+function WorkerRequest(worker) {
+ var self = this;
+ this._eventListeners = {};
+ var requestID = worker.nextRequestID++;
+ this._requestID = requestID;
+ this._worker = worker;
+ this._totalReporter = new ProgressReporter();
+ this._totalReporter.addListener(function (reporter) {
+ self._fireEvent("progress", reporter.getProgress(), reporter.getAction());
+ })
+ this._sendChunkReporter = this._totalReporter.addSubreporter(500);
+ this._executeReporter = this._totalReporter.addSubreporter(3000);
+ this._receiveChunkReporter = this._totalReporter.addSubreporter(100);
+ this._totalReporter.begin("Processing task in worker...");
+ var partialResult = null;
+ function onMessageFromWorker(msg) {
+ pendingMessages.push(msg);
+ scheduleMessageProcessing();
+ }
+ function processMessage(msg) {
+ var startTime = Date.now();
+ var data = msg.data;
+ var readTime = Date.now() - startTime;
+ if (readTime > 10)
+ console.log("reading data from worker message: " + readTime + "ms");
+ if (data.requestID == requestID || !data.requestID) {
+ switch(data.type) {
+ case "error":
+ self._sendChunkReporter.setAction("Error in worker: " + data.error);
+ self._executeReporter.setAction("Error in worker: " + data.error);
+ self._receiveChunkReporter.setAction("Error in worker: " + data.error);
+ self._totalReporter.setAction("Error in worker: " + data.error);
+ PROFILERERROR("Error in worker: " + data.error);
+ self._fireEvent("error", data.error);
+ break;
+ case "progress":
+ self._executeReporter.setProgress(data.progress);
+ break;
+ case "finished":
+ self._executeReporter.finish();
+ self._receiveChunkReporter.begin("Receiving data from worker...");
+ self._receiveChunkReporter.finish();
+ self._fireEvent("finished", data.result);
+ worker.removeEventListener("message", onMessageFromWorker);
+ break;
+ case "finishedStart":
+ partialResult = null;
+ self._totalReceiveChunks = data.numChunks;
+ self._gotReceiveChunks = 0;
+ self._executeReporter.finish();
+ self._receiveChunkReporter.begin("Receiving data from worker...");
+ break;
+ case "finishedChunk":
+ partialResult = partialResult ? partialResult.concat(data.chunk) : data.chunk;
+ var chunkIndex = self._gotReceiveChunks++;
+ self._receiveChunkReporter.setProgress((chunkIndex + 1) / self._totalReceiveChunks);
+ break;
+ case "finishedEnd":
+ self._receiveChunkReporter.finish();
+ self._fireEvent("finished", partialResult);
+ worker.removeEventListener("message", onMessageFromWorker);
+ break;
+ }
+ // dump log if present
+ if (data.log) {
+ for (var line in data.log) {
+ PROFILERLOG(line);
+ }
+ }
+ }
+ }
+ var pendingMessages = [];
+ var messageProcessingTimer = 0;
+ function processMessages() {
+ messageProcessingTimer = 0;
+ processMessage(pendingMessages.shift());
+ if (pendingMessages.length)
+ scheduleMessageProcessing();
+ }
+ function scheduleMessageProcessing() {
+ if (messageProcessingTimer)
+ return;
+ messageProcessingTimer = setTimeout(processMessages, 10);
+ }
+ worker.addEventListener("message", onMessageFromWorker);
+}
+
+WorkerRequest.prototype = {
+ send: function WorkerRequest_send(task, taskData) {
+ this._sendChunkReporter.begin("Sending data to worker...");
+ var startTime = Date.now();
+ this._worker.postMessage({
+ requestID: this._requestID,
+ task: task,
+ taskData: taskData
+ });
+ var postTime = Date.now() - startTime;
+ if (true || postTime > 10)
+ console.log("posting message to worker: " + postTime + "ms");
+ this._sendChunkReporter.finish();
+ this._executeReporter.begin("Processing worker request...");
+ },
+ sendInChunks: function WorkerRequest_sendInChunks(task, taskData, params, maxChunkSize) {
+ this._sendChunkReporter.begin("Sending data to worker...");
+ var self = this;
+ var chunks = bucketsBySplittingArray(taskData, maxChunkSize);
+ var pendingMessages = [
+ {
+ requestID: this._requestID,
+ task: "chunkedStart",
+ numChunks: chunks.length
+ }
+ ].concat(chunks.map(function (chunk) {
+ return {
+ requestID: self._requestID,
+ task: "chunkedChunk",
+ chunk: chunk
+ };
+ })).concat([
+ {
+ requestID: this._requestID,
+ task: "chunkedEnd"
+ },
+ {
+ requestID: this._requestID,
+ params: params,
+ task: task
+ },
+ ]);
+ var totalMessages = pendingMessages.length;
+ var numSentMessages = 0;
+ function postMessage(msg) {
+ var msgIndex = numSentMessages++;
+ var startTime = Date.now();
+ self._worker.postMessage(msg);
+ var postTime = Date.now() - startTime;
+ if (postTime > 10)
+ console.log("posting message to worker: " + postTime + "ms");
+ self._sendChunkReporter.setProgress((msgIndex + 1) / totalMessages);
+ }
+ var messagePostingTimer = 0;
+ function postMessages() {
+ messagePostingTimer = 0;
+ postMessage(pendingMessages.shift());
+ if (pendingMessages.length) {
+ scheduleMessagePosting();
+ } else {
+ self._sendChunkReporter.finish();
+ self._executeReporter.begin("Processing worker request...");
+ }
+ }
+ function scheduleMessagePosting() {
+ if (messagePostingTimer)
+ return;
+ messagePostingTimer = setTimeout(postMessages, 10);
+ }
+ scheduleMessagePosting();
+ },
+
+ // TODO: share code with TreeView
+ addEventListener: function WorkerRequest_addEventListener(eventName, callbackFunction) {
+ if (!(eventName in this._eventListeners))
+ this._eventListeners[eventName] = [];
+ if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
+ return;
+ this._eventListeners[eventName].push(callbackFunction);
+ },
+ removeEventListener: function WorkerRequest_removeEventListener(eventName, callbackFunction) {
+ if (!(eventName in this._eventListeners))
+ return;
+ var index = this._eventListeners[eventName].indexOf(callbackFunction);
+ if (index == -1)
+ return;
+ this._eventListeners[eventName].splice(index, 1);
+ },
+ _fireEvent: function WorkerRequest__fireEvent(eventName, eventObject, p1) {
+ if (!(eventName in this._eventListeners))
+ return;
+ this._eventListeners[eventName].forEach(function (callbackFunction) {
+ callbackFunction(eventObject, p1);
+ });
+ },
+}
+
+var Parser = {
+ parse: function Parser_parse(data, params) {
+ console.log("profile num chars: " + data.length);
+ var request = new WorkerRequest(gParserWorker);
+ request.sendInChunks("parseRawProfile", data, params, 3000000);
+ return request;
+ },
+
+ updateFilters: function Parser_updateFilters(filters) {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("updateFilters", {
+ filters: filters,
+ profileID: 0
+ });
+ return request;
+ },
+
+ updateViewOptions: function Parser_updateViewOptions(options) {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("updateViewOptions", {
+ options: options,
+ profileID: 0
+ });
+ return request;
+ },
+
+ getSerializedProfile: function Parser_getSerializedProfile(complete, callback) {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("getSerializedProfile", {
+ profileID: 0,
+ complete: complete
+ });
+ request.addEventListener("finished", callback);
+ },
+
+ calculateHistogramData: function Parser_calculateHistogramData() {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("calculateHistogramData", {
+ profileID: 0
+ });
+ return request;
+ },
+
+ calculateDiagnosticItems: function Parser_calculateDiagnosticItems(meta) {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("calculateDiagnosticItems", {
+ profileID: 0,
+ meta: meta
+ });
+ return request;
+ },
+
+ updateLogSetting: function Parser_updateLogSetting() {
+ var request = new WorkerRequest(gParserWorker);
+ request.send("initWorker", {
+ debugLog: gDebugLog,
+ debugTrace: gDebugTrace,
+ });
+ return request;
+ },
+};
diff --git a/browser/devtools/profiler/cleopatra/js/parserWorker.js b/browser/devtools/profiler/cleopatra/js/parserWorker.js
new file mode 100644
index 000000000..e7335af61
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/parserWorker.js
@@ -0,0 +1,1655 @@
+/* -*- Mode: js2; indent-tabs-mode: nil; js2-basic-offset: 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/. */
+
+importScripts("ProgressReporter.js");
+
+var gProfiles = [];
+
+var partialTaskData = {};
+
+var gNextProfileID = 0;
+
+var gLogLines = [];
+
+var gDebugLog = false;
+var gDebugTrace = false;
+// Use for verbose tracing, otherwise use log
+function PROFILDERTRACE(msg) {
+ if (gDebugTrace)
+ PROFILERLOG(msg);
+}
+function PROFILERLOG(msg) {
+ if (gDebugLog) {
+ msg = "Cleo: " + msg;
+ //if (window.dump)
+ // window.dump(msg + "\n");
+ }
+}
+function PROFILERERROR(msg) {
+ msg = "Cleo: " + msg;
+ //if (window.dump)
+ // window.dump(msg + "\n");
+}
+
+// http://stackoverflow.com/a/2548133
+function endsWith(str, suffix) {
+ return str.indexOf(suffix, this.length - suffix.length) !== -1;
+};
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=728780
+if (!String.prototype.startsWith) {
+ String.prototype.startsWith =
+ function(s) { return this.lastIndexOf(s, 0) === 0; }
+}
+
+// functions for which lr is unconditionally valid. These are
+// largely going to be atomics and other similar functions
+// that don't touch lr. This is currently populated with
+// some functions from bionic, largely via manual inspection
+// of the assembly in e.g.
+// http://androidxref.com/source/xref/bionic/libc/arch-arm/syscalls/
+var sARMFunctionsWithValidLR = [
+ "__atomic_dec",
+ "__atomic_inc",
+ "__atomic_cmpxchg",
+ "__atomic_swap",
+ "__atomic_dec",
+ "__atomic_inc",
+ "__atomic_cmpxchg",
+ "__atomic_swap",
+ "__futex_syscall3",
+ "__futex_wait",
+ "__futex_wake",
+ "__futex_syscall3",
+ "__futex_wait",
+ "__futex_wake",
+ "__futex_syscall4",
+ "__ioctl",
+ "__brk",
+ "__wait4",
+ "epoll_wait",
+ "fsync",
+ "futex",
+ "nanosleep",
+ "pause",
+ "sched_yield",
+ "syscall"
+];
+
+function log() {
+ var z = [];
+ for (var i = 0; i < arguments.length; ++i)
+ z.push(arguments[i]);
+ gLogLines.push(z.join(" "));
+}
+
+self.onmessage = function (msg) {
+ try {
+ var requestID = msg.data.requestID;
+ var task = msg.data.task;
+ var taskData = msg.data.taskData;
+ if (!taskData &&
+ (["chunkedStart", "chunkedChunk", "chunkedEnd"].indexOf(task) == -1)) {
+ taskData = partialTaskData[requestID];
+ delete partialTaskData[requestID];
+ }
+ PROFILERLOG("Start task: " + task);
+
+ gLogLines = [];
+
+ switch (task) {
+ case "initWorker":
+ gDebugLog = taskData.debugLog;
+ gDebugTrace = taskData.debugTrace;
+ PROFILERLOG("Init logging in parserWorker");
+ return;
+ case "chunkedStart":
+ partialTaskData[requestID] = null;
+ break;
+ case "chunkedChunk":
+ if (partialTaskData[requestID] === null)
+ partialTaskData[requestID] = msg.data.chunk;
+ else
+ partialTaskData[requestID] = partialTaskData[requestID].concat(msg.data.chunk);
+ break;
+ case "chunkedEnd":
+ break;
+ case "parseRawProfile":
+ parseRawProfile(requestID, msg.data.params, taskData);
+ break;
+ case "updateFilters":
+ updateFilters(requestID, taskData.profileID, taskData.filters);
+ break;
+ case "updateViewOptions":
+ updateViewOptions(requestID, taskData.profileID, taskData.options);
+ break;
+ case "getSerializedProfile":
+ getSerializedProfile(requestID, taskData.profileID, taskData.complete);
+ break;
+ case "calculateHistogramData":
+ calculateHistogramData(requestID, taskData.profileID);
+ break;
+ case "calculateDiagnosticItems":
+ calculateDiagnosticItems(requestID, taskData.profileID, taskData.meta);
+ break;
+ default:
+ sendError(requestID, "Unknown task " + task);
+ break;
+ }
+ PROFILERLOG("Complete task: " + task);
+ } catch (e) {
+ PROFILERERROR("Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
+ sendError(requestID, "Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
+ }
+}
+
+function sendError(requestID, error) {
+ // support sendError(msg)
+ if (error == null) {
+ error = requestID;
+ requestID = null;
+ }
+
+ self.postMessage({
+ requestID: requestID,
+ type: "error",
+ error: error,
+ log: gLogLines
+ });
+}
+
+function sendProgress(requestID, progress) {
+ self.postMessage({
+ requestID: requestID,
+ type: "progress",
+ progress: progress
+ });
+}
+
+function sendFinished(requestID, result) {
+ self.postMessage({
+ requestID: requestID,
+ type: "finished",
+ result: result,
+ log: gLogLines
+ });
+}
+
+function bucketsBySplittingArray(array, maxCostPerBucket, costOfElementCallback) {
+ var buckets = [];
+ var currentBucket = [];
+ var currentBucketCost = 0;
+ for (var i = 0; i < array.length; i++) {
+ var element = array[i];
+ var costOfCurrentElement = costOfElementCallback ? costOfElementCallback(element) : 1;
+ if (currentBucketCost + costOfCurrentElement > maxCostPerBucket) {
+ buckets.push(currentBucket);
+ currentBucket = [];
+ currentBucketCost = 0;
+ }
+ currentBucket.push(element);
+ currentBucketCost += costOfCurrentElement;
+ }
+ buckets.push(currentBucket);
+ return buckets;
+}
+
+function sendFinishedInChunks(requestID, result, maxChunkCost, costOfElementCallback) {
+ if (result.length === undefined || result.slice === undefined)
+ throw new Error("Can't slice result into chunks");
+ self.postMessage({
+ requestID: requestID,
+ type: "finishedStart"
+ });
+ var chunks = bucketsBySplittingArray(result, maxChunkCost, costOfElementCallback);
+ for (var i = 0; i < chunks.length; i++) {
+ self.postMessage({
+ requestID: requestID,
+ type: "finishedChunk",
+ chunk: chunks[i]
+ });
+ }
+ self.postMessage({
+ requestID: requestID,
+ type: "finishedEnd",
+ log: gLogLines
+ });
+}
+
+function makeSample(frames, extraInfo) {
+ return {
+ frames: frames,
+ extraInfo: extraInfo
+ };
+}
+
+function cloneSample(sample) {
+ return makeSample(sample.frames.slice(0), sample.extraInfo);
+}
+function parseRawProfile(requestID, params, rawProfile) {
+ var progressReporter = new ProgressReporter();
+ progressReporter.addListener(function (r) {
+ sendProgress(requestID, r.getProgress());
+ });
+ progressReporter.begin("Parsing...");
+
+ var symbolicationTable = {};
+ var symbols = [];
+ var symbolIndices = {};
+ var resources = {};
+ var functions = [];
+ var functionIndices = {};
+ var samples = [];
+ var meta = {};
+ var armIncludePCIndex = {};
+
+ if (rawProfile == null) {
+ throw "rawProfile is null";
+ }
+
+ if (typeof rawProfile == "string" && rawProfile[0] == "{") {
+ // rawProfile is a JSON string.
+ rawProfile = JSON.parse(rawProfile);
+ if (rawProfile === null) {
+ throw "rawProfile couldn't not successfully be parsed using JSON.parse. Make sure that the profile is a valid JSON encoding.";
+ }
+ }
+
+
+ if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) {
+ rawProfile.profileJSON.meta = rawProfile.meta;
+ }
+
+ if (typeof rawProfile == "object") {
+ switch (rawProfile.format) {
+ case "profileStringWithSymbolicationTable,1":
+ symbolicationTable = rawProfile.symbolicationTable;
+ parseProfileString(rawProfile.profileString);
+ break;
+ case "profileJSONWithSymbolicationTable,1":
+ symbolicationTable = rawProfile.symbolicationTable;
+ parseProfileJSON(rawProfile.profileJSON);
+ break;
+ default:
+ parseProfileJSON(rawProfile);
+ }
+ } else {
+ parseProfileString(rawProfile);
+ }
+
+ if (params.profileId) {
+ meta.profileId = params.profileId;
+ }
+
+ function cleanFunctionName(functionName) {
+ var ignoredPrefix = "non-virtual thunk to ";
+ if (functionName.startsWith(ignoredPrefix))
+ return functionName.substr(ignoredPrefix.length);
+ return functionName;
+ }
+
+ function resourceNameForAddon(addon) {
+ if (!addon)
+ return "";
+
+ var iconHTML = "";
+ if (addon.iconURL)
+ iconHTML = "<img src=\"" + addon.iconURL + "\" style='width:12px; height:12px;'> "
+ return iconHTML + " " + (/@jetpack$/.exec(addon.id) ? "Jetpack: " : "") + addon.name;
+ }
+
+ function addonWithID(addonID) {
+ return firstMatch(meta.addons, function addonHasID(addon) {
+ return addon.id.toLowerCase() == addonID.toLowerCase();
+ })
+ }
+
+ function resourceNameForAddonWithID(addonID) {
+ return resourceNameForAddon(addonWithID(addonID));
+ }
+
+ function findAddonForChromeURIHost(host) {
+ return firstMatch(meta.addons, function addonUsesChromeURIHost(addon) {
+ return addon.chromeURIHosts && addon.chromeURIHosts.indexOf(host) != -1;
+ });
+ }
+
+ function ensureResource(name, resourceDescription) {
+ if (!(name in resources)) {
+ resources[name] = resourceDescription;
+ }
+ return name;
+ }
+
+ function resourceNameFromLibrary(library) {
+ return ensureResource("lib_" + library, {
+ type: "library",
+ name: library
+ });
+ }
+
+ function getAddonForScriptURI(url, host) {
+ if (!meta || !meta.addons)
+ return null;
+
+ if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) {
+ // Assume this is a jetpack url
+ var jetpackID = host.substring(0, host.length - 11) + "@jetpack";
+ return addonWithID(jetpackID);
+ }
+
+ if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) {
+ var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url);
+ if (unpackedAddonNameMatch)
+ return addonWithID(decodeURIComponent(unpackedAddonNameMatch[1]));
+ return null;
+ }
+
+ if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) {
+ var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url);
+ if (packedAddonNameMatch)
+ return addonWithID(decodeURIComponent(packedAddonNameMatch[1]));
+ return null;
+ }
+
+ if (url.startsWith("chrome://")) {
+ var chromeURIMatch = /chrome\:\/\/(.*?)\//.exec(url);
+ if (chromeURIMatch)
+ return findAddonForChromeURIHost(chromeURIMatch[1]);
+ return null;
+ }
+
+ return null;
+ }
+
+ function resourceNameFromURI(url) {
+ if (!url)
+ return ensureResource("unknown", {type: "unknown", name: "<unknown>"});
+
+ var match = /^(.*):\/\/(.*?)\//.exec(url);
+
+ if (!match) {
+ // Can this happen? If so, we should change the regular expression above.
+ return ensureResource("url_" + url, {type: "url", name: url});
+ }
+
+ var urlRoot = match[0];
+ var protocol = match[1];
+ var host = match[2];
+
+ var addon = getAddonForScriptURI(url, host);
+ if (addon) {
+ return ensureResource("addon_" + addon.id, {
+ type: "addon",
+ name: addon.name,
+ addonID: addon.id,
+ icon: addon.iconURL
+ });
+ }
+
+ if (protocol.startsWith("http")) {
+ return ensureResource("webhost_" + host, {
+ type: "webhost",
+ name: host,
+ icon: urlRoot + "favicon.ico"
+ });
+ }
+
+ return ensureResource("otherhost_" + host, {
+ type: "otherhost",
+ name: host
+ });
+ }
+
+ function parseScriptFile(url) {
+ var match = /([^\/]*)$/.exec(url);
+ if (match && match[1])
+ return match[1];
+
+ return url;
+ }
+
+ // JS File information sometimes comes with multiple URIs which are chained
+ // with " -> ". We only want the last URI in this list.
+ function getRealScriptURI(url) {
+ if (url) {
+ var urls = url.split(" -> ");
+ return urls[urls.length - 1];
+ }
+ return url;
+ }
+
+ function getFunctionInfo(fullName) {
+
+ function getCPPFunctionInfo(fullName) {
+ var match =
+ /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) ||
+ /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) ||
+ /^(.*) \(in ([^\)]*)\)$/.exec(fullName);
+
+ if (!match)
+ return null;
+
+ return {
+ functionName: cleanFunctionName(match[1]),
+ libraryName: resourceNameFromLibrary(match[2]),
+ lineInformation: match[3] || "",
+ isRoot: false,
+ isJSFrame: false
+ };
+ }
+
+ function getJSFunctionInfo(fullName) {
+ var jsMatch =
+ /^(.*) \((.*):([0-9]+)\)$/.exec(fullName) ||
+ /^()(.*):([0-9]+)$/.exec(fullName);
+
+ if (!jsMatch)
+ return null;
+
+ var functionName = jsMatch[1] || "<Anonymous>";
+ var scriptURI = getRealScriptURI(jsMatch[2]);
+ var lineNumber = jsMatch[3];
+ var scriptFile = parseScriptFile(scriptURI);
+ var resourceName = resourceNameFromURI(scriptURI);
+
+ return {
+ functionName: functionName + "() @ " + scriptFile + ":" + lineNumber,
+ libraryName: resourceName,
+ lineInformation: "",
+ isRoot: false,
+ isJSFrame: true,
+ scriptLocation: {
+ scriptURI: scriptURI,
+ lineInformation: lineNumber
+ }
+ };
+ }
+
+ function getFallbackFunctionInfo(fullName) {
+ return {
+ functionName: cleanFunctionName(fullName),
+ libraryName: "",
+ lineInformation: "",
+ isRoot: fullName == "(root)",
+ isJSFrame: false
+ };
+ }
+
+ return getCPPFunctionInfo(fullName) ||
+ getJSFunctionInfo(fullName) ||
+ getFallbackFunctionInfo(fullName);
+ }
+
+ function indexForFunction(symbol, info) {
+ var resolve = info.functionName + "__" + info.libraryName;
+ if (resolve in functionIndices)
+ return functionIndices[resolve];
+ var newIndex = functions.length;
+ info.symbol = symbol;
+ functions[newIndex] = info;
+ functionIndices[resolve] = newIndex;
+ return newIndex;
+ }
+
+ function parseSymbol(symbol) {
+ var info = getFunctionInfo(symbol);
+ //dump("Parse symbol: " + symbol + "\n");
+ return {
+ symbolName: symbol,
+ functionName: info.functionName,
+ functionIndex: indexForFunction(symbol, info),
+ lineInformation: info.lineInformation,
+ isRoot: info.isRoot,
+ isJSFrame: info.isJSFrame,
+ scriptLocation: info.scriptLocation
+ };
+ }
+
+ function translatedSymbol(symbol) {
+ return symbolicationTable[symbol] || symbol;
+ }
+
+ function indexForSymbol(symbol) {
+ if (symbol in symbolIndices)
+ return symbolIndices[symbol];
+ var newIndex = symbols.length;
+ symbols[newIndex] = parseSymbol(translatedSymbol(symbol));
+ symbolIndices[symbol] = newIndex;
+ return newIndex;
+ }
+
+ function clearRegExpLastMatch() {
+ /./.exec(" ");
+ }
+
+ function shouldIncludeARMLRForPC(pcIndex) {
+ if (pcIndex in armIncludePCIndex)
+ return armIncludePCIndex[pcIndex];
+
+ var pcName = symbols[pcIndex].functionName;
+ var include = sARMFunctionsWithValidLR.indexOf(pcName) != -1;
+ armIncludePCIndex[pcIndex] = include;
+ return include;
+ }
+
+ function parseProfileString(data) {
+ var extraInfo = {};
+ var lines = data.split("\n");
+ var sample = null;
+ for (var i = 0; i < lines.length; ++i) {
+ var line = lines[i];
+ if (line.length < 2 || line[1] != '-') {
+ // invalid line, ignore it
+ continue;
+ }
+ var info = line.substring(2);
+ switch (line[0]) {
+ //case 'l':
+ // // leaf name
+ // if ("leafName" in extraInfo) {
+ // extraInfo.leafName += ":" + info;
+ // } else {
+ // extraInfo.leafName = info;
+ // }
+ // break;
+ case 'm':
+ // marker
+ if (!("marker" in extraInfo)) {
+ extraInfo.marker = [];
+ }
+ extraInfo.marker.push(info);
+ break;
+ case 's':
+ // sample
+ var sampleName = info;
+ sample = makeSample([indexForSymbol(sampleName)], extraInfo);
+ samples.push(sample);
+ extraInfo = {}; // reset the extra info for future rounds
+ break;
+ case 'c':
+ case 'l':
+ // continue sample
+ if (sample) { // ignore the case where we see a 'c' before an 's'
+ sample.frames.push(indexForSymbol(info));
+ }
+ break;
+ case 'L':
+ // continue sample; this is an ARM LR record. Stick it before the
+ // PC if it's one of the functions where we know LR is good.
+ if (sample && sample.frames.length > 1) {
+ var pcIndex = sample.frames[sample.frames.length - 1];
+ if (shouldIncludeARMLRForPC(pcIndex)) {
+ sample.frames.splice(-1, 0, indexForSymbol(info));
+ }
+ }
+ break;
+ case 't':
+ // time
+ if (sample) {
+ sample.extraInfo["time"] = parseFloat(info);
+ }
+ break;
+ case 'r':
+ // responsiveness
+ if (sample) {
+ sample.extraInfo["responsiveness"] = parseFloat(info);
+ }
+ break;
+ }
+ progressReporter.setProgress((i + 1) / lines.length);
+ }
+ }
+
+ function parseProfileJSON(profile) {
+ // Thread 0 will always be the main thread of interest
+ // TODO support all the thread in the profile
+ var profileSamples = null;
+ meta = profile.meta || {};
+ if (params.appendVideoCapture) {
+ meta.videoCapture = {
+ src: params.appendVideoCapture,
+ };
+ }
+ // Support older format that aren't thread aware
+ if (profile.threads != null) {
+ profileSamples = profile.threads[0].samples;
+ } else {
+ profileSamples = profile;
+ }
+ var rootSymbol = null;
+ var insertCommonRoot = false;
+ var frameStart = {};
+ meta.frameStart = frameStart;
+ for (var j = 0; j < profileSamples.length; j++) {
+ var sample = profileSamples[j];
+ var indicedFrames = [];
+ if (!sample) {
+ // This sample was filtered before saving
+ samples.push(null);
+ progressReporter.setProgress((j + 1) / profileSamples.length);
+ continue;
+ }
+ for (var k = 0; sample.frames && k < sample.frames.length; k++) {
+ var frame = sample.frames[k];
+ var pcIndex;
+ if (frame.location !== undefined) {
+ pcIndex = indexForSymbol(frame.location);
+ } else {
+ pcIndex = indexForSymbol(frame);
+ }
+
+ if (frame.lr !== undefined && shouldIncludeARMLRForPC(pcIndex)) {
+ indicedFrames.push(indexForSymbol(frame.lr));
+ }
+
+ indicedFrames.push(pcIndex);
+ }
+ if (indicedFrames.length >= 1) {
+ if (rootSymbol && rootSymbol != indicedFrames[0]) {
+ insertCommonRoot = true;
+ }
+ rootSymbol = rootSymbol || indicedFrames[0];
+ }
+ if (sample.extraInfo == null) {
+ sample.extraInfo = {};
+ }
+ if (sample.responsiveness) {
+ sample.extraInfo["responsiveness"] = sample.responsiveness;
+ }
+ if (sample.marker) {
+ sample.extraInfo["marker"] = sample.marker;
+ }
+ if (sample.time) {
+ sample.extraInfo["time"] = sample.time;
+ }
+ if (sample.frameNumber) {
+ sample.extraInfo["frameNumber"] = sample.frameNumber;
+ //dump("Got frame number: " + sample.frameNumber + "\n");
+ frameStart[sample.frameNumber] = samples.length;
+ }
+ samples.push(makeSample(indicedFrames, sample.extraInfo));
+ progressReporter.setProgress((j + 1) / profileSamples.length);
+ }
+ if (insertCommonRoot) {
+ var rootIndex = indexForSymbol("(root)");
+ for (var i = 0; i < samples.length; i++) {
+ var sample = samples[i];
+ if (!sample) continue;
+ // If length == 0 then the sample was filtered when saving the profile
+ if (sample.frames.length >= 1 && sample.frames[0] != rootIndex)
+ sample.frames.unshift(rootIndex)
+ }
+ }
+ }
+
+ progressReporter.finish();
+ // Don't increment the profile ID now because (1) it's buggy
+ // and (2) for now there's no point in storing each profile
+ // here if we're storing them in the local storage.
+ //var profileID = gNextProfileID++;
+ var profileID = gNextProfileID;
+ gProfiles[profileID] = JSON.parse(JSON.stringify({
+ meta: meta,
+ symbols: symbols,
+ functions: functions,
+ resources: resources,
+ allSamples: samples
+ }));
+ clearRegExpLastMatch();
+ sendFinished(requestID, {
+ meta: meta,
+ numSamples: samples.length,
+ profileID: profileID,
+ symbols: symbols,
+ functions: functions,
+ resources: resources
+ });
+}
+
+function getSerializedProfile(requestID, profileID, complete) {
+ var profile = gProfiles[profileID];
+ var symbolicationTable = {};
+ if (complete || !profile.filterSettings.mergeFunctions) {
+ for (var symbolIndex in profile.symbols) {
+ symbolicationTable[symbolIndex] = profile.symbols[symbolIndex].symbolName;
+ }
+ } else {
+ for (var functionIndex in profile.functions) {
+ var f = profile.functions[functionIndex];
+ symbolicationTable[functionIndex] = f.symbol;
+ }
+ }
+ var serializedProfile = JSON.stringify({
+ format: "profileJSONWithSymbolicationTable,1",
+ meta: profile.meta,
+ profileJSON: complete ? profile.allSamples : profile.filteredSamples,
+ symbolicationTable: symbolicationTable
+ });
+ sendFinished(requestID, serializedProfile);
+}
+
+function TreeNode(name, parent, startCount) {
+ this.name = name;
+ this.children = [];
+ this.counter = startCount;
+ this.parent = parent;
+}
+TreeNode.prototype.getDepth = function TreeNode__getDepth() {
+ if (this.parent)
+ return this.parent.getDepth() + 1;
+ return 0;
+};
+TreeNode.prototype.findChild = function TreeNode_findChild(name) {
+ for (var i = 0; i < this.children.length; i++) {
+ var child = this.children[i];
+ if (child.name == name)
+ return child;
+ }
+ return null;
+}
+// path is an array of strings which is matched to our nodes' names.
+// Try to walk path in our own tree and return the last matching node. The
+// length of the match can be calculated by the caller by comparing the
+// returned node's depth with the depth of the path's start node.
+TreeNode.prototype.followPath = function TreeNode_followPath(path) {
+ if (path.length == 0)
+ return this;
+
+ var matchingChild = this.findChild(path[0]);
+ if (!matchingChild)
+ return this;
+
+ return matchingChild.followPath(path.slice(1));
+};
+TreeNode.prototype.incrementCountersInParentChain = function TreeNode_incrementCountersInParentChain() {
+ this.counter++;
+ if (this.parent)
+ this.parent.incrementCountersInParentChain();
+};
+
+function convertToCallTree(samples, isReverse) {
+ function areSamplesMultiroot(samples) {
+ var previousRoot;
+ for (var i = 0; i < samples.length; ++i) {
+ if (!previousRoot) {
+ previousRoot = samples[i].frames[0];
+ continue;
+ }
+ if (previousRoot != samples[i].frames[0]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ samples = samples.filter(function noNullSamples(sample) {
+ return sample != null;
+ });
+ if (samples.length == 0)
+ return new TreeNode("(empty)", null, 0);
+ var firstRoot = null;
+ for (var i = 0; i < samples.length; ++i) {
+ firstRoot = samples[i].frames[0];
+ break;
+ }
+ if (firstRoot == null) {
+ return new TreeNode("(all filtered)", null, 0);
+ }
+ var multiRoot = areSamplesMultiroot(samples);
+ var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0);
+ for (var i = 0; i < samples.length; ++i) {
+ var sample = samples[i];
+ var callstack = sample.frames.slice(0);
+ callstack.shift();
+ if (isReverse)
+ callstack.reverse();
+ var deepestExistingNode = treeRoot.followPath(callstack);
+ var remainingCallstack = callstack.slice(deepestExistingNode.getDepth());
+ deepestExistingNode.incrementCountersInParentChain();
+ var node = deepestExistingNode;
+ for (var j = 0; j < remainingCallstack.length; ++j) {
+ var frame = remainingCallstack[j];
+ var child = new TreeNode(frame, node, 1);
+ node.children.push(child);
+ node = child;
+ }
+ }
+ return treeRoot;
+}
+
+function filterByJank(samples, filterThreshold) {
+ return samples.map(function nullNonJank(sample) {
+ if (!sample ||
+ !("responsiveness" in sample.extraInfo) ||
+ sample.extraInfo["responsiveness"] < filterThreshold)
+ return null;
+ return sample;
+ });
+}
+
+function filterBySymbol(samples, symbolOrFunctionIndex) {
+ return samples.map(function filterSample(origSample) {
+ if (!origSample)
+ return null;
+ var sample = cloneSample(origSample);
+ for (var i = 0; i < sample.frames.length; i++) {
+ if (symbolOrFunctionIndex == sample.frames[i]) {
+ sample.frames = sample.frames.slice(i);
+ return sample;
+ }
+ }
+ return null; // no frame matched; filter out complete sample
+ });
+}
+
+function filterByCallstackPrefix(samples, symbols, functions, callstack, appliesToJS, useFunctions) {
+ var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
+ return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
+ } : function isJSSymbolOrRoot(symbolIndex) {
+ return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
+ };
+ return samples.map(function filterSample(sample) {
+ if (!sample)
+ return null;
+ if (sample.frames.length < callstack.length)
+ return null;
+ for (var i = 0, j = 0; j < callstack.length; i++) {
+ if (i >= sample.frames.length)
+ return null;
+ if (appliesToJS && !isJSFrameOrRoot(sample.frames[i]))
+ continue;
+ if (sample.frames[i] != callstack[j])
+ return null;
+ j++;
+ }
+ return makeSample(sample.frames.slice(i - 1), sample.extraInfo);
+ });
+}
+
+function filterByCallstackPostfix(samples, symbols, functions, callstack, appliesToJS, useFunctions) {
+ var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
+ return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
+ } : function isJSSymbolOrRoot(symbolIndex) {
+ return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
+ };
+ return samples.map(function filterSample(sample) {
+ if (!sample)
+ return null;
+ if (sample.frames.length < callstack.length)
+ return null;
+ for (var i = 0, j = 0; j < callstack.length; i++) {
+ if (i >= sample.frames.length)
+ return null;
+ if (appliesToJS && !isJSFrameOrRoot(sample.frames[sample.frames.length - i - 1]))
+ continue;
+ if (sample.frames[sample.frames.length - i - 1] != callstack[j])
+ return null;
+ j++;
+ }
+ var newFrames = sample.frames.slice(0, sample.frames.length - i + 1);
+ return makeSample(newFrames, sample.extraInfo);
+ });
+}
+
+function chargeNonJSToCallers(samples, symbols, functions, useFunctions) {
+ var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) {
+ return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot);
+ } : function isJSSymbolOrRoot(symbolIndex) {
+ return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot);
+ };
+ samples = samples.slice(0);
+ for (var i = 0; i < samples.length; ++i) {
+ var sample = samples[i];
+ if (!sample)
+ continue;
+ var newFrames = sample.frames.filter(isJSFrameOrRoot);
+ if (!newFrames.length) {
+ samples[i] = null;
+ } else {
+ samples[i].frames = newFrames;
+ }
+ }
+ return samples;
+}
+
+function filterByName(samples, symbols, functions, filterName, useFunctions) {
+ function getSymbolOrFunctionName(index, useFunctions) {
+ if (useFunctions) {
+ if (!(index in functions))
+ return "";
+ return functions[index].functionName;
+ }
+ if (!(index in symbols))
+ return "";
+ return symbols[index].symbolName;
+ }
+ function getLibraryName(index, useFunctions) {
+ if (useFunctions) {
+ if (!(index in functions))
+ return "";
+ return functions[index].libraryName;
+ }
+ if (!(index in symbols))
+ return "";
+ return symbols[index].libraryName;
+ }
+ samples = samples.slice(0);
+ filterName = filterName.toLowerCase();
+ calltrace_it: for (var i = 0; i < samples.length; ++i) {
+ var sample = samples[i];
+ if (!sample)
+ continue;
+ var callstack = sample.frames;
+ for (var j = 0; j < callstack.length; ++j) {
+ var symbolOrFunctionName = getSymbolOrFunctionName(callstack[j], useFunctions);
+ var libraryName = getLibraryName(callstack[j], useFunctions);
+ if (symbolOrFunctionName.toLowerCase().indexOf(filterName) != -1 ||
+ libraryName.toLowerCase().indexOf(filterName) != -1) {
+ continue calltrace_it;
+ }
+ }
+ samples[i] = null;
+ }
+ return samples;
+}
+
+function discardLineLevelInformation(samples, symbols, functions) {
+ var data = samples;
+ var filteredData = [];
+ for (var i = 0; i < data.length; i++) {
+ if (!data[i]) {
+ filteredData.push(null);
+ continue;
+ }
+ filteredData.push(cloneSample(data[i]));
+ var frames = filteredData[i].frames;
+ for (var j = 0; j < frames.length; j++) {
+ if (!(frames[j] in symbols))
+ continue;
+ frames[j] = symbols[frames[j]].functionIndex;
+ }
+ }
+ return filteredData;
+}
+
+function mergeUnbranchedCallPaths(root) {
+ var mergedNames = [root.name];
+ var node = root;
+ while (node.children.length == 1 && node.counter == node.children[0].counter) {
+ node = node.children[0];
+ mergedNames.push(node.name);
+ }
+ if (node != root) {
+ // Merge path from root to node into root.
+ root.children = node.children;
+ root.mergedNames = mergedNames;
+ //root.name = clipText(root.name, 50) + " to " + this._clipText(node.name, 50);
+ }
+ for (var i = 0; i < root.children.length; i++) {
+ mergeUnbranchedCallPaths(root.children[i]);
+ }
+}
+
+function FocusedFrameSampleFilter(focusedSymbol) {
+ this._focusedSymbol = focusedSymbol;
+}
+FocusedFrameSampleFilter.prototype = {
+ filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions, useFunctions) {
+ return filterBySymbol(samples, this._focusedSymbol);
+ }
+};
+
+function FocusedCallstackPrefixSampleFilter(focusedCallstack, appliesToJS) {
+ this._focusedCallstackPrefix = focusedCallstack;
+ this._appliesToJS = appliesToJS;
+}
+FocusedCallstackPrefixSampleFilter.prototype = {
+ filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions, useFunctions) {
+ return filterByCallstackPrefix(samples, symbols, functions, this._focusedCallstackPrefix, this._appliesToJS, useFunctions);
+ }
+};
+
+function FocusedCallstackPostfixSampleFilter(focusedCallstack, appliesToJS) {
+ this._focusedCallstackPostfix = focusedCallstack;
+ this._appliesToJS = appliesToJS;
+}
+FocusedCallstackPostfixSampleFilter.prototype = {
+ filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions, useFunctions) {
+ return filterByCallstackPostfix(samples, symbols, functions, this._focusedCallstackPostfix, this._appliesToJS, useFunctions);
+ }
+};
+
+function RangeSampleFilter(start, end) {
+ this._start = start;
+ this._end = end;
+}
+RangeSampleFilter.prototype = {
+ filter: function RangeSampleFilter_filter(samples, symbols, functions) {
+ return samples.slice(this._start, this._end);
+ }
+}
+
+function unserializeSampleFilters(filters) {
+ return filters.map(function (filter) {
+ switch (filter.type) {
+ case "FocusedFrameSampleFilter":
+ return new FocusedFrameSampleFilter(filter.focusedSymbol);
+ case "FocusedCallstackPrefixSampleFilter":
+ return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack, filter.appliesToJS);
+ case "FocusedCallstackPostfixSampleFilter":
+ return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack, filter.appliesToJS);
+ case "RangeSampleFilter":
+ return new RangeSampleFilter(filter.start, filter.end);
+ case "PluginView":
+ return null;
+ default:
+ throw new Error("Unknown filter");
+ }
+ })
+}
+
+var gJankThreshold = 50 /* ms */;
+
+function updateFilters(requestID, profileID, filters) {
+ var profile = gProfiles[profileID];
+ var samples = profile.allSamples;
+ var symbols = profile.symbols;
+ var functions = profile.functions;
+
+ if (filters.mergeFunctions) {
+ samples = discardLineLevelInformation(samples, symbols, functions);
+ }
+ if (filters.nameFilter) {
+ try {
+ samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions);
+ } catch (e) {
+ dump("Could not filer by name: " + e + "\n");
+ }
+ }
+ samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) {
+ if (currentFilter===null) return filteredSamples;
+ return currentFilter.filter(filteredSamples, symbols, functions, filters.mergeFunctions);
+ }, samples);
+ if (filters.jankOnly) {
+ samples = filterByJank(samples, gJankThreshold);
+ }
+ if (filters.javascriptOnly) {
+ samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions);
+ }
+
+ gProfiles[profileID].filterSettings = filters;
+ gProfiles[profileID].filteredSamples = samples;
+ sendFinishedInChunks(requestID, samples, 40000,
+ function (sample) { return sample ? sample.frames.length : 1; });
+}
+
+function updateViewOptions(requestID, profileID, options) {
+ var profile = gProfiles[profileID];
+ var samples = profile.filteredSamples;
+ var symbols = profile.symbols;
+ var functions = profile.functions;
+
+ var treeData = convertToCallTree(samples, options.invertCallstack);
+ if (options.mergeUnbranched)
+ mergeUnbranchedCallPaths(treeData);
+ sendFinished(requestID, treeData);
+}
+
+// The responsiveness threshold (in ms) after which the sample shuold become
+// completely red in the histogram.
+var kDelayUntilWorstResponsiveness = 1000;
+
+function calculateHistogramData(requestID, profileID) {
+
+ function getStepColor(step) {
+ if (step.extraInfo && "responsiveness" in step.extraInfo) {
+ var res = step.extraInfo.responsiveness;
+ var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness));
+ return "rgb(" + redComponent + ",0,0)";
+ }
+
+ return "rgb(0,0,0)";
+ }
+
+ var profile = gProfiles[profileID];
+ var data = profile.filteredSamples;
+ var histogramData = [];
+ var maxHeight = 0;
+ for (var i = 0; i < data.length; ++i) {
+ if (!data[i])
+ continue;
+ var value = data[i].frames.length;
+ if (maxHeight < value)
+ maxHeight = value;
+ }
+ maxHeight += 1;
+ var nextX = 0;
+ // The number of data items per histogramData rects.
+ // Except when seperated by a marker.
+ // This is used to cut down the number of rects, since
+ // there's no point in having more rects then pixels
+ var samplesPerStep = Math.max(1, Math.floor(data.length / 2000));
+ var frameStart = {};
+ for (var i = 0; i < data.length; i++) {
+ var step = data[i];
+ if (!step) {
+ // Add a gap for the sample that was filtered out.
+ nextX += 1 / samplesPerStep;
+ continue;
+ }
+ nextX = Math.ceil(nextX);
+ var value = step.frames.length / maxHeight;
+ var frames = step.frames;
+ var currHistogramData = histogramData[histogramData.length-1];
+ if (step.extraInfo && "marker" in step.extraInfo) {
+ // A new marker boundary has been discovered.
+ histogramData.push({
+ frames: "marker",
+ x: nextX,
+ width: 2,
+ value: 1,
+ marker: step.extraInfo.marker,
+ color: "fuchsia"
+ });
+ nextX += 2;
+ histogramData.push({
+ frames: [step.frames],
+ x: nextX,
+ width: 1,
+ value: value,
+ color: getStepColor(step),
+ });
+ nextX += 1;
+ } else if (currHistogramData != null &&
+ currHistogramData.frames.length < samplesPerStep &&
+ !(step.extraInfo && "frameNumber" in step.extraInfo)) {
+ currHistogramData.frames.push(step.frames);
+ // When merging data items take the average:
+ currHistogramData.value =
+ (currHistogramData.value * (currHistogramData.frames.length - 1) + value) /
+ currHistogramData.frames.length;
+ // Merge the colors? For now we keep the first color set.
+ } else {
+ // A new name boundary has been discovered.
+ currHistogramData = {
+ frames: [step.frames],
+ x: nextX,
+ width: 1,
+ value: value,
+ color: getStepColor(step),
+ };
+ if (step.extraInfo && "frameNumber" in step.extraInfo) {
+ currHistogramData.frameNumber = step.extraInfo.frameNumber;
+ frameStart[step.extraInfo.frameNumber] = histogramData.length;
+ }
+ histogramData.push(currHistogramData);
+ nextX += 1;
+ }
+ }
+ sendFinished(requestID, { histogramData: histogramData, frameStart: frameStart, widthSum: Math.ceil(nextX) });
+}
+
+var diagnosticList = [
+ // *************** Known bugs first (highest priority)
+ {
+ image: "io.png",
+ title: "Main Thread IO - Bug 765135 - TISCreateInputSourceList",
+ check: function(frames, symbols, meta) {
+
+ if (!stepContains('TISCreateInputSourceList', frames, symbols))
+ return false;
+
+ return stepContains('__getdirentries64', frames, symbols)
+ || stepContains('__read', frames, symbols)
+ || stepContains('__open', frames, symbols)
+ || stepContains('stat$INODE64', frames, symbols)
+ ;
+ },
+ },
+
+ {
+ image: "js.png",
+ title: "Bug 772916 - Gradients are slow on mobile",
+ bugNumber: "772916",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('PaintGradient', frames, symbols)
+ && stepContains('BasicTiledLayerBuffer::PaintThebesSingleBufferDraw', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "cache.png",
+ title: "Bug 717761 - Main thread can be blocked by IO on the cache thread",
+ bugNumber: "717761",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('nsCacheEntryDescriptor::GetStoragePolicy', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "js.png",
+ title: "Web Content Shutdown Notification",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('nsAppStartup::Quit', frames, symbols)
+ && stepContains('nsDocShell::FirePageHideNotification', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "js.png",
+ title: "Bug 789193 - AMI_startup() takes 200ms on startup",
+ bugNumber: "789193",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('AMI_startup()', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "js.png",
+ title: "Bug 818296 - [Shutdown] js::NukeCrossCompartmentWrappers takes up 300ms on shutdown",
+ bugNumber: "818296",
+ check: function(frames, symbols, meta) {
+ return stepContains('js::NukeCrossCompartmentWrappers', frames, symbols)
+ && (stepContains('WindowDestroyedEvent', frames, symbols) || stepContains('DoShutdown', frames, symbols))
+ ;
+ },
+ },
+ {
+ image: "js.png",
+ title: "Bug 818274 - [Shutdown] Telemetry takes ~10ms on shutdown",
+ bugNumber: "818274",
+ check: function(frames, symbols, meta) {
+ return stepContains('TelemetryPing.js', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "plugin.png",
+ title: "Bug 818265 - [Shutdown] Plug-in shutdown takes ~90ms on shutdown",
+ bugNumber: "818265",
+ check: function(frames, symbols, meta) {
+ return stepContains('PluginInstanceParent::Destroy', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "snapshot.png",
+ title: "Bug 720575 - Make thumbnailing faster and/or asynchronous",
+ bugNumber: "720575",
+ check: function(frames, symbols, meta) {
+ return stepContains('Thumbnails_capture()', frames, symbols)
+ ;
+ },
+ },
+
+ {
+ image: "js.png",
+ title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ",
+ bugNumber: "789185",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('LoginManagerStorage_mozStorage.prototype.init()', frames, symbols)
+ ;
+ },
+ },
+
+ {
+ image: "js.png",
+ title: "JS - Bug 767070 - Text selection performance is bad on android",
+ bugNumber: "767070",
+ check: function(frames, symbols, meta) {
+
+ if (!stepContains('FlushPendingNotifications', frames, symbols))
+ return false;
+
+ return stepContains('sh_', frames, symbols)
+ && stepContains('browser.js', frames, symbols)
+ ;
+ },
+ },
+
+ {
+ image: "js.png",
+ title: "JS - Bug 765930 - Reader Mode: Optimize readability check",
+ bugNumber: "765930",
+ check: function(frames, symbols, meta) {
+
+ return stepContains('Readability.js', frames, symbols)
+ ;
+ },
+ },
+
+ // **************** General issues
+ {
+ image: "js.png",
+ title: "JS is triggering a sync reflow",
+ check: function(frames, symbols, meta) {
+ return symbolSequence(['js::RunScript','layout::DoReflow'], frames, symbols) ||
+ symbolSequence(['js::RunScript','layout::Flush'], frames, symbols)
+ ;
+ },
+ },
+
+ {
+ image: "gc.png",
+ title: "Garbage Collection Slice",
+ canMergeWithGC: false,
+ check: function(frames, symbols, meta, step) {
+ var slice = findGCSlice(frames, symbols, meta, step);
+
+ if (slice) {
+ var gcEvent = findGCEvent(frames, symbols, meta, step);
+ //dump("found event matching diagnostic\n");
+ //dump(JSON.stringify(gcEvent) + "\n");
+ return true;
+ }
+ return false;
+ },
+ details: function(frames, symbols, meta, step) {
+ var slice = findGCSlice(frames, symbols, meta, step);
+ if (slice) {
+ return "" +
+ "Reason: " + slice.reason + "\n" +
+ "Slice: " + slice.slice + "\n" +
+ "Pause: " + slice.pause + " ms";
+ }
+ return null;
+ },
+ onclickDetails: function(frames, symbols, meta, step) {
+ var gcEvent = findGCEvent(frames, symbols, meta, step);
+ if (gcEvent) {
+ return JSON.stringify(gcEvent);
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ image: "cc.png",
+ title: "Cycle Collect",
+ check: function(frames, symbols, meta, step) {
+ var ccEvent = findCCEvent(frames, symbols, meta, step);
+
+ if (ccEvent) {
+ return true;
+ }
+ return false;
+ },
+ details: function(frames, symbols, meta, step) {
+ var ccEvent = findCCEvent(frames, symbols, meta, step);
+ if (ccEvent) {
+ return "" +
+ "Duration: " + ccEvent.duration + " ms\n" +
+ "Suspected: " + ccEvent.suspected;
+ }
+ return null;
+ },
+ onclickDetails: function(frames, symbols, meta, step) {
+ var ccEvent = findCCEvent(frames, symbols, meta, step);
+ if (ccEvent) {
+ return JSON.stringify(ccEvent);
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ image: "gc.png",
+ title: "Garbage Collection",
+ canMergeWithGC: false,
+ check: function(frames, symbols, meta) {
+ return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols)
+ || stepContains('GarbageCollectNow', frames, symbols) // Label
+ || stepContains('JS_GC(', frames, symbols) // Label
+ || stepContains('CycleCollect__', frames, symbols) // Label
+ ;
+ },
+ },
+ {
+ image: "cc.png",
+ title: "Cycle Collect",
+ check: function(frames, symbols, meta) {
+ return stepContains('nsCycleCollector::Collect', frames, symbols)
+ || stepContains('CycleCollect__', frames, symbols) // Label
+ || stepContains('nsCycleCollectorRunner::Collect', frames, symbols) // Label
+ ;
+ },
+ },
+ {
+ image: "plugin.png",
+ title: "Sync Plugin Constructor",
+ check: function(frames, symbols, meta) {
+ return stepContains('CallPPluginInstanceConstructor', frames, symbols)
+ || stepContains('CallPCrashReporterConstructor', frames, symbols)
+ || stepContains('PPluginModuleParent::CallNP_Initialize', frames, symbols)
+ || stepContains('GeckoChildProcessHost::SyncLaunch', frames, symbols)
+ ;
+ },
+ },
+ {
+ image: "text.png",
+ title: "Font Loading",
+ check: function(frames, symbols, meta) {
+ return stepContains('gfxFontGroup::BuildFontList', frames, symbols);
+ },
+ },
+ {
+ image: "io.png",
+ title: "Main Thread IO!",
+ check: function(frames, symbols, meta) {
+ return stepContains('__getdirentries64', frames, symbols)
+ || stepContains('__open', frames, symbols)
+ || stepContains('NtFlushBuffersFile', frames, symbols)
+ || stepContains('storage:::Statement::ExecuteStep', frames, symbols)
+ || stepContains('__unlink', frames, symbols)
+ || stepContains('fsync', frames, symbols)
+ || stepContains('stat$INODE64', frames, symbols)
+ ;
+ },
+ },
+];
+
+function hasJSFrame(frames, symbols) {
+ for (var i = 0; i < frames.length; i++) {
+ if (symbols[frames[i]].isJSFrame === true) {
+ return true;
+ }
+ }
+ return false;
+}
+function findCCEvent(frames, symbols, meta, step) {
+ if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+ return null;
+
+ var time = step.extraInfo.time;
+
+ for (var i = 0; i < meta.gcStats.ccEvents.length; i++) {
+ var ccEvent = meta.gcStats.ccEvents[i];
+ if (ccEvent.start_timestamp <= time && ccEvent.end_timestamp >= time) {
+ //dump("JSON: " + js_beautify(JSON.stringify(ccEvent)) + "\n");
+ return ccEvent;
+ }
+ }
+
+ return null;
+}
+function findGCEvent(frames, symbols, meta, step) {
+ if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+ return null;
+
+ var time = step.extraInfo.time;
+
+ for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
+ var gcEvent = meta.gcStats.gcEvents[i];
+ if (!gcEvent.slices)
+ continue;
+ for (var j = 0; j < gcEvent.slices.length; j++) {
+ var slice = gcEvent.slices[j];
+ if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
+ return gcEvent;
+ }
+ }
+ }
+
+ return null;
+}
+function findGCSlice(frames, symbols, meta, step) {
+ if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+ return null;
+
+ var time = step.extraInfo.time;
+
+ for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
+ var gcEvent = meta.gcStats.gcEvents[i];
+ if (!gcEvent.slices)
+ continue;
+ for (var j = 0; j < gcEvent.slices.length; j++) {
+ var slice = gcEvent.slices[j];
+ if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
+ return slice;
+ }
+ }
+ }
+
+ return null;
+}
+function stepContains(substring, frames, symbols) {
+ for (var i = 0; frames && i < frames.length; i++) {
+ if (!(frames[i] in symbols))
+ continue;
+ var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+ if (frameSym.indexOf(substring) != -1) {
+ return true;
+ }
+ }
+ return false;
+}
+function stepContainsRegEx(regex, frames, symbols) {
+ for (var i = 0; frames && i < frames.length; i++) {
+ if (!(frames[i] in symbols))
+ continue;
+ var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+ if (regex.exec(frameSym)) {
+ return true;
+ }
+ }
+ return false;
+}
+function symbolSequence(symbolsOrder, frames, symbols) {
+ var symbolIndex = 0;
+ for (var i = 0; frames && i < frames.length; i++) {
+ if (!(frames[i] in symbols))
+ continue;
+ var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+ var substring = symbolsOrder[symbolIndex];
+ if (frameSym.indexOf(substring) != -1) {
+ symbolIndex++;
+ if (symbolIndex == symbolsOrder.length) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+function firstMatch(array, matchFunction) {
+ for (var i = 0; i < array.length; i++) {
+ if (matchFunction(array[i]))
+ return array[i];
+ }
+ return undefined;
+}
+
+function calculateDiagnosticItems(requestID, profileID, meta) {
+ /*
+ if (!histogramData || histogramData.length < 1) {
+ sendFinished(requestID, []);
+ return;
+ }*/
+
+ var profile = gProfiles[profileID];
+ //var symbols = profile.symbols;
+ var symbols = profile.functions;
+ var data = profile.filteredSamples;
+
+ var lastStep = data[data.length-1];
+ var widthSum = data.length;
+ var pendingDiagnosticInfo = null;
+
+ var diagnosticItems = [];
+
+ function finishPendingDiagnostic(endX) {
+ if (!pendingDiagnosticInfo)
+ return;
+
+ var diagnostic = pendingDiagnosticInfo.diagnostic;
+ var currDiagnostic = {
+ x: pendingDiagnosticInfo.x / widthSum,
+ width: (endX - pendingDiagnosticInfo.x) / widthSum,
+ imageFile: pendingDiagnosticInfo.diagnostic.image,
+ title: pendingDiagnosticInfo.diagnostic.title,
+ details: pendingDiagnosticInfo.details,
+ onclickDetails: pendingDiagnosticInfo.onclickDetails
+ };
+
+ if (!currDiagnostic.onclickDetails && diagnostic.bugNumber) {
+ currDiagnostic.onclickDetails = "bug " + diagnostic.bugNumber;
+ }
+
+ diagnosticItems.push(currDiagnostic);
+
+ pendingDiagnosticInfo = null;
+ }
+
+/*
+ dump("meta: " + meta.gcStats + "\n");
+ if (meta && meta.gcStats) {
+ dump("GC Stats: " + JSON.stringify(meta.gcStats) + "\n");
+ }
+*/
+
+ data.forEach(function diagnoseStep(step, x) {
+ if (step) {
+ var frames = step.frames;
+
+ var diagnostic = firstMatch(diagnosticList, function (diagnostic) {
+ return diagnostic.check(frames, symbols, meta, step);
+ });
+ }
+
+ if (!diagnostic) {
+ finishPendingDiagnostic(x);
+ return;
+ }
+
+ var details = diagnostic.details ? diagnostic.details(frames, symbols, meta, step) : null;
+
+ if (pendingDiagnosticInfo) {
+ // We're already inside a diagnostic range.
+ if (diagnostic == pendingDiagnosticInfo.diagnostic && pendingDiagnosticInfo.details == details) {
+ // We're still inside the same diagnostic.
+ return;
+ }
+
+ // We have left the old diagnostic and found a new one. Finish the old one.
+ finishPendingDiagnostic(x);
+ }
+
+ pendingDiagnosticInfo = {
+ diagnostic: diagnostic,
+ x: x,
+ details: details,
+ onclickDetails: diagnostic.onclickDetails ? diagnostic.onclickDetails(frames, symbols, meta, step) : null
+ };
+ });
+ if (pendingDiagnosticInfo)
+ finishPendingDiagnostic(data.length);
+
+ sendFinished(requestID, diagnosticItems);
+}
diff --git a/browser/devtools/profiler/cleopatra/js/strings.js b/browser/devtools/profiler/cleopatra/js/strings.js
new file mode 100644
index 000000000..42b368a3c
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/strings.js
@@ -0,0 +1,23 @@
+const Cu = Components.utils;
+Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
+
+/**
+ * Shortcuts for the L10N helper functions. Used in Cleopatra.
+ */
+var gStrings = {
+ // This strings are here so that Cleopatra code could use a simple object
+ // lookup. This makes it easier to merge upstream changes.
+ "Complete Profile": L10N.getStr("profiler.completeProfile"),
+ "Sample Range": L10N.getStr("profiler.sampleRange"),
+ "Running Time": L10N.getStr("profiler.runningTime"),
+ "Self": L10N.getStr("profiler.self"),
+ "Symbol Name": L10N.getStr("profiler.symbolName"),
+
+ getStr: function (name) {
+ return L10N.getStr(name);
+ },
+
+ getFormatStr: function (name, params) {
+ return L10N.getFormatStr(name, params);
+ }
+}; \ No newline at end of file
diff --git a/browser/devtools/profiler/cleopatra/js/tree.js b/browser/devtools/profiler/cleopatra/js/tree.js
new file mode 100644
index 000000000..07fdbba2a
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/tree.js
@@ -0,0 +1,705 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 kMaxChunkDuration = 30; // ms
+
+function escapeHTML(html) {
+ var pre = document.createElementNS("http://www.w3.org/1999/xhtml", "pre");
+ var text = document.createTextNode(html);
+ pre.appendChild(text);
+ return pre.innerHTML;
+}
+
+RegExp.escape = function(text) {
+ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
+
+var requestAnimationFrame = window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback, element) {
+ return window.setTimeout(callback, 1000 / 60);
+ };
+
+var cancelAnimationFrame = window.webkitCancelAnimationFrame ||
+ window.mozCancelAnimationFrame ||
+ window.oCancelAnimationFrame ||
+ window.msCancelAnimationFrame ||
+ function(req) {
+ window.clearTimeout(req);
+ };
+
+function TreeView() {
+ this._eventListeners = {};
+ this._pendingActions = [];
+ this._pendingActionsProcessingCallback = null;
+
+ this._container = document.createElement("div");
+ this._container.className = "treeViewContainer";
+ this._container.setAttribute("tabindex", "0"); // make it focusable
+
+ this._header = document.createElement("ul");
+ this._header.className = "treeHeader";
+ this._container.appendChild(this._header);
+
+ this._verticalScrollbox = document.createElement("div");
+ this._verticalScrollbox.className = "treeViewVerticalScrollbox";
+ this._container.appendChild(this._verticalScrollbox);
+
+ this._leftColumnBackground = document.createElement("div");
+ this._leftColumnBackground.className = "leftColumnBackground";
+ this._verticalScrollbox.appendChild(this._leftColumnBackground);
+
+ this._horizontalScrollbox = document.createElement("div");
+ this._horizontalScrollbox.className = "treeViewHorizontalScrollbox";
+ this._verticalScrollbox.appendChild(this._horizontalScrollbox);
+
+ this._styleElement = document.createElement("style");
+ this._styleElement.setAttribute("type", "text/css");
+ this._container.appendChild(this._styleElement);
+
+ this._contextMenu = document.createElement("menu");
+ this._contextMenu.setAttribute("type", "context");
+ this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++;
+ this._container.appendChild(this._contextMenu);
+
+ this._busyCover = document.createElement("div");
+ this._busyCover.className = "busyCover";
+ this._container.appendChild(this._busyCover);
+ this._abortToggleAll = false;
+ this.initSelection = true;
+
+ var self = this;
+ this._container.onkeydown = function (e) {
+ self._onkeypress(e);
+ };
+ this._container.onkeypress = function (e) {
+ // on key down gives us '8' and mapping shift+8='*' may not be portable.
+ if (String.fromCharCode(e.charCode) == '*')
+ self._onkeypress(e);
+ };
+ this._container.onclick = function (e) {
+ self._onclick(e);
+ };
+ this._verticalScrollbox.addEventListener("contextmenu", function(event) {
+ self._populateContextMenu(event);
+ }, true);
+ this._setUpScrolling();
+};
+TreeView.instanceCounter = 0;
+
+TreeView.prototype = {
+ getContainer: function TreeView_getContainer() {
+ return this._container;
+ },
+ setColumns: function TreeView_setColumns(columns) {
+ this._header.innerHTML = "";
+ for (var i = 0; i < columns.length; i++) {
+ var li = document.createElement("li");
+ li.className = "treeColumnHeader treeColumnHeader" + i;
+ li.id = columns[i].name + "Header";
+ li.textContent = columns[i].title;
+ this._header.appendChild(li);
+ }
+ },
+ dataIsOutdated: function TreeView_dataIsOutdated() {
+ this._busyCover.classList.add("busy");
+ },
+ display: function TreeView_display(data, resources, filterByName) {
+ this._busyCover.classList.remove("busy");
+ this._filterByName = filterByName;
+ this._resources = resources;
+ this._addResourceIconStyles();
+ this._filterByNameReg = null; // lazy init
+ if (this._filterByName === "")
+ this._filterByName = null;
+ this._horizontalScrollbox.innerHTML = "";
+ this._horizontalScrollbox.data = data[0].getData();
+ if (this._pendingActionsProcessingCallback) {
+ cancelAnimationFrame(this._pendingActionsProcessingCallback);
+ this._pendingActionsProcessingCallback = 0;
+ }
+ this._pendingActions = [];
+
+ this._pendingActions.push({
+ parentElement: this._horizontalScrollbox,
+ parentNode: null,
+ data: data[0].getData()
+ });
+ this._processPendingActionsChunk();
+ if (this._initSelection === true) {
+ this._initSelection = false;
+ this._select(this._horizontalScrollbox.firstChild);
+ this._toggle(this._horizontalScrollbox.firstChild);
+ }
+ changeFocus(this._container);
+ },
+ // Provide a snapshot of the reverse selection to restore with 'invert callback'
+ getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) {
+ if (!this._selectedNode)
+ return;
+ var snapshot = [];
+ var curr = this._selectedNode.data;
+
+ while(curr) {
+ if (isJavascriptOnly && curr.isJSFrame || !isJavascriptOnly) {
+ snapshot.push(curr.name);
+ //dump(JSON.stringify(curr.name) + "\n");
+ }
+ if (curr.treeChildren && curr.treeChildren.length >= 1) {
+ curr = curr.treeChildren[0].getData();
+ } else {
+ break;
+ }
+ }
+
+ return snapshot.reverse();
+ },
+ // Provide a snapshot of the current selection to restore
+ getSelectionSnapshot: function TreeView__getSelectionSnapshot(isJavascriptOnly) {
+ var snapshot = [];
+ var curr = this._selectedNode;
+
+ while(curr) {
+ if (isJavascriptOnly && curr.data.isJSFrame || !isJavascriptOnly) {
+ snapshot.push(curr.data.name);
+ //dump(JSON.stringify(curr.data.name) + "\n");
+ }
+ curr = curr.treeParent;
+ }
+
+ return snapshot.reverse();
+ },
+ setSelection: function TreeView_setSelection(frames) {
+ this.restoreSelectionSnapshot(frames, false);
+ },
+ // Take a selection snapshot and restore the selection
+ restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContigious) {
+ //console.log("restore selection: " + JSON.stringify(snapshot));
+ var currNode = this._horizontalScrollbox.firstChild;
+ if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") {
+ snapshot.shift();
+ }
+ //dump("len: " + snapshot.length + "\n");
+ next_level: while (currNode && snapshot.length > 0) {
+ this._toggle(currNode, false, true);
+ this._syncProcessPendingActionProcessing();
+ for (var i = 0; i < currNode.treeChildren.length; i++) {
+ if (currNode.treeChildren[i].data.name == snapshot[0]) {
+ //console.log("Found: " + currNode.treeChildren[i].data.name + "\n");
+ snapshot.shift();
+ this._toggle(currNode, false, true);
+ currNode = currNode.treeChildren[i];
+ continue next_level;
+ }
+ }
+ if (allowNonContigious === true) {
+ // We need to do a Breadth-first search to find a match
+ var pendingSearch = [currNode.data];
+ while (pendingSearch.length > 0) {
+ var node = pendingSearch.shift();
+ //console.log("searching: " + node.name + " for: " + snapshot[0] + "\n");
+ if (!node.treeChildren)
+ continue;
+ for (var i = 0; i < node.treeChildren.length; i++) {
+ var childNode = node.treeChildren[i].getData();
+ if (childNode.name == snapshot[0]) {
+ //dump("found: " + childNode.name + "\n");
+ snapshot.shift();
+ var nodesToToggle = [childNode];
+ while (nodesToToggle[0].name != currNode.data.name) {
+ nodesToToggle.splice(0, 0, nodesToToggle[0].parent);
+ }
+ var lastToggle = currNode;
+ for (var j = 0; j < nodesToToggle.length; j++) {
+ for (var k = 0; k < lastToggle.treeChildren.length; k++) {
+ if (lastToggle.treeChildren[k].data.name == nodesToToggle[j].name) {
+ //dump("Expend: " + nodesToToggle[j].name + "\n");
+ this._toggle(lastToggle.treeChildren[k], false, true);
+ lastToggle = lastToggle.treeChildren[k];
+ this._syncProcessPendingActionProcessing();
+ }
+ }
+ }
+ currNode = lastToggle;
+ continue next_level;
+ }
+ //dump("pending: " + childNode.name + "\n");
+ pendingSearch.push(childNode);
+ }
+ }
+ }
+ break; // Didn't find child node matching
+ }
+
+ if (currNode == this._horizontalScrollbox) {
+ PROFILERERROR("Failed to restore selection, could not find root.\n");
+ return;
+ }
+
+ this._toggle(currNode, true, true);
+ this._select(currNode);
+ },
+ _processPendingActionsChunk: function TreeView__processPendingActionsChunk(isSync) {
+ this._pendingActionsProcessingCallback = 0;
+
+ var startTime = Date.now();
+ var endTime = startTime + kMaxChunkDuration;
+ while ((isSync == true || Date.now() < endTime) && this._pendingActions.length > 0) {
+ this._processOneAction(this._pendingActions.shift());
+ }
+ this._scrollHeightChanged();
+
+ this._schedulePendingActionProcessing();
+ },
+ _schedulePendingActionProcessing: function TreeView__schedulePendingActionProcessing() {
+ if (!this._pendingActionsProcessingCallback && this._pendingActions.length > 0) {
+ var self = this;
+ this._pendingActionsProcessingCallback = requestAnimationFrame(function () {
+ self._processPendingActionsChunk();
+ });
+ }
+ },
+ _syncProcessPendingActionProcessing: function TreeView__syncProcessPendingActionProcessing() {
+ this._processPendingActionsChunk(true);
+ },
+ _processOneAction: function TreeView__processOneAction(action) {
+ var li = this._createTree(action.parentElement, action.parentNode, action.data);
+ if ("allChildrenCollapsedValue" in action) {
+ if (this._abortToggleAll)
+ return;
+ this._toggleAll(li, action.allChildrenCollapsedValue, true);
+ }
+ },
+ addEventListener: function TreeView_addEventListener(eventName, callbackFunction) {
+ if (!(eventName in this._eventListeners))
+ this._eventListeners[eventName] = [];
+ if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
+ return;
+ this._eventListeners[eventName].push(callbackFunction);
+ },
+ removeEventListener: function TreeView_removeEventListener(eventName, callbackFunction) {
+ if (!(eventName in this._eventListeners))
+ return;
+ var index = this._eventListeners[eventName].indexOf(callbackFunction);
+ if (index == -1)
+ return;
+ this._eventListeners[eventName].splice(index, 1);
+ },
+ _fireEvent: function TreeView__fireEvent(eventName, eventObject) {
+ if (!(eventName in this._eventListeners))
+ return;
+ this._eventListeners[eventName].forEach(function (callbackFunction) {
+ callbackFunction(eventObject);
+ });
+ },
+ _setUpScrolling: function TreeView__setUpScrolling() {
+ var waitingForPaint = false;
+ var accumulatedDeltaX = 0;
+ var accumulatedDeltaY = 0;
+ var self = this;
+ function scrollListener(e) {
+ if (!waitingForPaint) {
+ requestAnimationFrame(function () {
+ self._horizontalScrollbox.scrollLeft += accumulatedDeltaX;
+ self._verticalScrollbox.scrollTop += accumulatedDeltaY;
+ accumulatedDeltaX = 0;
+ accumulatedDeltaY = 0;
+ waitingForPaint = false;
+ });
+ waitingForPaint = true;
+ }
+ if (e.axis == e.HORIZONTAL_AXIS) {
+ accumulatedDeltaX += e.detail;
+ } else {
+ accumulatedDeltaY += e.detail;
+ }
+ e.preventDefault();
+ }
+ this._verticalScrollbox.addEventListener("MozMousePixelScroll", scrollListener, false);
+ this._verticalScrollbox.cleanUp = function () {
+ self._verticalScrollbox.removeEventListener("MozMousePixelScroll", scrollListener, false);
+ };
+ },
+ _scrollHeightChanged: function TreeView__scrollHeightChanged() {
+ if (!this._pendingScrollHeightChanged) {
+ var self = this;
+ this._pendingScrollHeightChanged = setTimeout(function() {
+ self._leftColumnBackground.style.height = self._horizontalScrollbox.getBoundingClientRect().height + 'px';
+ self._pendingScrollHeightChanged = null;
+ }, 0);
+ }
+ },
+ _createTree: function TreeView__createTree(parentElement, parentNode, data) {
+ var div = document.createElement("div");
+ div.className = "treeViewNode collapsed";
+ var hasChildren = ("children" in data) && (data.children.length > 0);
+ if (!hasChildren)
+ div.classList.add("leaf");
+ var treeLine = document.createElement("div");
+ treeLine.className = "treeLine";
+ treeLine.innerHTML = this._HTMLForFunction(data);
+ div.depth = parentNode ? parentNode.depth + 1 : 0;
+ div.style.marginLeft = div.depth + "em";
+ // When this item is toggled we will expand its children
+ div.pendingExpand = [];
+ div.treeLine = treeLine;
+ div.data = data;
+ // Useful for debugging
+ //this.uniqueID = this.uniqueID || 0;
+ //div.id = "Node" + this.uniqueID++;
+ div.appendChild(treeLine);
+ div.treeChildren = [];
+ div.treeParent = parentNode;
+ if (hasChildren) {
+ for (var i = 0; i < data.children.length; ++i) {
+ div.pendingExpand.push({parentElement: this._horizontalScrollbox, parentNode: div, data: data.children[i].getData() });
+ }
+ }
+ if (parentNode) {
+ parentNode.treeChildren.push(div);
+ }
+ if (parentNode != null) {
+ var nextTo;
+ if (parentNode.treeChildren.length > 1) {
+ nextTo = parentNode.treeChildren[parentNode.treeChildren.length-2].nextSibling;
+ } else {
+ nextTo = parentNode.nextSibling;
+ }
+ parentElement.insertBefore(div, nextTo);
+ } else {
+ parentElement.appendChild(div);
+ }
+ return div;
+ },
+ _addResourceIconStyles: function TreeView__addResourceIconStyles() {
+ var styles = [];
+ for (var resourceName in this._resources) {
+ var resource = this._resources[resourceName];
+ if (resource.icon) {
+ styles.push('.resourceIcon[data-resource="' + resourceName + '"] { background-image: url("' + resource.icon + '"); }');
+ }
+ }
+ this._styleElement.textContent = styles.join("\n");
+ },
+ _populateContextMenu: function TreeView__populateContextMenu(event) {
+ this._verticalScrollbox.setAttribute("contextmenu", "");
+
+ var target = event.target;
+ if (target.classList.contains("expandCollapseButton") ||
+ target.classList.contains("focusCallstackButton"))
+ return;
+
+ var li = this._getParentTreeViewNode(target);
+ if (!li)
+ return;
+
+ this._select(li);
+
+ this._contextMenu.innerHTML = "";
+
+ var self = this;
+ this._contextMenuForFunction(li.data).forEach(function (menuItem) {
+ var menuItemNode = document.createElement("menuitem");
+ menuItemNode.onclick = (function (menuItem) {
+ return function() {
+ self._contextMenuClick(li.data, menuItem);
+ };
+ })(menuItem);
+ menuItemNode.label = menuItem;
+ self._contextMenu.appendChild(menuItemNode);
+ });
+
+ this._verticalScrollbox.setAttribute("contextmenu", this._contextMenu.id);
+ },
+ _contextMenuClick: function TreeView__contextMenuClick(node, menuItem) {
+ this._fireEvent("contextMenuClick", { node: node, menuItem: menuItem });
+ },
+ _contextMenuForFunction: function TreeView__contextMenuForFunction(node) {
+ // TODO move me outside tree.js
+ var menu = [];
+ if (node.library && (
+ node.library.toLowerCase() == "lib_xul" ||
+ node.library.toLowerCase() == "lib_xul.dll"
+ )) {
+ menu.push("View Source");
+ }
+ if (node.isJSFrame && node.scriptLocation) {
+ menu.push("View JS Source");
+ }
+ menu.push("Focus Frame");
+ menu.push("Focus Callstack");
+ menu.push("Google Search");
+ menu.push("Plugin View: Pie");
+ menu.push("Plugin View: Tree");
+ return menu;
+ },
+ _HTMLForFunction: function TreeView__HTMLForFunction(node) {
+ var nodeName = escapeHTML(node.name);
+ var resource = this._resources[node.library] || {};
+ var libName = escapeHTML(resource.name || "");
+ if (this._filterByName) {
+ if (!this._filterByNameReg) {
+ this._filterByName = RegExp.escape(this._filterByName);
+ this._filterByNameReg = new RegExp("(" + this._filterByName + ")","gi");
+ }
+ nodeName = nodeName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
+ libName = libName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
+ }
+ var samplePercentage;
+ if (isNaN(node.ratio)) {
+ samplePercentage = "";
+ } else {
+ samplePercentage = (100 * node.ratio).toFixed(1) + "%";
+ }
+ return '<input type="button" value="Expand / Collapse" class="expandCollapseButton" tabindex="-1"> ' +
+ '<span class="sampleCount">' + node.counter + '</span> ' +
+ '<span class="samplePercentage">' + samplePercentage + '</span> ' +
+ '<span class="selfSampleCount">' + node.selfCounter + '</span> ' +
+ '<span class="resourceIcon" data-resource="' + node.library + '"></span> ' +
+ '<span class="functionName">' + nodeName + '</span>' +
+ '<span class="libraryName">' + libName + '</span>' +
+ (nodeName === '(total)' ? '' :
+ '<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">');
+ },
+ _resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) {
+ while (div.pendingExpand != null && div.pendingExpand.length > 0) {
+ var pendingExpand = div.pendingExpand.shift();
+ pendingExpand.allChildrenCollapsedValue = childrenCollapsedValue;
+ this._pendingActions.push(pendingExpand);
+ this._schedulePendingActionProcessing();
+ }
+ },
+ _showChild: function TreeView__showChild(div, isVisible) {
+ for (var i = 0; i < div.treeChildren.length; i++) {
+ div.treeChildren[i].style.display = isVisible?"":"none";
+ if (!isVisible) {
+ div.treeChildren[i].classList.add("collapsed");
+ this._showChild(div.treeChildren[i], isVisible);
+ }
+ }
+ },
+ _toggle: function TreeView__toggle(div, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) {
+ var currentCollapsedValue = this._isCollapsed(div);
+ if (newCollapsedValue === undefined)
+ newCollapsedValue = !currentCollapsedValue;
+ if (newCollapsedValue) {
+ div.classList.add("collapsed");
+ this._showChild(div, false);
+ } else {
+ this._resolveChildren(div, true);
+ div.classList.remove("collapsed");
+ this._showChild(div, true);
+ }
+ if (!suppressScrollHeightNotification)
+ this._scrollHeightChanged();
+ },
+ _toggleAll: function TreeView__toggleAll(subtreeRoot, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) {
+
+ // Reset abort
+ this._abortToggleAll = false;
+
+ // Expands / collapses all child nodes, too.
+
+ if (newCollapsedValue === undefined)
+ newCollapsedValue = !this._isCollapsed(subtreeRoot);
+ if (!newCollapsedValue) {
+ // expanding
+ this._resolveChildren(subtreeRoot, newCollapsedValue);
+ }
+ this._toggle(subtreeRoot, newCollapsedValue, true);
+ for (var i = 0; i < subtreeRoot.treeChildren.length; ++i) {
+ this._toggleAll(subtreeRoot.treeChildren[i], newCollapsedValue, true);
+ }
+ if (!suppressScrollHeightNotification)
+ this._scrollHeightChanged();
+ },
+ _getParent: function TreeView__getParent(div) {
+ return div.treeParent;
+ },
+ _getFirstChild: function TreeView__getFirstChild(div) {
+ if (this._isCollapsed(div))
+ return null;
+ var child = div.treeChildren[0];
+ return child;
+ },
+ _getLastChild: function TreeView__getLastChild(div) {
+ if (this._isCollapsed(div))
+ return div;
+ var lastChild = div.treeChildren[div.treeChildren.length-1];
+ if (lastChild == null)
+ return div;
+ return this._getLastChild(lastChild);
+ },
+ _getPrevSib: function TreeView__getPevSib(div) {
+ if (div.treeParent == null)
+ return null;
+ var nodeIndex = div.treeParent.treeChildren.indexOf(div);
+ if (nodeIndex == 0)
+ return null;
+ return div.treeParent.treeChildren[nodeIndex-1];
+ },
+ _getNextSib: function TreeView__getNextSib(div) {
+ if (div.treeParent == null)
+ return null;
+ var nodeIndex = div.treeParent.treeChildren.indexOf(div);
+ if (nodeIndex == div.treeParent.treeChildren.length - 1)
+ return this._getNextSib(div.treeParent);
+ return div.treeParent.treeChildren[nodeIndex+1];
+ },
+ _scheduleScrollIntoView: function TreeView__scheduleScrollIntoView(element, maxImportantWidth) {
+ // Schedule this on the animation frame otherwise we may run this more then once per frames
+ // causing more work then needed.
+ var self = this;
+ if (self._pendingAnimationFrame != null) {
+ return;
+ }
+ self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() {
+ cancelAnimationFrame(self._pendingAnimationFrame);
+ self._pendingAnimationFrame = null;
+ self._scrollIntoView(element, maxImportantWidth);
+ });
+ },
+ _scrollIntoView: function TreeView__scrollIntoView(element, maxImportantWidth) {
+ // Make sure that element is inside the visible part of our scrollbox by
+ // adjusting the scroll positions. If element is wider or
+ // higher than the scroll port, the left and top edges are prioritized over
+ // the right and bottom edges.
+ // If maxImportantWidth is set, parts of the beyond this widths are
+ // considered as not important; they'll not be moved into view.
+
+ if (maxImportantWidth === undefined)
+ maxImportantWidth = Infinity;
+
+ var visibleRect = {
+ left: this._horizontalScrollbox.getBoundingClientRect().left + 150, // TODO: un-hardcode 150
+ top: this._verticalScrollbox.getBoundingClientRect().top,
+ right: this._horizontalScrollbox.getBoundingClientRect().right,
+ bottom: this._verticalScrollbox.getBoundingClientRect().bottom
+ }
+ var r = element.getBoundingClientRect();
+ var right = Math.min(r.right, r.left + maxImportantWidth);
+ var leftCutoff = visibleRect.left - r.left;
+ var rightCutoff = right - visibleRect.right;
+ var topCutoff = visibleRect.top - r.top;
+ var bottomCutoff = r.bottom - visibleRect.bottom;
+ if (leftCutoff > 0)
+ this._horizontalScrollbox.scrollLeft -= leftCutoff;
+ else if (rightCutoff > 0)
+ this._horizontalScrollbox.scrollLeft += Math.min(rightCutoff, -leftCutoff);
+ if (topCutoff > 0)
+ this._verticalScrollbox.scrollTop -= topCutoff;
+ else if (bottomCutoff > 0)
+ this._verticalScrollbox.scrollTop += Math.min(bottomCutoff, -topCutoff);
+ },
+ _select: function TreeView__select(li) {
+ if (this._selectedNode != null) {
+ this._selectedNode.treeLine.classList.remove("selected");
+ this._selectedNode = null;
+ }
+ if (li) {
+ li.treeLine.classList.add("selected");
+ this._selectedNode = li;
+ var functionName = li.treeLine.querySelector(".functionName");
+ this._scheduleScrollIntoView(functionName, 400);
+ this._fireEvent("select", li.data);
+ }
+ updateDocumentURL();
+ },
+ _isCollapsed: function TreeView__isCollapsed(div) {
+ return div.classList.contains("collapsed");
+ },
+ _getParentTreeViewNode: function TreeView__getParentTreeViewNode(node) {
+ while (node) {
+ if (node.nodeType != node.ELEMENT_NODE)
+ break;
+ if (node.classList.contains("treeViewNode"))
+ return node;
+ node = node.parentNode;
+ }
+ return null;
+ },
+ _onclick: function TreeView__onclick(event) {
+ var target = event.target;
+ var node = this._getParentTreeViewNode(target);
+ if (!node)
+ return;
+ if (target.classList.contains("expandCollapseButton")) {
+ if (event.altKey)
+ this._toggleAll(node);
+ else
+ this._toggle(node);
+ } else if (target.classList.contains("focusCallstackButton")) {
+ this._fireEvent("focusCallstackButtonClicked", node.data);
+ } else {
+ this._select(node);
+ if (event.detail == 2) // dblclick
+ this._toggle(node);
+ }
+ },
+ _onkeypress: function TreeView__onkeypress(event) {
+ if (event.ctrlKey || event.altKey || event.metaKey)
+ return;
+
+ this._abortToggleAll = true;
+
+ var selected = this._selectedNode;
+ if (event.keyCode < 37 || event.keyCode > 40) {
+ if (event.keyCode != 0 ||
+ String.fromCharCode(event.charCode) != '*') {
+ return;
+ }
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ if (!selected)
+ return;
+ if (event.keyCode == 37) { // KEY_LEFT
+ var isCollapsed = this._isCollapsed(selected);
+ if (!isCollapsed) {
+ this._toggle(selected);
+ } else {
+ var parent = this._getParent(selected);
+ if (parent != null) {
+ this._select(parent);
+ }
+ }
+ } else if (event.keyCode == 38) { // KEY_UP
+ var prevSib = this._getPrevSib(selected);
+ var parent = this._getParent(selected);
+ if (prevSib != null) {
+ this._select(this._getLastChild(prevSib));
+ } else if (parent != null) {
+ this._select(parent);
+ }
+ } else if (event.keyCode == 39) { // KEY_RIGHT
+ var isCollapsed = this._isCollapsed(selected);
+ if (isCollapsed) {
+ this._toggle(selected);
+ this._syncProcessPendingActionProcessing();
+ } else {
+ // Do KEY_DOWN
+ var nextSib = this._getNextSib(selected);
+ var child = this._getFirstChild(selected);
+ if (child != null) {
+ this._select(child);
+ } else if (nextSib) {
+ this._select(nextSib);
+ }
+ }
+ } else if (event.keyCode == 40) { // KEY_DOWN
+ var nextSib = this._getNextSib(selected);
+ var child = this._getFirstChild(selected);
+ if (child != null) {
+ this._select(child);
+ } else if (nextSib) {
+ this._select(nextSib);
+ }
+ } else if (String.fromCharCode(event.charCode) == '*') {
+ this._toggleAll(selected);
+ }
+ },
+};
+
diff --git a/browser/devtools/profiler/cleopatra/js/ui.js b/browser/devtools/profiler/cleopatra/js/ui.js
new file mode 100644
index 000000000..d9a1966e6
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/ui.js
@@ -0,0 +1,2013 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 EIDETICKER_BASE_URL = "http://eideticker.wrla.ch/";
+
+var gDebugLog = false;
+var gDebugTrace = false;
+var gLocation = window.location + "";
+if (gLocation.indexOf("file:") == 0) {
+ gDebugLog = true;
+ gDebugTrace = true;
+ PROFILERLOG("Turning on logging+tracing since cleopatra is served from the file protocol");
+}
+// Use for verbose tracing, otherwise use log
+function PROFILERTRACE(msg) {
+ if (gDebugTrace)
+ PROFILERLOG(msg);
+}
+function PROFILERLOG(msg) {
+ if (gDebugLog) {
+ msg = "Cleo: " + msg;
+ console.log(msg);
+ if (window.dump)
+ window.dump(msg + "\n");
+ }
+}
+function PROFILERERROR(msg) {
+ msg = "Cleo: " + msg;
+ console.log(msg);
+ if (window.dump)
+ window.dump(msg + "\n");
+}
+function enableProfilerTracing() {
+ gDebugLog = true;
+ gDebugTrace = true;
+ Parser.updateLogSetting();
+}
+function enableProfilerLogging() {
+ gDebugLog = true;
+ Parser.updateLogSetting();
+}
+
+function removeAllChildren(element) {
+ while (element.firstChild) {
+ element.removeChild(element.firstChild);
+ }
+}
+
+function FileList() {
+ this._container = document.createElement("ul");
+ this._container.id = "fileList";
+ this._selectedFileItem = null;
+ this._fileItemList = [];
+}
+
+FileList.prototype = {
+ getContainer: function FileList_getContainer() {
+ return this._container;
+ },
+
+ clearFiles: function FileList_clearFiles() {
+ this.fileItemList = [];
+ this._selectedFileItem = null;
+ this._container.innerHTML = "";
+ },
+
+ loadProfileListFromLocalStorage: function FileList_loadProfileListFromLocalStorage() {
+ var self = this;
+ gLocalStorage.getProfileList(function(profileList) {
+ for (var i = profileList.length - 1; i >= 0; i--) {
+ (function closure() {
+ // This only carries info about the profile and the access key to retrieve it.
+ var profileInfo = profileList[i];
+ //PROFILERTRACE("Profile list from local storage: " + JSON.stringify(profileInfo));
+ var dateObj = new Date(profileInfo.date);
+ var fileEntry = self.addFile(profileInfo, dateObj.toLocaleString(), function fileEntryClick() {
+ PROFILERLOG("open: " + profileInfo.profileKey + "\n");
+ loadLocalStorageProfile(profileInfo.profileKey);
+ });
+ })();
+ }
+ });
+ gLocalStorage.onProfileListChange(function(profileList) {
+ self.clearFiles();
+ self.loadProfileListFromLocalStorage();
+ });
+ },
+
+ addFile: function FileList_addFile(profileInfo, description, onselect) {
+ var li = document.createElement("li");
+
+ var fileName;
+ if (profileInfo.profileKey && profileInfo.profileKey.indexOf("http://profile-store.commondatastorage.googleapis.com/") >= 0) {
+ fileName = profileInfo.profileKey.substring(54);
+ fileName = fileName.substring(0, 8) + "..." + fileName.substring(28);
+ } else {
+ fileName = profileInfo.name;
+ }
+ li.fileName = fileName || "(New Profile)";
+ li.description = description || "(empty)";
+
+ li.className = "fileListItem";
+ if (!this._selectedFileItem) {
+ li.classList.add("selected");
+ this._selectedFileItem = li;
+ }
+
+ var self = this;
+ li.onclick = function() {
+ self.setSelection(li);
+ if (onselect)
+ onselect();
+ }
+
+ var fileListItemTitleSpan = document.createElement("span");
+ fileListItemTitleSpan.className = "fileListItemTitle";
+ fileListItemTitleSpan.textContent = li.fileName;
+ li.appendChild(fileListItemTitleSpan);
+
+ var fileListItemDescriptionSpan = document.createElement("span");
+ fileListItemDescriptionSpan.className = "fileListItemDescription";
+ fileListItemDescriptionSpan.textContent = li.description;
+ li.appendChild(fileListItemDescriptionSpan);
+
+ this._container.appendChild(li);
+
+ this._fileItemList.push(li);
+
+ return li;
+ },
+
+ setSelection: function FileList_setSelection(fileEntry) {
+ if (this._selectedFileItem) {
+ this._selectedFileItem.classList.remove("selected");
+ }
+ this._selectedFileItem = fileEntry;
+ fileEntry.classList.add("selected");
+ if (this._selectedFileItem.onselect)
+ this._selectedFileItem.onselect();
+ },
+
+ profileParsingFinished: function FileList_profileParsingFinished() {
+ //this._container.querySelector(".fileListItemTitle").textContent = "Current Profile";
+ //this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples";
+ }
+}
+
+function treeObjSort(a, b) {
+ return b.counter - a.counter;
+}
+
+function ProfileTreeManager() {
+ this.treeView = new TreeView();
+ this.treeView.setColumns([
+ { name: "sampleCount", title: gStrings["Running Time"] },
+ { name: "selfSampleCount", title: gStrings["Self"] },
+ { name: "resource", title: "" },
+ { name: "symbolName", title: gStrings["Symbol Name"] }
+ ]);
+ var self = this;
+ this.treeView.addEventListener("select", function (frameData) {
+ self.highlightFrame(frameData);
+ if (window.comparator_setSelection) {
+ window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), frameData);
+ }
+ });
+ this.treeView.addEventListener("contextMenuClick", function (e) {
+ self._onContextMenuClick(e);
+ });
+ this.treeView.addEventListener("focusCallstackButtonClicked", function (frameData) {
+ // NOTE: Not in the original Cleopatra source code.
+ notifyParent("displaysource", {
+ line: frameData.scriptLocation.lineInformation,
+ uri: frameData.scriptLocation.scriptURI,
+ isChrome: /^otherhost_*/.test(frameData.library)
+ });
+ });
+ this._container = document.createElement("div");
+ this._container.className = "tree";
+ this._container.appendChild(this.treeView.getContainer());
+
+ // If this is set when the tree changes the snapshot is immediately restored.
+ this._savedSnapshot = null;
+}
+ProfileTreeManager.prototype = {
+ getContainer: function ProfileTreeManager_getContainer() {
+ return this._container;
+ },
+ highlightFrame: function Treedisplay_highlightFrame(frameData) {
+ setHighlightedCallstack(this._getCallstackUpTo(frameData), this._getHeaviestCallstack(frameData));
+ },
+ dataIsOutdated: function ProfileTreeManager_dataIsOutdated() {
+ this.treeView.dataIsOutdated();
+ },
+ saveSelectionSnapshot: function ProfileTreeManager_saveSelectionSnapshot(isJavascriptOnly) {
+ this._savedSnapshot = this.treeView.getSelectionSnapshot(isJavascriptOnly);
+ },
+ saveReverseSelectionSnapshot: function ProfileTreeManager_saveReverseSelectionSnapshot(isJavascriptOnly) {
+ this._savedSnapshot = this.treeView.getReverseSelectionSnapshot(isJavascriptOnly);
+ },
+ hasNonTrivialSelection: function ProfileTreeManager_hasNonTrivialSelection() {
+ return this.treeView.getSelectionSnapshot().length > 1;
+ },
+ serializeCurrentSelectionSnapshot: function ProfileTreeManager_serializeCurrentSelectionSnapshot() {
+ return JSON.stringify(this.treeView.getSelectionSnapshot());
+ },
+ restoreSerializedSelectionSnapshot: function ProfileTreeManager_restoreSerializedSelectionSnapshot(selection) {
+ this._savedSnapshot = JSON.parse(selection);
+ },
+ _restoreSelectionSnapshot: function ProfileTreeManager__restoreSelectionSnapshot(snapshot, allowNonContigous) {
+ return this.treeView.restoreSelectionSnapshot(snapshot, allowNonContigous);
+ },
+ setSelection: function ProfileTreeManager_setSelection(frames) {
+ return this.treeView.setSelection(frames);
+ },
+ _getCallstackUpTo: function ProfileTreeManager__getCallstackUpTo(frame) {
+ var callstack = [];
+ var curr = frame;
+ while (curr != null) {
+ if (curr.name != null) {
+ var subCallstack = curr.fullFrameNamesAsInSample.clone();
+ subCallstack.reverse();
+ callstack = callstack.concat(subCallstack);
+ }
+ curr = curr.parent;
+ }
+ callstack.reverse();
+ if (gInvertCallstack)
+ callstack.shift(); // remove (total)
+ return callstack;
+ },
+ _getHeaviestCallstack: function ProfileTreeManager__getHeaviestCallstack(frame) {
+ // FIXME: This gets the first leaf which is not the heaviest leaf.
+ while(frame.children && frame.children.length > 0) {
+ var nextFrame = frame.children[0].getData();
+ if (!nextFrame)
+ break;
+ frame = nextFrame;
+ }
+ return this._getCallstackUpTo(frame);
+ },
+ _onContextMenuClick: function ProfileTreeManager__onContextMenuClick(e) {
+ var node = e.node;
+ var menuItem = e.menuItem;
+
+ if (menuItem == "View Source") {
+ // Remove anything after ( since MXR doesn't handle search with the arguments.
+ var symbol = node.name.split("(")[0];
+ window.open("http://mxr.mozilla.org/mozilla-central/search?string=" + symbol, "View Source");
+ } else if (menuItem == "View JS Source") {
+ viewJSSource(node);
+ } else if (menuItem == "Plugin View: Pie") {
+ focusOnPluginView("protovis", {type:"pie"});
+ } else if (menuItem == "Plugin View: Tree") {
+ focusOnPluginView("protovis", {type:"tree"});
+ } else if (menuItem == "Google Search") {
+ var symbol = node.name;
+ window.open("https://www.google.ca/search?q=" + symbol, "View Source");
+ } else if (menuItem == "Focus Frame") {
+ var symbol = node.fullFrameNamesAsInSample[0]; // TODO: we only function one symbol when callpath merging is on, fix that
+ focusOnSymbol(symbol, node.name);
+ } else if (menuItem == "Focus Callstack") {
+ var focusedCallstack = this._getCallstackUpTo(node);
+ focusOnCallstack(focusedCallstack, node.name);
+ }
+ },
+ setAllowNonContigous: function ProfileTreeManager_setAllowNonContigous() {
+ this._allowNonContigous = true;
+ },
+ display: function ProfileTreeManager_display(tree, symbols, functions, resources, useFunctions, filterByName) {
+ this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), resources, filterByName);
+ if (this._savedSnapshot) {
+ var old = this._savedSnapshot.clone();
+ this._restoreSelectionSnapshot(this._savedSnapshot, this._allowNonContigous);
+ this._savedSnapshot = old;
+ this._allowNonContigous = false;
+ }
+ },
+ convertToJSTreeData: function ProfileTreeManager__convertToJSTreeData(rootNode, symbols, functions, useFunctions) {
+ var totalSamples = rootNode.counter;
+ function createTreeViewNode(node, parent) {
+ var curObj = {};
+ curObj.parent = parent;
+ curObj.counter = node.counter;
+ var selfCounter = node.counter;
+ for (var i = 0; i < node.children.length; ++i) {
+ selfCounter -= node.children[i].counter;
+ }
+ curObj.selfCounter = selfCounter;
+ curObj.ratio = node.counter / totalSamples;
+ curObj.fullFrameNamesAsInSample = node.mergedNames ? node.mergedNames : [node.name];
+ if (!(node.name in (useFunctions ? functions : symbols))) {
+ curObj.name = node.name;
+ curObj.library = "";
+ } else {
+ var functionObj = useFunctions ? functions[node.name] : functions[symbols[node.name].functionIndex];
+ var info = {
+ functionName: functionObj.functionName,
+ libraryName: functionObj.libraryName,
+ lineInformation: useFunctions ? "" : symbols[node.name].lineInformation
+ };
+ curObj.name = (info.functionName + " " + info.lineInformation).trim();
+ curObj.library = info.libraryName;
+ curObj.isJSFrame = functionObj.isJSFrame;
+ if (functionObj.scriptLocation) {
+ curObj.scriptLocation = functionObj.scriptLocation;
+ }
+ }
+ if (node.children.length) {
+ curObj.children = getChildrenObjects(node.children, curObj);
+ }
+ return curObj;
+ }
+ function getChildrenObjects(children, parent) {
+ var sortedChildren = children.slice(0).sort(treeObjSort);
+ return sortedChildren.map(function (child) {
+ var createdNode = null;
+ return {
+ getData: function () {
+ if (!createdNode) {
+ createdNode = createTreeViewNode(child, parent);
+ }
+ return createdNode;
+ }
+ };
+ });
+ }
+ return getChildrenObjects([rootNode], null);
+ },
+};
+
+function SampleBar() {
+ this._container = document.createElement("div");
+ this._container.id = "sampleBar";
+ this._container.className = "sideBar";
+
+ this._header = document.createElement("h2");
+ this._header.innerHTML = "Selection - Most time spent in:";
+ this._header.alt = "This shows the heaviest leaf of the selected sample. Use this to get a quick glimpse of where the selection is spending most of its time.";
+ this._container.appendChild(this._header);
+
+ this._text = document.createElement("ul");
+ this._text.style.whiteSpace = "pre";
+ this._text.innerHTML = "Sample text";
+ this._container.appendChild(this._text);
+}
+
+SampleBar.prototype = {
+ getContainer: function SampleBar_getContainer() {
+ return this._container;
+ },
+ setSample: function SampleBar_setSample(sample) {
+ var str = "";
+ var list = [];
+
+ this._text.innerHTML = "";
+
+ for (var i = 0; i < sample.length; i++) {
+ var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+ if (!functionObj)
+ continue;
+ var functionItem = document.createElement("li");
+ var functionLink = document.createElement("a");
+ functionLink.textContent = functionLink.title = functionObj.functionName;
+ functionLink.href = "#";
+ functionItem.appendChild(functionLink);
+ this._text.appendChild(functionItem);
+ list.push(functionObj.functionName);
+ functionLink.selectIndex = i;
+ functionLink.onclick = function() {
+ var selectedFrames = [];
+ if (gInvertCallstack) {
+ for (var i = 0; i <= this.selectIndex; i++) {
+ var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+ selectedFrames.push(functionObj.functionName);
+ }
+ } else {
+ for (var i = sample.length - 1; i >= this.selectIndex; i--) {
+ var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+ selectedFrames.push(functionObj.functionName);
+ }
+ }
+ gTreeManager.setSelection(selectedFrames);
+ return false;
+ }
+ }
+ return list;
+ },
+}
+
+
+function PluginView() {
+ this._container = document.createElement("div");
+ this._container.className = "pluginview";
+ this._container.style.visibility = 'hidden';
+ this._iframe = document.createElement("iframe");
+ this._iframe.className = "pluginviewIFrame";
+ this._container.appendChild(this._iframe);
+ this._container.style.top = "";
+}
+PluginView.prototype = {
+ getContainer: function PluginView_getContainer() {
+ return this._container;
+ },
+ hide: function() {
+ // get rid of the scrollbars
+ this._container.style.top = "";
+ this._container.style.visibility = 'hidden';
+ },
+ show: function() {
+ // This creates extra scrollbar so only do it when needed
+ this._container.style.top = "0px";
+ this._container.style.visibility = '';
+ },
+ display: function(pluginName, param, data) {
+ this._iframe.src = "js/plugins/" + pluginName + "/index.html";
+ var self = this;
+ this._iframe.onload = function() {
+ self._iframe.contentWindow.initCleopatraPlugin(data, param, gSymbols);
+ }
+ this.show();
+ },
+}
+
+function HistogramView() {
+ this._container = document.createElement("div");
+ this._container.className = "histogram";
+
+ this._canvas = this._createCanvas();
+ this._container.appendChild(this._canvas);
+
+ this._rangeSelector = new RangeSelector(this._canvas, this);
+ this._rangeSelector.enableRangeSelectionOnHistogram();
+ this._container.appendChild(this._rangeSelector.getContainer());
+
+ this._busyCover = document.createElement("div");
+ this._busyCover.className = "busyCover";
+ this._container.appendChild(this._busyCover);
+
+ this._histogramData = [];
+}
+HistogramView.prototype = {
+ dataIsOutdated: function HistogramView_dataIsOutdated() {
+ this._busyCover.classList.add("busy");
+ },
+ _createCanvas: function HistogramView__createCanvas() {
+ var canvas = document.createElement("canvas");
+ canvas.height = 60;
+ canvas.style.width = "100%";
+ canvas.style.height = "100%";
+ return canvas;
+ },
+ getContainer: function HistogramView_getContainer() {
+ return this._container;
+ },
+ selectRange: function HistogramView_selectRange(start, end) {
+ this._rangeSelector._finishSelection(start, end);
+ },
+ showVideoFramePosition: function HistogramView_showVideoFramePosition(frame) {
+ if (!this._frameStart || !this._frameStart[frame])
+ return;
+ var frameStart = this._frameStart[frame];
+ // Now we look for the frame end. Because we can swap frame we don't present we have to look ahead
+ // in the stream if frame+1 doesn't exist.
+ var frameEnd = this._frameStart[frame+1];
+ for (var i = 0; i < 10 && !frameEnd; i++) {
+ frameEnd = this._frameStart[frame+1+i];
+ }
+ this._rangeSelector.showVideoRange(frameStart, frameEnd);
+ },
+ showVideoPosition: function HistogramView_showVideoPosition(position) {
+ // position in 0..1
+ this._rangeSelector.showVideoPosition(position);
+ },
+ _gatherMarkersList: function HistogramView__gatherMarkersList(histogramData) {
+ var markers = [];
+ for (var i = 0; i < histogramData.length; ++i) {
+ var step = histogramData[i];
+ if ("marker" in step) {
+ markers.push({
+ index: i,
+ name: step.marker
+ });
+ }
+ }
+ return markers;
+ },
+ _calculateWidthMultiplier: function () {
+ var minWidth = 2000;
+ return Math.ceil(minWidth / this._widthSum);
+ },
+ histogramClick: function HistogramView_histogramClick(index) {
+ var sample = this._histogramData[index];
+ var frames = sample.frames;
+ var list = gSampleBar.setSample(frames[0]);
+ gTreeManager.setSelection(list);
+ setHighlightedCallstack(frames[0], frames[0]);
+ },
+ display: function HistogramView_display(histogramData, frameStart, widthSum, highlightedCallstack) {
+ this._histogramData = histogramData;
+ this._frameStart = frameStart;
+ this._widthSum = widthSum;
+ this._widthMultiplier = this._calculateWidthMultiplier();
+ this._canvas.width = this._widthMultiplier * this._widthSum;
+ this._render(highlightedCallstack);
+ this._busyCover.classList.remove("busy");
+ },
+ _scheduleRender: function HistogramView__scheduleRender(highlightedCallstack) {
+ var self = this;
+ if (self._pendingAnimationFrame != null) {
+ return;
+ }
+ self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() {
+ cancelAnimationFrame(self._pendingAnimationFrame);
+ self._pendingAnimationFrame = null;
+ self._render(highlightedCallstack);
+ self._busyCover.classList.remove("busy");
+ });
+ },
+ _render: function HistogramView__render(highlightedCallstack) {
+ var ctx = this._canvas.getContext("2d");
+ var height = this._canvas.height;
+ ctx.setTransform(this._widthMultiplier, 0, 0, 1, 0, 0);
+ ctx.font = "20px Georgia";
+ ctx.clearRect(0, 0, this._widthSum, height);
+
+ var self = this;
+ var markerCount = 0;
+ for (var i = 0; i < this._histogramData.length; i++) {
+ var step = this._histogramData[i];
+ var isSelected = self._isStepSelected(step, highlightedCallstack);
+ var isInRangeSelector = self._isInRangeSelector(i);
+ if (isSelected) {
+ ctx.fillStyle = "green";
+ } else if (isInRangeSelector) {
+ ctx.fillStyle = "blue";
+ } else {
+ ctx.fillStyle = step.color;
+ }
+ var roundedHeight = Math.round(step.value * height);
+ ctx.fillRect(step.x, height - roundedHeight, step.width, roundedHeight);
+ if (step.marker) {
+ var x = step.x + step.width + 2;
+ var endPoint = x + ctx.measureText(step.marker).width;
+ var lastDataPoint = this._histogramData[this._histogramData.length-1];
+ if (endPoint >= lastDataPoint.x + lastDataPoint.width) {
+ x -= endPoint - (lastDataPoint.x + lastDataPoint.width) - 1;
+ }
+ ctx.fillText(step.marker, x, 15 + ((markerCount % 2) == 0 ? 0 : 20));
+ markerCount++;
+ }
+ }
+
+ this._finishedRendering = true;
+ },
+ highlightedCallstackChanged: function HistogramView_highlightedCallstackChanged(highlightedCallstack) {
+ this._scheduleRender(highlightedCallstack);
+ },
+ _isInRangeSelector: function HistogramView_isInRangeSelector(index) {
+ return false;
+ },
+ _isStepSelected: function HistogramView__isStepSelected(step, highlightedCallstack) {
+ if ("marker" in step)
+ return false;
+
+ search_frames: for (var i = 0; i < step.frames.length; i++) {
+ var frames = step.frames[i];
+
+ if (frames.length < highlightedCallstack.length ||
+ highlightedCallstack.length <= (gInvertCallstack ? 0 : 1))
+ continue;
+
+ var compareFrames = frames;
+ if (gInvertCallstack) {
+ for (var j = 0; j < highlightedCallstack.length; j++) {
+ var compareFrameIndex = compareFrames.length - 1 - j;
+ if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) {
+ continue search_frames;
+ }
+ }
+ } else {
+ for (var j = 0; j < highlightedCallstack.length; j++) {
+ var compareFrameIndex = j;
+ if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) {
+ continue search_frames;
+ }
+ }
+ }
+ return true;
+ };
+ return false;
+ },
+ getHistogramData: function HistogramView__getHistogramData() {
+ return this._histogramData;
+ },
+ _getStepColor: function HistogramView__getStepColor(step) {
+ if ("responsiveness" in step.extraInfo) {
+ var res = step.extraInfo.responsiveness;
+ var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness));
+ return "rgb(" + redComponent + ",0,0)";
+ }
+
+ return "rgb(0,0,0)";
+ },
+};
+
+function RangeSelector(graph, histogram) {
+ this._histogram = histogram;
+ this.container = document.createElement("div");
+ this.container.className = "rangeSelectorContainer";
+ this._graph = graph;
+ this._selectedRange = { startX: 0, endX: 0 };
+ this._selectedSampleRange = { start: 0, end: 0 };
+
+ this._highlighter = document.createElement("div");
+ this._highlighter.className = "histogramHilite collapsed";
+ this.container.appendChild(this._highlighter);
+
+ this._mouseMarker = document.createElement("div");
+ this._mouseMarker.className = "histogramMouseMarker";
+ this.container.appendChild(this._mouseMarker);
+}
+RangeSelector.prototype = {
+ getContainer: function RangeSelector_getContainer() {
+ return this.container;
+ },
+ // echo the location off the mouse on the histogram
+ drawMouseMarker: function RangeSelector_drawMouseMarker(x) {
+ console.log("Draw");
+ var mouseMarker = this._mouseMarker;
+ mouseMarker.style.left = x + "px";
+ },
+ showVideoPosition: function RangeSelector_showVideoPosition(position) {
+ this.drawMouseMarker(position * (this._graph.parentNode.clientWidth-1));
+ PROFILERLOG("Show video position: " + position);
+ },
+ drawHiliteRectangle: function RangeSelector_drawHiliteRectangle(x, y, width, height) {
+ var hilite = this._highlighter;
+ hilite.style.left = x + "px";
+ hilite.style.top = "0";
+ hilite.style.width = width + "px";
+ hilite.style.height = height + "px";
+ },
+ clearCurrentRangeSelection: function RangeSelector_clearCurrentRangeSelection() {
+ try {
+ this.changeEventSuppressed = true;
+ var children = this.selector.childNodes;
+ for (var i = 0; i < children.length; ++i) {
+ children[i].selected = false;
+ }
+ } finally {
+ this.changeEventSuppressed = false;
+ }
+ },
+ showVideoRange: function RangeSelector_showVideoRange(startIndex, endIndex) {
+ if (!endIndex || endIndex < 0)
+ endIndex = gCurrentlyShownSampleData.length;
+
+ var len = this._graph.parentNode.getBoundingClientRect().right - this._graph.parentNode.getBoundingClientRect().left;
+ this._selectedRange.startX = startIndex * len / this._histogram._histogramData.length;
+ this._selectedRange.endX = endIndex * len / this._histogram._histogramData.length;
+ var width = this._selectedRange.endX - this._selectedRange.startX;
+ var height = this._graph.parentNode.clientHeight;
+ this._highlighter.classList.remove("collapsed");
+ this.drawHiliteRectangle(this._selectedRange.startX, 0, width, height);
+ //this._finishSelection(startIndex, endIndex);
+ },
+ enableRangeSelectionOnHistogram: function RangeSelector_enableRangeSelectionOnHistogram() {
+ var graph = this._graph;
+ var isDrawingRectangle = false;
+ var origX, origY;
+ var self = this;
+ // Compute this on the mouse down rather then forcing a sync reflow
+ // every frame.
+ var boundingRect = null;
+ function histogramClick(clickX, clickY) {
+ clickX = Math.min(clickX, graph.parentNode.getBoundingClientRect().right);
+ clickX = clickX - graph.parentNode.getBoundingClientRect().left;
+ var index = self._histogramIndexFromPoint(clickX);
+ self._histogram.histogramClick(index);
+ }
+ function updateHiliteRectangle(newX, newY) {
+ newX = Math.min(newX, boundingRect.right);
+ var startX = Math.min(newX, origX) - boundingRect.left;
+ var startY = 0;
+ var width = Math.abs(newX - origX);
+ var height = graph.parentNode.clientHeight;
+ if (startX < 0) {
+ width += startX;
+ startX = 0;
+ }
+ self._selectedRange.startX = startX;
+ self._selectedRange.endX = startX + width;
+ self.drawHiliteRectangle(startX, startY, width, height);
+ }
+ function updateMouseMarker(newX) {
+ self.drawMouseMarker(newX - graph.parentNode.getBoundingClientRect().left);
+ }
+ graph.addEventListener("mousedown", function(e) {
+ if (e.button != 0)
+ return;
+ graph.style.cursor = "col-resize";
+ isDrawingRectangle = true;
+ self.beginHistogramSelection();
+ origX = e.pageX;
+ origY = e.pageY;
+ boundingRect = graph.parentNode.getBoundingClientRect();
+ if (this.setCapture)
+ this.setCapture();
+ // Reset the highlight rectangle
+ updateHiliteRectangle(e.pageX, e.pageY);
+ e.preventDefault();
+ this._movedDuringClick = false;
+ }, false);
+ graph.addEventListener("mouseup", function(e) {
+ graph.style.cursor = "default";
+ if (!this._movedDuringClick) {
+ isDrawingRectangle = false;
+ // Handle as a click on the histogram. Select the sample:
+ histogramClick(e.pageX, e.pageY);
+ } else if (isDrawingRectangle) {
+ isDrawingRectangle = false;
+ updateHiliteRectangle(e.pageX, e.pageY);
+ self.finishHistogramSelection(e.pageX != origX);
+ if (e.pageX == origX) {
+ // Simple click in the histogram
+ var index = self._sampleIndexFromPoint(e.pageX - graph.parentNode.getBoundingClientRect().left);
+ // TODO Select this sample in the tree view
+ var sample = gCurrentlyShownSampleData[index];
+ }
+ }
+ }, false);
+ graph.addEventListener("mousemove", function(e) {
+ this._movedDuringClick = true;
+ if (isDrawingRectangle) {
+ updateMouseMarker(-1); // Clear
+ updateHiliteRectangle(e.pageX, e.pageY);
+ } else {
+ updateMouseMarker(e.pageX);
+ }
+ }, false);
+ graph.addEventListener("mouseout", function(e) {
+ updateMouseMarker(-1); // Clear
+ }, false);
+ },
+ beginHistogramSelection: function RangeSelector_beginHistgramSelection() {
+ var hilite = this._highlighter;
+ hilite.classList.remove("finished");
+ hilite.classList.add("selecting");
+ hilite.classList.remove("collapsed");
+ if (this._transientRestrictionEnteringAffordance) {
+ this._transientRestrictionEnteringAffordance.discard();
+ }
+ },
+ _finishSelection: function RangeSelector__finishSelection(start, end) {
+ var newFilterChain = gSampleFilters.concat({ type: "RangeSampleFilter", start: start, end: end });
+ var self = this;
+ self._transientRestrictionEnteringAffordance = gBreadcrumbTrail.add({
+ title: gStrings["Sample Range"] + " [" + start + ", " + (end + 1) + "]",
+ enterCallback: function () {
+ gSampleFilters = newFilterChain;
+ self.collapseHistogramSelection();
+ filtersChanged();
+ }
+ });
+ },
+ finishHistogramSelection: function RangeSelector_finishHistgramSelection(isSomethingSelected) {
+ var self = this;
+ var hilite = this._highlighter;
+ hilite.classList.remove("selecting");
+ if (isSomethingSelected) {
+ hilite.classList.add("finished");
+ var start = this._sampleIndexFromPoint(this._selectedRange.startX);
+ var end = this._sampleIndexFromPoint(this._selectedRange.endX);
+ self._finishSelection(start, end);
+ } else {
+ hilite.classList.add("collapsed");
+ }
+ },
+ collapseHistogramSelection: function RangeSelector_collapseHistogramSelection() {
+ var hilite = this._highlighter;
+ hilite.classList.add("collapsed");
+ },
+ _sampleIndexFromPoint: function RangeSelector__sampleIndexFromPoint(x) {
+ // XXX this is completely wrong, fix please
+ var totalSamples = parseFloat(gCurrentlyShownSampleData.length);
+ var width = parseFloat(this._graph.parentNode.clientWidth);
+ var factor = totalSamples / width;
+ return parseInt(parseFloat(x) * factor);
+ },
+ _histogramIndexFromPoint: function RangeSelector__histogramIndexFromPoint(x) {
+ // XXX this is completely wrong, fix please
+ var totalSamples = parseFloat(this._histogram._histogramData.length);
+ var width = parseFloat(this._graph.parentNode.clientWidth);
+ var factor = totalSamples / width;
+ return parseInt(parseFloat(x) * factor);
+ },
+};
+
+function videoPaneTimeChange(video) {
+ if (!gMeta || !gMeta.frameStart)
+ return;
+
+ var frame = gVideoPane.getCurrentFrameNumber();
+ //var frameStart = gMeta.frameStart[frame];
+ //var frameEnd = gMeta.frameStart[frame+1]; // If we don't have a frameEnd assume the end of the profile
+
+ gHistogramView.showVideoFramePosition(frame);
+}
+
+
+window.onpopstate = function(ev) {
+ return; // Conflicts with document url
+ if (!gBreadcrumbTrail)
+ return;
+ console.log("pop: " + JSON.stringify(ev.state));
+ gBreadcrumbTrail.pop();
+ if (ev.state) {
+ console.log("state");
+ if (ev.state.action === "popbreadcrumb") {
+ console.log("bread");
+ //gBreadcrumbTrail.pop();
+ }
+ }
+}
+
+function BreadcrumbTrail() {
+ this._breadcrumbs = [];
+ this._selectedBreadcrumbIndex = -1;
+
+ this._containerElement = document.createElement("div");
+ this._containerElement.className = "breadcrumbTrail";
+ var self = this;
+ this._containerElement.addEventListener("click", function (e) {
+ if (!e.target.classList.contains("breadcrumbTrailItem"))
+ return;
+ self._enter(e.target.breadcrumbIndex);
+ });
+}
+BreadcrumbTrail.prototype = {
+ getContainer: function BreadcrumbTrail_getContainer() {
+ return this._containerElement;
+ },
+ /**
+ * Add a breadcrumb. The breadcrumb parameter is an object with the following
+ * properties:
+ * - title: The text that will be shown in the breadcrumb's button.
+ * - enterCallback: A function that will be called when entering this
+ * breadcrumb.
+ */
+ add: function BreadcrumbTrail_add(breadcrumb) {
+ for (var i = this._breadcrumbs.length - 1; i > this._selectedBreadcrumbIndex; i--) {
+ var rearLi = this._breadcrumbs[i];
+ if (!rearLi.breadcrumbIsTransient)
+ throw "Can only add new breadcrumbs if after the current one there are only transient ones.";
+ rearLi.breadcrumbDiscarder.discard();
+ }
+ var div = document.createElement("div");
+ div.className = "breadcrumbTrailItem";
+ div.textContent = breadcrumb.title;
+ var index = this._breadcrumbs.length;
+ div.breadcrumbIndex = index;
+ div.breadcrumbEnterCallback = breadcrumb.enterCallback;
+ div.breadcrumbIsTransient = true;
+ div.style.zIndex = 1000 - index;
+ this._containerElement.appendChild(div);
+ this._breadcrumbs.push(div);
+ if (index == 0)
+ this._enter(index);
+ var self = this;
+ div.breadcrumbDiscarder = {
+ discard: function () {
+ if (div.breadcrumbIsTransient) {
+ self._deleteBeyond(index - 1);
+ delete div.breadcrumbIsTransient;
+ delete div.breadcrumbDiscarder;
+ }
+ }
+ };
+ return div.breadcrumbDiscarder;
+ },
+ addAndEnter: function BreadcrumbTrail_addAndEnter(breadcrumb) {
+ var removalHandle = this.add(breadcrumb);
+ this._enter(this._breadcrumbs.length - 1);
+ },
+ pop : function BreadcrumbTrail_pop() {
+ if (this._breadcrumbs.length-2 >= 0)
+ this._enter(this._breadcrumbs.length-2);
+ },
+ enterLastItem: function BreadcrumbTrail_enterLastItem(forceSelection) {
+ this._enter(this._breadcrumbs.length-1, forceSelection);
+ },
+ _enter: function BreadcrumbTrail__select(index, forceSelection) {
+ if (index == this._selectedBreadcrumbIndex)
+ return;
+ if (forceSelection) {
+ gTreeManager.restoreSerializedSelectionSnapshot(forceSelection);
+ } else {
+ gTreeManager.saveSelectionSnapshot();
+ }
+ var prevSelected = this._breadcrumbs[this._selectedBreadcrumbIndex];
+ if (prevSelected)
+ prevSelected.classList.remove("selected");
+ var li = this._breadcrumbs[index];
+ if (this === gBreadcrumbTrail && index != 0) {
+ // Support for back button, disabled until the forward button is implemented.
+ //var state = {action: "popbreadcrumb",};
+ //window.history.pushState(state, "Cleopatra");
+ }
+ if (!li)
+ console.log("li at index " + index + " is null!");
+ delete li.breadcrumbIsTransient;
+ li.classList.add("selected");
+ this._deleteBeyond(index);
+ this._selectedBreadcrumbIndex = index;
+ li.breadcrumbEnterCallback();
+ // Add history state
+ },
+ _deleteBeyond: function BreadcrumbTrail__deleteBeyond(index) {
+ while (this._breadcrumbs.length > index + 1) {
+ this._hide(this._breadcrumbs[index + 1]);
+ this._breadcrumbs.splice(index + 1, 1);
+ }
+ },
+ _hide: function BreadcrumbTrail__hide(breadcrumb) {
+ delete breadcrumb.breadcrumbIsTransient;
+ breadcrumb.classList.add("deleted");
+ setTimeout(function () {
+ breadcrumb.parentNode.removeChild(breadcrumb);
+ }, 1000);
+ },
+};
+
+function maxResponsiveness() {
+ var data = gCurrentlyShownSampleData;
+ var maxRes = 0.0;
+ for (var i = 0; i < data.length; ++i) {
+ if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"])
+ continue;
+ if (maxRes < data[i].extraInfo["responsiveness"])
+ maxRes = data[i].extraInfo["responsiveness"];
+ }
+ return maxRes;
+}
+
+function effectiveInterval() {
+ var data = gCurrentlyShownSampleData;
+ var interval = 0.0;
+ var sampleCount = 0;
+ var timeCount = 0;
+ var lastTime = null;
+ for (var i = 0; i < data.length; ++i) {
+ if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) {
+ lastTime = null;
+ continue;
+ }
+ if (lastTime) {
+ sampleCount++;
+ timeCount += data[i].extraInfo["time"] - lastTime;
+ }
+ lastTime = data[i].extraInfo["time"];
+ }
+ var effectiveInterval = timeCount/sampleCount;
+ // Biggest diff
+ var biggestDiff = 0;
+ lastTime = null;
+ for (var i = 0; i < data.length; ++i) {
+ if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) {
+ lastTime = null;
+ continue;
+ }
+ if (lastTime) {
+ if (biggestDiff < Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime)))
+ biggestDiff = Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime));
+ }
+ lastTime = data[i].extraInfo["time"];
+ }
+
+ if (effectiveInterval != effectiveInterval)
+ return "Time info not collected";
+
+ return (effectiveInterval).toFixed(2) + " ms ±" + biggestDiff.toFixed(2);
+}
+
+function numberOfCurrentlyShownSamples() {
+ var data = gCurrentlyShownSampleData;
+ var num = 0;
+ for (var i = 0; i < data.length; ++i) {
+ if (data[i])
+ num++;
+ }
+ return num;
+}
+
+function avgResponsiveness() {
+ var data = gCurrentlyShownSampleData;
+ var totalRes = 0.0;
+ for (var i = 0; i < data.length; ++i) {
+ if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"])
+ continue;
+ totalRes += data[i].extraInfo["responsiveness"];
+ }
+ return totalRes / numberOfCurrentlyShownSamples();
+}
+
+function copyProfile() {
+ window.prompt ("Copy to clipboard: Ctrl+C, Enter", document.getElementById("data").value);
+}
+
+function saveProfileToLocalStorage() {
+ Parser.getSerializedProfile(true, function (serializedProfile) {
+ gLocalStorage.storeLocalProfile(serializedProfile, gMeta.profileId, function profileSaved() {
+
+ });
+ });
+}
+function downloadProfile() {
+ Parser.getSerializedProfile(true, function (serializedProfile) {
+ var blob = new Blob([serializedProfile], { "type": "application/octet-stream" });
+ location.href = window.URL.createObjectURL(blob);
+ });
+}
+
+function promptUploadProfile(selected) {
+ var overlay = document.createElement("div");
+ overlay.style.position = "absolute";
+ overlay.style.top = 0;
+ overlay.style.left = 0;
+ overlay.style.width = "100%";
+ overlay.style.height = "100%";
+ overlay.style.backgroundColor = "transparent";
+
+ var bg = document.createElement("div");
+ bg.style.position = "absolute";
+ bg.style.top = 0;
+ bg.style.left = 0;
+ bg.style.width = "100%";
+ bg.style.height = "100%";
+ bg.style.opacity = "0.6";
+ bg.style.backgroundColor = "#aaaaaa";
+ overlay.appendChild(bg);
+
+ var contentDiv = document.createElement("div");
+ contentDiv.className = "sideBar";
+ contentDiv.style.position = "absolute";
+ contentDiv.style.top = "50%";
+ contentDiv.style.left = "50%";
+ contentDiv.style.width = "40em";
+ contentDiv.style.height = "20em";
+ contentDiv.style.marginLeft = "-20em";
+ contentDiv.style.marginTop = "-10em";
+ contentDiv.style.padding = "10px";
+ contentDiv.style.border = "2px solid black";
+ contentDiv.style.backgroundColor = "rgb(219, 223, 231)";
+ overlay.appendChild(contentDiv);
+
+ var noticeHTML = "";
+ noticeHTML += "<center><h2 style='font-size: 2em'>Upload Profile - Privacy Notice</h2></center>";
+ noticeHTML += "You're about to upload your profile publicly where anyone will be able to access it. ";
+ noticeHTML += "To better diagnose performance problems profiles include the following information:";
+ noticeHTML += "<ul>";
+ noticeHTML += " <li>The <b>URLs</b> and scripts of the tabs that were executing.</li>";
+ noticeHTML += " <li>The <b>metadata of all your Add-ons</b> to identify slow Add-ons.</li>";
+ noticeHTML += " <li>Firefox build and runtime configuration.</li>";
+ noticeHTML += "</ul><br>";
+ noticeHTML += "To view all the information you can download the full profile to a file and open the json structure with a text editor.<br><br>";
+ contentDiv.innerHTML = noticeHTML;
+
+ var cancelButton = document.createElement("input");
+ cancelButton.style.position = "absolute";
+ cancelButton.style.bottom = "10px";
+ cancelButton.type = "button";
+ cancelButton.value = "Cancel";
+ cancelButton.onclick = function() {
+ document.body.removeChild(overlay);
+ }
+ contentDiv.appendChild(cancelButton);
+
+ var uploadButton = document.createElement("input");
+ uploadButton.style.position = "absolute";
+ uploadButton.style.right = "10px";
+ uploadButton.style.bottom = "10px";
+ uploadButton.type = "button";
+ uploadButton.value = "Upload";
+ uploadButton.onclick = function() {
+ document.body.removeChild(overlay);
+ uploadProfile(selected);
+ }
+ contentDiv.appendChild(uploadButton);
+
+ document.body.appendChild(overlay);
+}
+
+function uploadProfile(selected) {
+ Parser.getSerializedProfile(!selected, function (dataToUpload) {
+ var oXHR = new XMLHttpRequest();
+ oXHR.onload = function (oEvent) {
+ if (oXHR.status == 200) {
+ gReportID = oXHR.responseText;
+ updateDocumentURL();
+ document.getElementById("upload_status").innerHTML = "Success! Use this <a id='linkElem'>link</a>";
+ document.getElementById("linkElem").href = document.URL;
+ } else {
+ document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file.";
+ }
+ };
+ oXHR.onerror = function (oEvent) {
+ document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file.";
+ }
+ oXHR.upload.onprogress = function(oEvent) {
+ if (oEvent.lengthComputable) {
+ var progress = Math.round((oEvent.loaded / oEvent.total)*100);
+ if (progress == 100) {
+ document.getElementById("upload_status").innerHTML = "Uploading: Waiting for server side compression";
+ } else {
+ document.getElementById("upload_status").innerHTML = "Uploading: " + Math.round((oEvent.loaded / oEvent.total)*100) + "%";
+ }
+ }
+ };
+
+ var dataSize;
+ if (dataToUpload.length > 1024*1024) {
+ dataSize = (dataToUpload.length/1024/1024).toFixed(1) + " MB(s)";
+ } else {
+ dataSize = (dataToUpload.length/1024).toFixed(1) + " KB(s)";
+ }
+
+ var formData = new FormData();
+ formData.append("file", dataToUpload);
+ document.getElementById("upload_status").innerHTML = "Uploading Profile (" + dataSize + ")";
+ oXHR.open("POST", "http://profile-store.appspot.com/store", true);
+ oXHR.send(formData);
+ });
+}
+
+function populate_skip_symbol() {
+ var skipSymbolCtrl = document.getElementById('skipsymbol')
+ //skipSymbolCtrl.options = gSkipSymbols;
+ for (var i = 0; i < gSkipSymbols.length; i++) {
+ var elOptNew = document.createElement('option');
+ elOptNew.text = gSkipSymbols[i];
+ elOptNew.value = gSkipSymbols[i];
+ elSel.add(elOptNew);
+ }
+
+}
+
+function delete_skip_symbol() {
+ var skipSymbol = document.getElementById('skipsymbol').value
+}
+
+function add_skip_symbol() {
+
+}
+
+var gFilterChangeCallback = null;
+var gFilterChangeDelay = 1200;
+function filterOnChange() {
+ if (gFilterChangeCallback != null) {
+ clearTimeout(gFilterChangeCallback);
+ gFilterChangeCallback = null;
+ }
+
+ gFilterChangeCallback = setTimeout(filterUpdate, gFilterChangeDelay);
+}
+function filterUpdate() {
+ gFilterChangeCallback = null;
+
+ filtersChanged();
+
+ var filterNameInput = document.getElementById("filterName");
+ if (filterNameInput != null) {
+ changeFocus(filterNameInput);
+ }
+}
+
+// Maps document id to a tooltip description
+var tooltip = {
+ "mergeFunctions" : "Ignore line information and merge samples based on function names.",
+ "showJank" : "Show only samples with >50ms responsiveness.",
+ "showJS" : "Show only samples which involve running chrome or content Javascript code.",
+ "mergeUnbranched" : "Collapse unbranched call paths in the call tree into a single node.",
+ "filterName" : "Show only samples with a frame containing the filter as a substring.",
+ "invertCallstack" : "Invert the callstack (Heavy view) to find the most expensive leaf functions.",
+ "upload" : "Upload the full profile to public cloud storage to share with others.",
+ "upload_select" : "Upload only the selected view.",
+ "download" : "Initiate a download of the full profile.",
+}
+
+function addTooltips() {
+ for (var elemId in tooltip) {
+ var elem = document.getElementById(elemId);
+ if (!elem)
+ continue;
+ if (elem.parentNode.nodeName.toLowerCase() == "label")
+ elem = elem.parentNode;
+ elem.title = tooltip[elemId];
+ }
+}
+
+function InfoBar() {
+ this._container = document.createElement("div");
+ this._container.id = "infoBar";
+ this._container.className = "sideBar";
+}
+
+InfoBar.prototype = {
+ getContainer: function InfoBar_getContainer() {
+ return this._container;
+ },
+ display: function InfoBar_display() {
+ function getMetaFeatureString() {
+ features = "<dt>Stackwalk:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>";
+ features += "<dt>Jank:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>";
+ return features;
+ }
+ function getPlatformInfo() {
+ return gMeta.oscpu + " (" + gMeta.toolkit + ")";
+ }
+ var infobar = this._container;
+ var infoText = "";
+
+ if (gMeta) {
+ infoText += "<h2>Profile Info</h2>\n<dl>\n";
+ infoText += "<dt>Product:</dt><dd>" + gMeta.product + "</dd>";
+ infoText += "<dt>Platform:</dt><dd>" + getPlatformInfo() + "</dd>";
+ infoText += getMetaFeatureString();
+ infoText += "<dt>Interval:</dt><dd>" + gMeta.interval + " ms</dd></dl>";
+ }
+ infoText += "<h2>Selection Info</h2>\n<dl>\n";
+ infoText += " <dt>Avg. Responsiveness:</dt><dd>" + avgResponsiveness().toFixed(2) + " ms</dd>\n";
+ infoText += " <dt>Max Responsiveness:</dt><dd>" + maxResponsiveness().toFixed(2) + " ms</dd>\n";
+ infoText += " <dt>Real Interval:</dt><dd>" + effectiveInterval() + "</dd>";
+ infoText += "</dl>\n";
+ infoText += "<h2>Pre Filtering</h2>\n";
+ // Disable for now since it's buggy and not useful
+ //infoText += "<label><input type='checkbox' id='mergeFunctions' " + (gMergeFunctions ?" checked='true' ":" ") + " onchange='toggleMergeFunctions()'/>Functions, not lines</label><br>\n";
+
+ var filterNameInputOld = document.getElementById("filterName");
+ infoText += "<a>Filter:\n";
+ infoText += "<input type='search' id='filterName' oninput='filterOnChange()'/></a>\n";
+
+ infoText += "<h2>Post Filtering</h2>\n";
+ infoText += "<label><input type='checkbox' id='showJank' " + (gJankOnly ?" checked='true' ":" ") + " onchange='toggleJank()'/>Show Jank only</label>\n";
+ infoText += "<h2>View Options</h2>\n";
+ infoText += "<label><input type='checkbox' id='showJS' " + (gJavascriptOnly ?" checked='true' ":" ") + " onchange='toggleJavascriptOnly()'/>Javascript only</label><br>\n";
+ infoText += "<label><input type='checkbox' id='mergeUnbranched' " + (gMergeUnbranched ?" checked='true' ":" ") + " onchange='toggleMergeUnbranched()'/>Merge unbranched call paths</label><br>\n";
+ infoText += "<label><input type='checkbox' id='invertCallstack' " + (gInvertCallstack ?" checked='true' ":" ") + " onchange='toggleInvertCallStack()'/>Invert callstack</label><br>\n";
+
+ infoText += "<h2>Share</h2>\n";
+ infoText += "<div id='upload_status' aria-live='polite'>No upload in progress</div><br>\n";
+ infoText += "<input type='button' id='upload' value='Upload full profile'>\n";
+ infoText += "<input type='button' id='upload_select' value='Upload view'><br>\n";
+ infoText += "<input type='button' id='download' value='Download full profile'>\n";
+
+ infoText += "<h2>Compare</h2>\n";
+ infoText += "<input type='button' id='compare' value='Compare'>\n";
+
+ //infoText += "<br>\n";
+ //infoText += "Skip functions:<br>\n";
+ //infoText += "<select size=8 id='skipsymbol'></select><br />"
+ //infoText += "<input type='button' id='delete_skipsymbol' value='Delete'/><br />\n";
+ //infoText += "<input type='button' id='add_skipsymbol' value='Add'/><br />\n";
+
+ infobar.innerHTML = infoText;
+ addTooltips();
+
+ var filterNameInputNew = document.getElementById("filterName");
+ if (filterNameInputOld != null && filterNameInputNew != null) {
+ filterNameInputNew.parentNode.replaceChild(filterNameInputOld, filterNameInputNew);
+ //filterNameInputNew.value = filterNameInputOld.value;
+ } else if (gQueryParamFilterName != null) {
+ filterNameInputNew.value = gQueryParamFilterName;
+ gQueryParamFilterName = null;
+ }
+ document.getElementById('compare').onclick = function() {
+ openProfileCompare();
+ }
+ document.getElementById('upload').onclick = function() {
+ promptUploadProfile(false);
+ };
+ document.getElementById('download').onclick = downloadProfile;
+ document.getElementById('upload_select').onclick = function() {
+ promptUploadProfile(true);
+ };
+ //document.getElementById('delete_skipsymbol').onclick = delete_skip_symbol;
+ //document.getElementById('add_skipsymbol').onclick = add_skip_symbol;
+
+ //populate_skip_symbol();
+ }
+}
+
+var gNumSamples = 0;
+var gMeta = null;
+var gSymbols = {};
+var gFunctions = {};
+var gResources = {};
+var gHighlightedCallstack = [];
+var gFrameView = null;
+var gTreeManager = null;
+var gSampleBar = null;
+var gBreadcrumbTrail = null;
+var gHistogramView = null;
+var gDiagnosticBar = null;
+var gVideoPane = null;
+var gPluginView = null;
+var gFileList = null;
+var gInfoBar = null;
+var gMainArea = null;
+var gCurrentlyShownSampleData = null;
+var gSkipSymbols = ["test2", "test1"];
+var gAppendVideoCapture = null;
+var gQueryParamFilterName = null;
+var gRestoreSelection = null;
+var gReportID = null;
+
+function getTextData() {
+ var data = [];
+ var samples = gCurrentlyShownSampleData;
+ for (var i = 0; i < samples.length; i++) {
+ data.push(samples[i].lines.join("\n"));
+ }
+ return data.join("\n");
+}
+
+function loadProfileFile(fileList) {
+ if (fileList.length == 0)
+ return;
+ var file = fileList[0];
+ var reporter = enterProgressUI();
+ var subreporters = reporter.addSubreporters({
+ fileLoading: 1000,
+ parsing: 1000
+ });
+
+ var reader = new FileReader();
+ reader.onloadend = function () {
+ subreporters.fileLoading.finish();
+ loadRawProfile(subreporters.parsing, reader.result);
+ };
+ reader.onprogress = function (e) {
+ subreporters.fileLoading.setProgress(e.loaded / e.total);
+ };
+ reader.readAsText(file, "utf-8");
+ subreporters.fileLoading.begin("Reading local file...");
+}
+
+function loadLocalStorageProfile(profileKey) {
+ var reporter = enterProgressUI();
+ var subreporters = reporter.addSubreporters({
+ fileLoading: 1000,
+ parsing: 1000
+ });
+
+ gLocalStorage.getProfile(profileKey, function(profile) {
+ subreporters.fileLoading.finish();
+ loadRawProfile(subreporters.parsing, profile, profileKey);
+ });
+ subreporters.fileLoading.begin("Reading local storage...");
+}
+
+function appendVideoCapture(videoCapture) {
+ if (videoCapture.indexOf("://") == -1) {
+ videoCapture = EIDETICKER_BASE_URL + videoCapture;
+ }
+ gAppendVideoCapture = videoCapture;
+}
+
+function loadZippedProfileURL(url) {
+ var reporter = enterProgressUI();
+ var subreporters = reporter.addSubreporters({
+ fileLoading: 1000,
+ parsing: 1000
+ });
+
+ // Crude way to detect if we're using a relative URL or not :(
+ if (url.indexOf("://") == -1) {
+ url = EIDETICKER_BASE_URL + url;
+ }
+ reporter.begin("Fetching " + url);
+ PROFILERTRACE("Fetch url: " + url);
+
+ function onerror(e) {
+ PROFILERERROR("zip.js error");
+ PROFILERERROR(JSON.stringify(e));
+ }
+
+ zip.workerScriptsPath = "js/zip.js/";
+ zip.createReader(new zip.HttpReader(url), function(zipReader) {
+ subreporters.fileLoading.setProgress(0.4);
+ zipReader.getEntries(function(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ var entry = entries[i];
+ PROFILERTRACE("Zip file: " + entry.filename);
+ if (entry.filename === "symbolicated_profile.txt") {
+ reporter.begin("Decompressing " + url);
+ subreporters.fileLoading.setProgress(0.8);
+ entry.getData(new zip.TextWriter(), function(profileText) {
+ subreporters.fileLoading.finish();
+ loadRawProfile(subreporters.parsing, profileText);
+ });
+ return;
+ }
+ onerror("symbolicated_profile.txt not found in zip file.");
+ }
+ });
+ }, onerror);
+}
+
+function loadProfileURL(url) {
+ var reporter = enterProgressUI();
+ var subreporters = reporter.addSubreporters({
+ fileLoading: 1000,
+ parsing: 1000
+ });
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "text";
+ xhr.onreadystatechange = function (e) {
+ if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) {
+ subreporters.fileLoading.finish();
+ PROFILERLOG("Got profile from '" + url + "'.");
+ if (xhr.responseText == null || xhr.responseText === "") {
+ subreporters.fileLoading.begin("Profile '" + url + "' is empty. Did you set the CORS headers?");
+ return;
+ }
+ loadRawProfile(subreporters.parsing, xhr.responseText, url);
+ }
+ };
+ xhr.onerror = function (e) {
+ subreporters.fileLoading.begin("Error fetching profile :(. URL: '" + url + "'. Did you set the CORS headers?");
+ }
+ xhr.onprogress = function (e) {
+ if (e.lengthComputable && (e.loaded <= e.total)) {
+ subreporters.fileLoading.setProgress(e.loaded / e.total);
+ } else {
+ subreporters.fileLoading.setProgress(NaN);
+ }
+ };
+ xhr.send(null);
+ subreporters.fileLoading.begin("Loading remote file...");
+}
+
+function loadProfile(rawProfile) {
+ if (!rawProfile)
+ return;
+ var reporter = enterProgressUI();
+ loadRawProfile(reporter, rawProfile);
+}
+
+function loadRawProfile(reporter, rawProfile, profileId) {
+ PROFILERLOG("Parse raw profile: ~" + rawProfile.length + " bytes");
+ reporter.begin("Parsing...");
+ if (rawProfile == null || rawProfile.length === 0) {
+ reporter.begin("Profile is null or empty");
+ return;
+ }
+ var startTime = Date.now();
+ var parseRequest = Parser.parse(rawProfile, {
+ appendVideoCapture : gAppendVideoCapture,
+ profileId: profileId,
+ });
+ gVideoCapture = null;
+ parseRequest.addEventListener("progress", function (progress, action) {
+ if (action)
+ reporter.setAction(action);
+ reporter.setProgress(progress);
+ });
+ parseRequest.addEventListener("finished", function (result) {
+ console.log("parsing (in worker): " + (Date.now() - startTime) + "ms");
+ reporter.finish();
+ gMeta = result.meta;
+ gNumSamples = result.numSamples;
+ gSymbols = result.symbols;
+ gFunctions = result.functions;
+ gResources = result.resources;
+ enterFinishedProfileUI();
+ gFileList.profileParsingFinished();
+ });
+}
+
+var gImportFromAddonSubreporters = null;
+
+window.addEventListener("message", function messageFromAddon(msg) {
+ // This is triggered by the profiler add-on.
+ var o = JSON.parse(msg.data);
+ switch (o.task) {
+ case "importFromAddonStart":
+ var totalReporter = enterProgressUI();
+ gImportFromAddonSubreporters = totalReporter.addSubreporters({
+ import: 10000,
+ parsing: 1000
+ });
+ gImportFromAddonSubreporters.import.begin("Symbolicating...");
+ break;
+ case "importFromAddonProgress":
+ gImportFromAddonSubreporters.import.setProgress(o.progress);
+ if (o.action != null) {
+ gImportFromAddonSubreporters.import.setAction(o.action);
+ }
+ break;
+ case "importFromAddonFinish":
+ importFromAddonFinish(o.rawProfile);
+ break;
+ }
+});
+
+function importFromAddonFinish(rawProfile) {
+ gImportFromAddonSubreporters.import.finish();
+ loadRawProfile(gImportFromAddonSubreporters.parsing, rawProfile);
+}
+
+var gInvertCallstack = false;
+function toggleInvertCallStack() {
+ gTreeManager.saveReverseSelectionSnapshot(gJavascriptOnly);
+ gInvertCallstack = !gInvertCallstack;
+ var startTime = Date.now();
+ viewOptionsChanged();
+ console.log("invert time: " + (Date.now() - startTime) + "ms");
+}
+
+var gMergeUnbranched = false;
+function toggleMergeUnbranched() {
+ gMergeUnbranched = !gMergeUnbranched;
+ viewOptionsChanged();
+}
+
+var gMergeFunctions = true;
+function toggleMergeFunctions() {
+ gMergeFunctions = !gMergeFunctions;
+ filtersChanged();
+}
+
+var gJankOnly = false;
+var gJankThreshold = 50 /* ms */;
+function toggleJank(/* optional */ threshold) {
+ // Currently we have no way to change the threshold in the UI
+ // once we add this we will need to change the tooltip.
+ gJankOnly = !gJankOnly;
+ if (threshold != null ) {
+ gJankThreshold = threshold;
+ }
+ filtersChanged();
+}
+
+var gJavascriptOnly = false;
+function toggleJavascriptOnly() {
+ if (gJavascriptOnly) {
+ // When going from JS only to non js there's going to be new C++
+ // frames in the selection so we need to restore the selection
+ // while allowing non contigous symbols to be in the stack (the c++ ones)
+ gTreeManager.setAllowNonContigous();
+ }
+ gJavascriptOnly = !gJavascriptOnly;
+ gTreeManager.saveSelectionSnapshot(gJavascriptOnly);
+ filtersChanged();
+}
+
+var gSampleFilters = [];
+function focusOnSymbol(focusSymbol, name) {
+ var newFilterChain = gSampleFilters.concat([{type: "FocusedFrameSampleFilter", name: name, focusedSymbol: focusSymbol}]);
+ gBreadcrumbTrail.addAndEnter({
+ title: name,
+ enterCallback: function () {
+ gSampleFilters = newFilterChain;
+ filtersChanged();
+ }
+ });
+}
+
+function focusOnCallstack(focusedCallstack, name, overwriteCallstack) {
+ var invertCallstack = gInvertCallstack;
+
+ if (overwriteCallstack != null) {
+ invertCallstack = overwriteCallstack;
+ }
+ var filter = {
+ type: !invertCallstack ? "FocusedCallstackPostfixSampleFilter" : "FocusedCallstackPrefixSampleFilter",
+ name: name,
+ focusedCallstack: focusedCallstack,
+ appliesToJS: gJavascriptOnly
+ };
+ var newFilterChain = gSampleFilters.concat([filter]);
+ gBreadcrumbTrail.addAndEnter({
+ title: name,
+ enterCallback: function () {
+ gSampleFilters = newFilterChain;
+ filtersChanged();
+ }
+ })
+}
+
+function focusOnPluginView(pluginName, param) {
+ var filter = {
+ type: "PluginView",
+ pluginName: pluginName,
+ param: param,
+ };
+ var newFilterChain = gSampleFilters.concat([filter]);
+ gBreadcrumbTrail.addAndEnter({
+ title: "Plugin View: " + pluginName,
+ enterCallback: function () {
+ gSampleFilters = newFilterChain;
+ filtersChanged();
+ }
+ })
+}
+
+function viewJSSource(sample) {
+ var sourceView = new SourceView();
+ sourceView.setScriptLocation(sample.scriptLocation);
+ sourceView.setSource(gMeta.js.source[sample.scriptLocation.scriptURI]);
+ gMainArea.appendChild(sourceView.getContainer());
+
+}
+
+function setHighlightedCallstack(samples, heaviestSample) {
+ PROFILERTRACE("highlight: " + samples);
+ gHighlightedCallstack = samples;
+ gHistogramView.highlightedCallstackChanged(gHighlightedCallstack);
+ if (!gInvertCallstack) {
+ // Always show heavy
+ heaviestSample = heaviestSample.clone().reverse();
+ }
+
+ if (gSampleBar) {
+ gSampleBar.setSample(heaviestSample);
+ }
+}
+
+function enterMainUI() {
+ var uiContainer = document.createElement("div");
+ uiContainer.id = "ui";
+
+ gFileList = new FileList();
+ uiContainer.appendChild(gFileList.getContainer());
+
+ //gFileList.addFile();
+ gFileList.loadProfileListFromLocalStorage();
+
+ gInfoBar = new InfoBar();
+ uiContainer.appendChild(gInfoBar.getContainer());
+
+ gMainArea = document.createElement("div");
+ gMainArea.id = "mainarea";
+ uiContainer.appendChild(gMainArea);
+ document.body.appendChild(uiContainer);
+
+ var profileEntryPane = document.createElement("div");
+ profileEntryPane.className = "profileEntryPane";
+ profileEntryPane.innerHTML = '' +
+ '<h1>Upload your profile here:</h1>' +
+ '<input type="file" id="datafile" onchange="loadProfileFile(this.files);">' +
+ '<h1>Or, alternatively, enter your profile data here:</h1>' +
+ '<textarea rows=20 cols=80 id=data autofocus spellcheck=false></textarea>' +
+ '<p><button onclick="loadProfile(document.getElementById(\'data\').value);">Parse</button></p>' +
+ '';
+
+ gMainArea.appendChild(profileEntryPane);
+}
+
+function enterProgressUI() {
+ var profileProgressPane = document.createElement("div");
+ profileProgressPane.className = "profileProgressPane";
+
+ var progressLabel = document.createElement("a");
+ profileProgressPane.appendChild(progressLabel);
+
+ var progressBar = document.createElement("progress");
+ profileProgressPane.appendChild(progressBar);
+
+ var totalProgressReporter = new ProgressReporter();
+ totalProgressReporter.addListener(function (r) {
+ var progress = r.getProgress();
+ progressLabel.innerHTML = r.getAction();
+ console.log("Action: " + r.getAction());
+ if (isNaN(progress))
+ progressBar.removeAttribute("value");
+ else
+ progressBar.value = progress;
+ });
+
+ gMainArea.appendChild(profileProgressPane);
+
+ Parser.updateLogSetting();
+
+ return totalProgressReporter;
+}
+
+function enterFinishedProfileUI() {
+ saveProfileToLocalStorage();
+
+ var finishedProfilePaneBackgroundCover = document.createElement("div");
+ finishedProfilePaneBackgroundCover.className = "finishedProfilePaneBackgroundCover";
+
+ var finishedProfilePane = document.createElement("table");
+ var rowIndex = 0;
+ var currRow;
+ finishedProfilePane.style.width = "100%";
+ finishedProfilePane.style.height = "100%";
+ finishedProfilePane.border = "0";
+ finishedProfilePane.cellPadding = "0";
+ finishedProfilePane.cellSpacing = "0";
+ finishedProfilePane.borderCollapse = "collapse";
+ finishedProfilePane.className = "finishedProfilePane";
+ setTimeout(function() {
+ // Work around a webkit bug. For some reason the table doesn't show up
+ // until some actions happen such as focusing this box
+ var filterNameInput = document.getElementById("filterName");
+ if (filterNameInput != null) {
+ changeFocus(filterNameInput);
+ }
+ }, 100);
+
+ gBreadcrumbTrail = new BreadcrumbTrail();
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gBreadcrumbTrail.getContainer());
+
+ gHistogramView = new HistogramView();
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gHistogramView.getContainer());
+
+ if (false && gLocation.indexOf("file:") == 0) {
+ // Local testing for frameView
+ gFrameView = new FrameView();
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gFrameView.getContainer());
+ }
+
+ gDiagnosticBar = new DiagnosticBar();
+ gDiagnosticBar.setDetailsListener(function(details) {
+ if (details.indexOf("bug ") == 0) {
+ window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=' + details.substring(4));
+ } else {
+ var sourceView = new SourceView();
+ sourceView.setText("Diagnostic", js_beautify(details));
+ gMainArea.appendChild(sourceView.getContainer());
+ }
+ });
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gDiagnosticBar.getContainer());
+
+ // For testing:
+ //gMeta.videoCapture = {
+ // src: "http://videos-cdn.mozilla.net/brand/Mozilla_Firefox_Manifesto_v0.2_640.webm",
+ //};
+
+ if (gMeta && gMeta.videoCapture) {
+ gVideoPane = new VideoPane(gMeta.videoCapture);
+ gVideoPane.onTimeChange(videoPaneTimeChange);
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.insertCell(0).appendChild(gVideoPane.getContainer());
+ }
+
+ var treeContainerDiv = document.createElement("div");
+ treeContainerDiv.className = "treeContainer";
+ treeContainerDiv.style.width = "100%";
+ treeContainerDiv.style.height = "100%";
+
+ gTreeManager = new ProfileTreeManager();
+ currRow = finishedProfilePane.insertRow(rowIndex++);
+ currRow.style.height = "100%";
+ var cell = currRow.insertCell(0);
+ cell.appendChild(treeContainerDiv);
+ treeContainerDiv.appendChild(gTreeManager.getContainer());
+
+ gSampleBar = new SampleBar();
+ treeContainerDiv.appendChild(gSampleBar.getContainer());
+
+ // sampleBar
+
+ gPluginView = new PluginView();
+ //currRow = finishedProfilePane.insertRow(4);
+ treeContainerDiv.appendChild(gPluginView.getContainer());
+
+ gMainArea.appendChild(finishedProfilePaneBackgroundCover);
+ gMainArea.appendChild(finishedProfilePane);
+
+ var currentBreadcrumb = gSampleFilters;
+ gBreadcrumbTrail.add({
+ title: gStrings["Complete Profile"],
+ enterCallback: function () {
+ gSampleFilters = [];
+ filtersChanged();
+ }
+ });
+ if (currentBreadcrumb == null || currentBreadcrumb.length == 0) {
+ gTreeManager.restoreSerializedSelectionSnapshot(gRestoreSelection);
+ viewOptionsChanged();
+ }
+ for (var i = 0; i < currentBreadcrumb.length; i++) {
+ var filter = currentBreadcrumb[i];
+ var forceSelection = null;
+ if (gRestoreSelection != null && i == currentBreadcrumb.length - 1) {
+ forceSelection = gRestoreSelection;
+ }
+ switch (filter.type) {
+ case "FocusedFrameSampleFilter":
+ focusOnSymbol(filter.name, filter.symbolName);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "FocusedCallstackPrefixSampleFilter":
+ focusOnCallstack(filter.focusedCallstack, filter.name, false);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "FocusedCallstackPostfixSampleFilter":
+ focusOnCallstack(filter.focusedCallstack, filter.name, true);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ case "RangeSampleFilter":
+ gHistogramView.selectRange(filter.start, filter.end);
+ gBreadcrumbTrail.enterLastItem(forceSelection);
+ }
+ }
+}
+
+// Make all focus change events go through this function.
+// This function will mediate the focus changes in case
+// that we're in a compare view. In a compare view an inactive
+// instance of cleopatra should not steal focus from the active
+// cleopatra instance.
+function changeFocus(elem) {
+ if (window.comparator_changeFocus) {
+ window.comparator_changeFocus(elem);
+ } else {
+ PROFILERLOG("FOCUS\n\n\n\n\n\n\n\n\n");
+ elem.focus();
+ }
+}
+
+function comparator_receiveSelection(snapshot, frameData) {
+ gTreeManager.restoreSerializedSelectionSnapshot(snapshot);
+ if (frameData)
+ gTreeManager.highlightFrame(frameData);
+ viewOptionsChanged();
+}
+
+function filtersChanged() {
+ if (window.comparator_setSelection) {
+ // window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), null);
+ }
+ updateDocumentURL();
+ var data = { symbols: {}, functions: {}, samples: [] };
+
+ gHistogramView.dataIsOutdated();
+ var filterNameInput = document.getElementById("filterName");
+ var updateRequest = Parser.updateFilters({
+ mergeFunctions: gMergeFunctions,
+ nameFilter: (filterNameInput && filterNameInput.value) || gQueryParamFilterName || "",
+ sampleFilters: gSampleFilters,
+ jankOnly: gJankOnly,
+ javascriptOnly: gJavascriptOnly
+ });
+ var start = Date.now();
+ updateRequest.addEventListener("finished", function (filteredSamples) {
+ console.log("profile filtering (in worker): " + (Date.now() - start) + "ms.");
+ gCurrentlyShownSampleData = filteredSamples;
+ gInfoBar.display();
+
+ if (gSampleFilters.length > 0 && gSampleFilters[gSampleFilters.length-1].type === "PluginView") {
+ start = Date.now();
+ gPluginView.display(gSampleFilters[gSampleFilters.length-1].pluginName, gSampleFilters[gSampleFilters.length-1].param,
+ gCurrentlyShownSampleData, gHighlightedCallstack);
+ console.log("plugin displaying: " + (Date.now() - start) + "ms.");
+ } else {
+ gPluginView.hide();
+ }
+ });
+
+ var histogramRequest = Parser.calculateHistogramData();
+ histogramRequest.addEventListener("finished", function (data) {
+ start = Date.now();
+ gHistogramView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack);
+ if (gFrameView)
+ gFrameView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack);
+ console.log("histogram displaying: " + (Date.now() - start) + "ms.");
+ });
+
+ if (gDiagnosticBar) {
+ var diagnosticsRequest = Parser.calculateDiagnosticItems(gMeta);
+ diagnosticsRequest.addEventListener("finished", function (diagnosticItems) {
+ start = Date.now();
+ gDiagnosticBar.display(diagnosticItems);
+ console.log("diagnostic items displaying: " + (Date.now() - start) + "ms.");
+ });
+ }
+
+ viewOptionsChanged();
+}
+
+function viewOptionsChanged() {
+ gTreeManager.dataIsOutdated();
+ var filterNameInput = document.getElementById("filterName");
+ var updateViewOptionsRequest = Parser.updateViewOptions({
+ invertCallstack: gInvertCallstack,
+ mergeUnbranched: gMergeUnbranched
+ });
+ updateViewOptionsRequest.addEventListener("finished", function (calltree) {
+ var start = Date.now();
+ gTreeManager.display(calltree, gSymbols, gFunctions, gResources, gMergeFunctions, filterNameInput && filterNameInput.value);
+ console.log("tree displaying: " + (Date.now() - start) + "ms.");
+ });
+}
+
+function loadQueryData(queryData) {
+ var isFiltersChanged = false;
+ var queryDataOriginal = queryData;
+ var queryData = {};
+ for (var i in queryDataOriginal) {
+ queryData[i] = unQueryEscape(queryDataOriginal[i]);
+ }
+ if (queryData.search) {
+ gQueryParamFilterName = queryData.search;
+ isFiltersChanged = true;
+ }
+ if (queryData.jankOnly) {
+ gJankOnly = queryData.jankOnly;
+ isFiltersChanged = true;
+ }
+ if (queryData.javascriptOnly) {
+ gJavascriptOnly = queryData.javascriptOnly;
+ isFiltersChanged = true;
+ }
+ if (queryData.mergeUnbranched) {
+ gMergeUnbranched = queryData.mergeUnbranched;
+ isFiltersChanged = true;
+ }
+ if (queryData.invertCallback) {
+ gInvertCallstack = queryData.invertCallback;
+ isFiltersChanged = true;
+ }
+ if (queryData.report) {
+ gReportID = queryData.report;
+ }
+ if (queryData.filter) {
+ var filterChain = JSON.parse(queryData.filter);
+ gSampleFilters = filterChain;
+ }
+ if (queryData.selection) {
+ var selection = queryData.selection;
+ gRestoreSelection = selection;
+ }
+
+ if (isFiltersChanged) {
+ //filtersChanged();
+ }
+}
+
+function unQueryEscape(str) {
+ return decodeURIComponent(str);
+}
+
+function queryEscape(str) {
+ return encodeURIComponent(encodeURIComponent(str));
+}
+
+function updateDocumentURL() {
+ location.hash = getDocumentHashString();
+ return document.location;
+}
+
+function getDocumentHashString() {
+ var query = "";
+ if (gReportID) {
+ if (query != "")
+ query += "&";
+ query += "report=" + queryEscape(gReportID);
+ }
+ if (document.getElementById("filterName") != null &&
+ document.getElementById("filterName").value != null &&
+ document.getElementById("filterName").value != "") {
+ if (query != "")
+ query += "&";
+ query += "search=" + queryEscape(document.getElementById("filterName").value);
+ }
+ // For now don't restore the view rest
+ return query;
+ if (gJankOnly) {
+ if (query != "")
+ query += "&";
+ query += "jankOnly=" + queryEscape(gJankOnly);
+ }
+ if (gJavascriptOnly) {
+ if (query != "")
+ query += "&";
+ query += "javascriptOnly=" + queryEscape(gJavascriptOnly);
+ }
+ if (gMergeUnbranched) {
+ if (query != "")
+ query += "&";
+ query += "mergeUnbranched=" + queryEscape(gMergeUnbranched);
+ }
+ if (gInvertCallstack) {
+ if (query != "")
+ query += "&";
+ query += "invertCallback=" + queryEscape(gInvertCallstack);
+ }
+ if (gSampleFilters && gSampleFilters.length != 0) {
+ if (query != "")
+ query += "&";
+ query += "filter=" + queryEscape(JSON.stringify(gSampleFilters));
+ }
+ if (gTreeManager.hasNonTrivialSelection()) {
+ if (query != "")
+ query += "&";
+ query += "selection=" + queryEscape(gTreeManager.serializeCurrentSelectionSnapshot());
+ }
+ if (!gReportID) {
+ query = "uploadProfileFirst!";
+ }
+
+ return query;
+}
+
diff --git a/browser/devtools/profiler/cmd-profiler.jsm b/browser/devtools/profiler/cmd-profiler.jsm
new file mode 100644
index 000000000..e146626c5
--- /dev/null
+++ b/browser/devtools/profiler/cmd-profiler.jsm
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/Require.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+var Promise = require('util/promise');
+
+/*
+ * 'profiler' command. Doesn't do anything.
+ */
+gcli.addCommand({
+ name: "profiler",
+ description: gcli.lookup("profilerDesc"),
+ manual: gcli.lookup("profilerManual")
+});
+
+/*
+ * 'profiler open' command
+ */
+gcli.addCommand({
+ name: "profiler open",
+ description: gcli.lookup("profilerOpenDesc"),
+ params: [],
+
+ exec: function (args, context) {
+ return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+ .then(function () null);
+ }
+});
+
+/*
+ * 'profiler close' command
+ */
+gcli.addCommand({
+ name: "profiler close",
+ description: gcli.lookup("profilerCloseDesc"),
+ params: [],
+
+ exec: function (args, context) {
+ if (!getPanel(context, "jsprofiler"))
+ return;
+
+ return gDevTools.closeToolbox(context.environment.target)
+ .then(function () null);
+ }
+});
+
+/*
+ * 'profiler start' command
+ */
+gcli.addCommand({
+ name: "profiler start",
+ description: gcli.lookup("profilerStartDesc"),
+ returnType: "string",
+
+ params: [
+ {
+ name: "name",
+ type: "string",
+ manual: gcli.lookup("profilerStartManual")
+ }
+ ],
+
+ exec: function (args, context) {
+ function start() {
+ let name = args.name;
+ let panel = getPanel(context, "jsprofiler");
+ let profile = panel.getProfileByName(name) || panel.createProfile(name);
+
+ if (profile.isStarted) {
+ throw gcli.lookup("profilerAlreadyStarted");
+ }
+
+ if (profile.isFinished) {
+ throw gcli.lookup("profilerAlreadyFinished");
+ }
+
+ let item = panel.sidebar.getItemByProfile(profile);
+
+ if (panel.sidebar.selectedItem === item) {
+ profile.start();
+ } else {
+ panel.on("profileSwitched", () => profile.start());
+ panel.sidebar.selectedItem = item;
+ }
+
+ return gcli.lookup("profilerStarting2");
+ }
+
+ return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+ .then(start);
+ }
+});
+
+/*
+ * 'profiler stop' command
+ */
+gcli.addCommand({
+ name: "profiler stop",
+ description: gcli.lookup("profilerStopDesc"),
+ returnType: "string",
+
+ params: [
+ {
+ name: "name",
+ type: "string",
+ manual: gcli.lookup("profilerStopManual")
+ }
+ ],
+
+ exec: function (args, context) {
+ function stop() {
+ let panel = getPanel(context, "jsprofiler");
+ let profile = panel.getProfileByName(args.name);
+
+ if (!profile) {
+ throw gcli.lookup("profilerNotFound");
+ }
+
+ if (profile.isFinished) {
+ throw gcli.lookup("profilerAlreadyFinished");
+ }
+
+ if (!profile.isStarted) {
+ throw gcli.lookup("profilerNotStarted2");
+ }
+
+ let item = panel.sidebar.getItemByProfile(profile);
+
+ if (panel.sidebar.selectedItem === item) {
+ profile.stop();
+ } else {
+ panel.on("profileSwitched", () => profile.stop());
+ panel.sidebar.selectedItem = item;
+ }
+
+ return gcli.lookup("profilerStopping2");
+ }
+
+ return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+ .then(stop);
+ }
+});
+
+/*
+ * 'profiler list' command
+ */
+gcli.addCommand({
+ name: "profiler list",
+ description: gcli.lookup("profilerListDesc"),
+ returnType: "dom",
+ params: [],
+
+ exec: function (args, context) {
+ let panel = getPanel(context, "jsprofiler");
+
+ if (!panel) {
+ throw gcli.lookup("profilerNotReady");
+ }
+
+ let doc = panel.document;
+ let div = createXHTMLElement(doc, "div");
+ let ol = createXHTMLElement(doc, "ol");
+
+ for ([ uid, profile] of panel.profiles) {
+ let li = createXHTMLElement(doc, "li");
+ li.textContent = profile.name;
+ if (profile.isStarted) {
+ li.textContent += " *";
+ }
+ ol.appendChild(li);
+ }
+
+ div.appendChild(ol);
+ return div;
+ }
+});
+
+/*
+ * 'profiler show' command
+ */
+gcli.addCommand({
+ name: "profiler show",
+ description: gcli.lookup("profilerShowDesc"),
+
+ params: [
+ {
+ name: "name",
+ type: "string",
+ manual: gcli.lookup("profilerShowManual")
+ }
+ ],
+
+ exec: function (args, context) {
+ let panel = getPanel(context, "jsprofiler");
+
+ if (!panel) {
+ throw gcli.lookup("profilerNotReady");
+ }
+
+ let profile = panel.getProfileByName(args.name);
+ if (!profile) {
+ throw gcli.lookup("profilerNotFound");
+ }
+
+ panel.sidebar.selectedItem = panel.sidebar.getItemByProfile(profile);
+ }
+});
+
+function getPanel(context, id) {
+ if (context == null) {
+ return undefined;
+ }
+
+ let toolbox = gDevTools.getToolbox(context.environment.target);
+ return toolbox == null ? undefined : toolbox.getPanel(id);
+}
+
+function createXHTMLElement(document, tagname) {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/moz.build b/browser/devtools/profiler/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/profiler/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/profiler/profiler.xul b/browser/devtools/profiler/profiler.xul
new file mode 100644
index 000000000..fbda6d55b
--- /dev/null
+++ b/browser/devtools/profiler/profiler.xul
@@ -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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/profiler.css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css"?>
+
+<!DOCTYPE window [
+<!ENTITY % profilerDTD SYSTEM "chrome://browser/locale/devtools/profiler.dtd">
+ %profilerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <box flex="1" id="profiler-chrome" class="devtools-responsive-container">
+ <vbox class="profiler-sidebar">
+ <toolbar class="devtools-toolbar">
+ <toolbarbutton id="profiler-create"
+ class="devtools-toolbarbutton"
+ label="&profilerNew.label;"
+ disabled="true"/>
+ </toolbar>
+
+ <vbox id="profiles-list" flex="1">
+ </vbox>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <vbox flex="1">
+ <toolbar class="devtools-toolbar">
+ </toolbar>
+
+ <vbox flex="1" id="profiler-report">
+ <!-- Example:
+ <iframe id="profiler-cleo-1"
+ src="devtools/cleopatra.html" flex="1"></iframe>
+ -->
+ </vbox>
+ </vbox>
+ </box>
+</window>
diff --git a/browser/devtools/profiler/test/Makefile.in b/browser/devtools/profiler/test/Makefile.in
new file mode 100644
index 000000000..398afe09a
--- /dev/null
+++ b/browser/devtools/profiler/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+# Disabled for intermittent failures.
+# browser_profiler_run.js \
+# browser_profiler_bug_855244_multiple_tabs.js \
+# browser_profiler_controller.js \
+# browser_profiler_bug_830664_multiple_profiles.js \
+# browser_profiler_console_api.js \
+# browser_profiler_console_api_named.js \
+# browser_profiler_console_api_mixed.js \
+# browser_profiler_console_api_content.js \
+
+MOCHITEST_BROWSER_TESTS = \
+ browser_profiler_profiles.js \
+ browser_profiler_remote.js \
+ browser_profiler_bug_834878_source_buttons.js \
+ browser_profiler_cmd.js \
+ head.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_PAGES = \
+ mock_profiler_bug_834878_page.html \
+ mock_profiler_bug_834878_script.js \
+ mock_console_api.html \
+ $(NULL)
+
+MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js b/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
new file mode 100644
index 000000000..1acafd5c8
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel, gUid;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, function onSetUp(tab, browser, panel) {
+ gTab = tab;
+ gPanel = panel;
+
+ gPanel.once("profileCreated", function (_, uid) {
+ gUid = uid;
+ let profile = gPanel.profiles.get(uid);
+
+ if (profile.isReady) {
+ startProfiling();
+ } else {
+ profile.once("ready", startProfiling);
+ }
+ });
+ gPanel.createProfile();
+ });
+}
+
+function startProfiling() {
+ gPanel.profiles.get(gPanel.activeProfile.uid).once("started", function () {
+ setTimeout(function () {
+ sendFromProfile(2, "start");
+ gPanel.profiles.get(2).once("started", function () setTimeout(stopProfiling, 50));
+ }, 50);
+ });
+
+ sendFromProfile(gPanel.activeProfile.uid, "start");
+}
+
+function stopProfiling() {
+ is(getSidebarItem(1).attachment.state, PROFILE_RUNNING);
+ is(getSidebarItem(2).attachment.state, PROFILE_RUNNING);
+
+ gPanel.profiles.get(gPanel.activeProfile.uid).once("stopped", function () {
+ is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
+
+ sendFromProfile(2, "stop");
+ gPanel.profiles.get(2).once("stopped", confirmAndFinish);
+ });
+
+ sendFromProfile(gPanel.activeProfile.uid, "stop");
+}
+
+function confirmAndFinish(ev, data) {
+ is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
+ is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
+
+ tearDown(gTab, function onTearDown() {
+ gPanel = null;
+ gTab = null;
+ gUid = null;
+ });
+}
diff --git a/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js b/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js
new file mode 100644
index 000000000..3fdc40e95
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const BASE = "http://example.com/browser/browser/devtools/profiler/test/";
+const URL = BASE + "mock_profiler_bug_834878_page.html";
+const SCRIPT = BASE + "mock_profiler_bug_834878_script.js";
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, function onSetUp(tab, browser, panel) {
+ panel.once("profileCreated", function () {
+ let data = { uri: SCRIPT, line: 5, isChrome: false };
+
+ panel.displaySource(data, function onOpen() {
+ let target = TargetFactory.forTab(tab);
+ let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
+ let view = dbg.panelWin.DebuggerView;
+
+ is(view.Sources.selectedValue, data.uri, "URI is different");
+ is(view.editor.getCaretPosition().line, data.line - 1,
+ "Line is different");
+
+ // Test the case where script is already loaded.
+ view.editor.setCaretPosition(1);
+ gDevTools.showToolbox(target, "jsprofiler").then(function () {
+ panel.displaySource(data, function onOpenAgain() {
+ is(view.editor.getCaretPosition().line, data.line - 1,
+ "Line is different");
+ tearDown(tab);
+ });
+ });
+ });
+ });
+
+ panel.createProfile();
+ });
+}
diff --git a/browser/devtools/profiler/test/browser_profiler_bug_855244_multiple_tabs.js b/browser/devtools/profiler/test/browser_profiler_bug_855244_multiple_tabs.js
new file mode 100644
index 000000000..74a831d0e
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_bug_855244_multiple_tabs.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab1, gPanel1;
+let gTab2, gPanel2;
+
+// Tests that you can run the profiler in multiple tabs at the same
+// time and that closing the debugger panel in one tab doesn't lock
+// profilers in other tabs.
+
+registerCleanupFunction(function () {
+ gTab1 = gTab2 = gPanel1 = gPanel2 = null;
+});
+
+function test() {
+ waitForExplicitFinish();
+
+ openTwoTabs()
+ .then(startTwoProfiles)
+ .then(stopFirstProfile)
+ .then(stopSecondProfile)
+ .then(closeTabs)
+ .then(openTwoTabs)
+ .then(startTwoProfiles)
+ .then(closeFirstPanel)
+ .then(stopSecondProfile)
+ .then(closeTabs)
+ .then(finish);
+}
+
+function openTwoTabs() {
+ let deferred = Promise.defer();
+
+ setUp(URL, (tab, browser, panel) => {
+ gTab1 = tab;
+ gPanel1 = panel;
+
+ loadTab(URL, (tab, browser) => {
+ gTab2 = tab;
+ openProfiler(tab, () => {
+ let target = TargetFactory.forTab(tab);
+ gPanel2 = gDevTools.getToolbox(target).getPanel("jsprofiler");
+ deferred.resolve();
+ });
+ });
+ });
+
+ return deferred.promise;
+}
+
+function startTwoProfiles() {
+ let deferred = Promise.defer();
+ gPanel1.controller.start("Profile 1", (err) => {
+ ok(!err, "Profile in tab 1 started without errors");
+ gPanel2.controller.start("Profile 1", (err) => {
+ ok(!err, "Profile in tab 2 started without errors");
+ gPanel1.controller.isActive((err, isActive) => {
+ ok(isActive, "Profiler is active");
+ deferred.resolve();
+ });
+ });
+ });
+
+ return deferred.promise;
+}
+
+function stopFirstProfile() {
+ let deferred = Promise.defer();
+
+ gPanel1.controller.stop("Profile 1", (err, data) => {
+ ok(!err, "Profile in tab 1 stopped without errors");
+ ok(data, "Profile in tab 1 returned some data");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function stopSecondProfile() {
+ let deferred = Promise.defer();
+
+ gPanel2.controller.stop("Profile 1", (err, data) => {
+ ok(!err, "Profile in tab 2 stopped without errors");
+ ok(data, "Profile in tab 2 returned some data");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function closeTabs() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+function closeFirstPanel() {
+ let target = TargetFactory.forTab(gTab1);
+ let toolbox = gDevTools.getToolbox(target);
+ return toolbox.destroy;
+}
diff --git a/browser/devtools/profiler/test/browser_profiler_cmd.js b/browser/devtools/profiler/test/browser_profiler_cmd.js
new file mode 100644
index 000000000..321b01b48
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_cmd.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gcli = Cu.import("resource://gre/modules/devtools/gcli.jsm", {}).gcli;
+let gTarget, gPanel, gOptions;
+
+function cmd(typed, expected="") {
+ helpers.audit(gOptions, [{
+ setup: typed,
+ exec: { output: expected }
+ }]);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ helpers.addTabWithToolbar(URL, function (options) {
+ gOptions = options;
+ gTarget = options.target;
+
+ return gDevTools.showToolbox(options.target, "jsprofiler")
+ .then(setupGlobals)
+ .then(testProfilerStart)
+ .then(testProfilerList)
+ .then(testProfilerStop)
+ .then(testProfilerClose)
+ .then(testProfilerCloseWhenClosed)
+ }).then(finishUp);
+}
+
+function setupGlobals() {
+ let deferred = Promise.defer();
+ gPanel = gDevTools.getToolbox(gTarget).getPanel("jsprofiler");
+ deferred.resolve();
+ return deferred.promise;
+}
+
+function testProfilerStart() {
+ let deferred = Promise.defer();
+
+ gPanel.once("started", function () {
+ is(gPanel.profiles.size, 2, "There are two profiles");
+ ok(!gPanel.getProfileByName("Profile 1").isStarted, "Profile 1 wasn't started");
+ ok(gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was started");
+ cmd('profiler start "Profile 2"', "This profile has already been started");
+ deferred.resolve();
+ });
+
+ cmd("profiler start", gcli.lookup("profilerStarting2"));
+ return deferred.promise;
+}
+
+function testProfilerList() {
+ let deferred = Promise.defer();
+
+ cmd("profiler list", /^.*Profile\s1.*Profile\s2\s\*.*$/);
+ deferred.resolve();
+
+ return deferred.promise;
+}
+
+function testProfilerStop() {
+ let deferred = Promise.defer();
+
+ gPanel.once("stopped", function () {
+ ok(!gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was stopped");
+ ok(gPanel.getProfileByName("Profile 2").isFinished, "Profile 2 was stopped");
+ cmd('profiler stop "Profile 2"', "This profile has already been completed. " +
+ "Use 'profile show' command to see its results");
+ cmd('profiler stop "Profile 1"', "This profile has not been started yet. " +
+ "Use 'profile start' to start profiling");
+ cmd('profiler stop "invalid"', "Profile not found")
+ deferred.resolve();
+ });
+
+ cmd('profiler stop "Profile 2"', gcli.lookup("profilerStopping2"));
+ return deferred.promise;
+}
+
+function testProfilerShow() {
+ let deferred = Promise.defer();
+
+ is(gPanel.getProfileByName("Profile 2").uid, gPanel.activeProfile.uid,
+ "Profile 2 is active");
+
+ gPanel.once("profileSwitched", function () {
+ is(gPanel.getProfileByName("Profile 1").uid, gPanel.activeProfile.uid,
+ "Profile 1 is active");
+ cmd('profile show "invalid"', "Profile not found");
+ deferred.resolve();
+ });
+
+ cmd('profile show "Profile 1"');
+ return deferred.promise;
+}
+
+function testProfilerClose() {
+ let deferred = Promise.defer();
+
+ helpers.audit(gOptions, [{
+ setup: "profiler close",
+ completed: false,
+ exec: { output: "" }
+ }]);
+
+ let toolbox = gDevTools.getToolbox(gOptions.target);
+ if (!toolbox) {
+ ok(true, "Profiler was closed.");
+ deferred.resolve();
+ } else {
+ toolbox.on("destroyed", function () {
+ ok(true, "Profiler was closed.");
+ deferred.resolve();
+ });
+ }
+
+ return deferred.promise;
+}
+
+function testProfilerCloseWhenClosed() {
+ // We need to call this test to make sure there are no
+ // errors when executing 'profiler close' on a closed
+ // toolbox. See bug 863636 for more info.
+
+ let deferred = Promise.defer();
+
+ helpers.audit(gOptions, [{
+ setup: "profiler close",
+ completed: false,
+ exec: { output: "" }
+ }]);
+
+ let toolbox = gDevTools.getToolbox(gOptions.target);
+ if (!toolbox) {
+ ok(true, "Profiler was closed.");
+ deferred.resolve();
+ } else {
+ toolbox.on("destroyed", function () {
+ ok(true, "Profiler was closed.");
+ deferred.resolve();
+ });
+ }
+
+ return deferred.promise;
+}
+
+function finishUp() {
+ gTarget = null;
+ gPanel = null;
+ gOptions = null;
+ finish();
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_console_api.js b/browser/devtools/profiler/test/browser_profiler_console_api.js
new file mode 100644
index 000000000..0b25c0142
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_console_api.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, (tab, browser, panel) => {
+ gTab = tab;
+ gPanel = panel;
+
+ openConsole(tab, testConsoleProfile);
+ });
+}
+
+function testConsoleProfile(hud) {
+ hud.jsterm.clearOutput(true);
+
+ // Here we start two named profiles and then end one of them.
+ // profileEnd, when name is not provided, simply pops the latest
+ // profile.
+
+ let profilesStarted = 0;
+
+ function profileEnd(_, uid) {
+ let profile = gPanel.profiles.get(uid);
+
+ profile.once("started", () => {
+ if (++profilesStarted < 2)
+ return;
+
+ gPanel.off("profileCreated", profileEnd);
+ gPanel.profiles.get(3).once("stopped", () => {
+ openProfiler(gTab, checkProfiles);
+ });
+
+ hud.jsterm.execute("console.profileEnd()");
+ });
+ }
+
+ gPanel.on("profileCreated", profileEnd);
+ hud.jsterm.execute("console.profile()");
+ hud.jsterm.execute("console.profile()");
+}
+
+function checkProfiles(toolbox) {
+ let panel = toolbox.getPanel("jsprofiler");
+
+ is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
+ is(getSidebarItem(2, panel).attachment.state, PROFILE_RUNNING);
+ is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
+
+ // Make sure we can still stop profiles via the UI.
+
+ gPanel.profiles.get(2).once("stopped", () => {
+ is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
+ tearDown(gTab, () => gTab = gPanel = null);
+ });
+
+ sendFromProfile(2, "stop");
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_content.js b/browser/devtools/profiler/test/browser_profiler_console_api_content.js
new file mode 100644
index 000000000..67e0b1084
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_content.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+const BASE = "http://example.com/browser/browser/devtools/profiler/test/";
+const PAGE = BASE + "mock_console_api.html";
+
+let gTab, gPanel, gToolbox;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, (tab, browser, panel) => {
+ gTab = tab;
+ gPanel = panel;
+
+ openProfiler(tab, (toolbox) => {
+ gToolbox = toolbox;
+ loadUrl(PAGE, tab, () => {
+ gPanel.sidebar.on("stateChanged", (_, item) => {
+ if (item.attachment.state !== PROFILE_COMPLETED)
+ return;
+
+ runTests();
+ });
+ });
+ });
+ });
+}
+
+function runTests() {
+ is(getSidebarItem(1).attachment.state, PROFILE_IDLE);
+ is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
+
+ gPanel.once("parsed", () => {
+ function assertSampleAndFinish() {
+ let [win,doc] = getProfileInternals();
+ let sample = doc.getElementsByClassName("samplePercentage");
+
+ if (sample.length <= 0)
+ return void setTimeout(assertSampleAndFinish, 100);
+
+ ok(sample.length > 0, "We have Cleopatra UI displayed");
+ tearDown(gTab, () => {
+ gTab = null;
+ gPanel = null;
+ gToolbox = null;
+ });
+ }
+
+ assertSampleAndFinish();
+ });
+
+ let profile = gPanel.profiles.get(2);
+ gPanel.sidebar.selectedItem = gPanel.sidebar.getItemByProfile(profile);
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
new file mode 100644
index 000000000..548050e88
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, (tab, browser, panel) => {
+ gTab = tab;
+ gPanel = panel;
+
+ openProfiler(tab, runTests);
+ });
+}
+
+function runTests(toolbox) {
+ let panel = toolbox.getPanel("jsprofiler");
+
+ panel.profiles.get(1).once("started", () => {
+ is(getSidebarItem(1, panel).attachment.state, PROFILE_RUNNING);
+
+ openConsole(gTab, (hud) => {
+ panel.profiles.get(1).once("stopped", () => {
+ is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED);
+ tearDown(gTab, () => gTab = gPanel = null);
+ });
+
+ hud.jsterm.execute("console.profileEnd()");
+ });
+ });
+
+ sendFromProfile(1, "start");
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_named.js b/browser/devtools/profiler/test/browser_profiler_console_api_named.js
new file mode 100644
index 000000000..460676aa7
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_named.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, (tab, browser, panel) => {
+ gTab = tab;
+ gPanel = panel;
+
+ openConsole(tab, testConsoleProfile);
+ });
+}
+
+function testConsoleProfile(hud) {
+ hud.jsterm.clearOutput(true);
+
+ // Here we start two named profiles and then end one of them.
+
+ let profilesStarted = 0;
+
+ function profileEnd(_, uid) {
+ let profile = gPanel.profiles.get(uid);
+
+ profile.once("started", () => {
+ if (++profilesStarted < 2)
+ return;
+
+ gPanel.off("profileCreated", profileEnd);
+ gPanel.profiles.get(2).once("stopped", () => {
+ openProfiler(gTab, checkProfiles);
+ });
+
+ hud.jsterm.execute("console.profileEnd('Second')");
+ });
+ }
+
+ gPanel.on("profileCreated", profileEnd);
+ hud.jsterm.execute("console.profile('Second')");
+ hud.jsterm.execute("console.profile('Third')");
+}
+
+function checkProfiles(toolbox) {
+ let panel = toolbox.getPanel("jsprofiler");
+
+ is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
+ is(getSidebarItem(2, panel).attachment.name, "Second");
+ is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
+ is(getSidebarItem(3, panel).attachment.name, "Third");
+ is(getSidebarItem(3, panel).attachment.state, PROFILE_RUNNING);
+
+ // Make sure we can still stop profiles via the queue pop.
+
+ gPanel.profiles.get(3).once("stopped", () => {
+ openProfiler(gTab, () => {
+ is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
+ tearDown(gTab, () => gTab = gPanel = null);
+ });
+ });
+
+ openConsole(gTab, (hud) => hud.jsterm.execute("console.profileEnd()"));
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_controller.js b/browser/devtools/profiler/test/browser_profiler_controller.js
new file mode 100644
index 000000000..8881f01bd
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_controller.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, function onSetUp(tab, browser, panel) {
+ gTab = tab;
+ gPanel = panel;
+
+ testInactive(startFirstProfile);
+ });
+}
+
+function testInactive(next=function(){}) {
+ gPanel.controller.isActive(function (err, isActive) {
+ ok(!err, "isActive didn't return any errors");
+ ok(!isActive, "Profiler is not active");
+ next();
+ });
+}
+
+function testActive(next=function(){}) {
+ gPanel.controller.isActive(function (err, isActive) {
+ ok(!err, "isActive didn't return any errors");
+ ok(isActive, "Profiler is active");
+ next();
+ });
+}
+
+function startFirstProfile() {
+ gPanel.controller.start("Profile 1", function (err) {
+ ok(!err, "Profile 1 started without errors");
+ testActive(startSecondProfile);
+ });
+}
+
+function startSecondProfile() {
+ gPanel.controller.start("Profile 2", function (err) {
+ ok(!err, "Profile 2 started without errors");
+ testActive(stopFirstProfile);
+ });
+}
+
+function stopFirstProfile() {
+ gPanel.controller.stop("Profile 1", function (err, data) {
+ ok(!err, "Profile 1 stopped without errors");
+ ok(data, "Profiler returned some data");
+
+ testActive(stopSecondProfile);
+ });
+}
+
+function stopSecondProfile() {
+ gPanel.controller.stop("Profile 2", function (err, data) {
+ ok(!err, "Profile 2 stopped without errors");
+ ok(data, "Profiler returned some data");
+ testInactive(tearDown.call(null, gTab, function () gTab = gPanel = null));
+ });
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_profiles.js b/browser/devtools/profiler/test/browser_profiler_profiles.js
new file mode 100644
index 000000000..10f7b4bcb
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_profiles.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, function onSetUp(tab, browser, panel) {
+ gTab = tab;
+ gPanel = panel;
+
+ panel.once("profileCreated", onProfileCreated);
+ panel.once("profileSwitched", onProfileSwitched);
+
+ testNewProfile();
+ });
+}
+
+function testNewProfile() {
+ is(gPanel.profiles.size, 1, "There is only one profile");
+
+ let btn = gPanel.document.getElementById("profiler-create");
+ ok(!btn.getAttribute("disabled"), "Create Profile button is not disabled");
+ btn.click();
+}
+
+function onProfileCreated(name, uid) {
+ is(gPanel.profiles.size, 2, "There are two profiles now");
+ ok(gPanel.activeProfile.uid !== uid, "New profile is not yet active");
+
+ let btn = gPanel.document.getElementById("profile-" + uid);
+ ok(btn, "Profile item has been added to the sidebar");
+ btn.click();
+}
+
+function onProfileSwitched(name, uid) {
+ gPanel.once("profileCreated", onNamedProfileCreated);
+ gPanel.once("profileSwitched", onNamedProfileSwitched);
+
+ ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
+ gPanel.createProfile("Custom Profile");
+}
+
+function onNamedProfileCreated(name, uid) {
+ is(gPanel.profiles.size, 3, "There are three profiles now");
+ is(gPanel.getProfileByUID(uid).name, "Custom Profile", "Name is correct");
+
+ let profile = gPanel.profiles.get(uid);
+ let data = gPanel.sidebar.getItemByProfile(profile).attachment;
+
+ is(data.uid, uid, "UID is correct");
+ is(data.name, "Custom Profile", "Name is correct on the label");
+
+ let btn = gPanel.document.getElementById("profile-" + uid);
+ ok(btn, "Profile item has been added to the sidebar");
+ btn.click();
+}
+
+function onNamedProfileSwitched(name, uid) {
+ ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
+
+ tearDown(gTab, function onTearDown() {
+ gPanel = null;
+ gTab = null;
+ });
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_remote.js b/browser/devtools/profiler/test/browser_profiler_remote.js
new file mode 100644
index 000000000..450f9d290
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_remote.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let temp = {};
+
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
+let DebuggerServer = temp.DebuggerServer;
+
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm", temp);
+let DebuggerClient = temp.DebuggerClient;
+let debuggerSocketConnect = temp.debuggerSocketConnect;
+
+Cu.import("resource:///modules/devtools/ProfilerController.jsm", temp);
+let ProfilerController = temp.ProfilerController;
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref(REMOTE_ENABLED, true);
+
+ loadTab(URL, function onTabLoad(tab, browser) {
+ DebuggerServer.init(function () true);
+ DebuggerServer.addBrowserActors();
+ is(DebuggerServer._socketConnections, 0);
+
+ DebuggerServer.openListener(2929);
+ is(DebuggerServer._socketConnections, 1);
+
+ let transport = debuggerSocketConnect("127.0.0.1", 2929);
+ let client = new DebuggerClient(transport);
+ client.connect(function onClientConnect() {
+ let target = { isRemote: true, client: client };
+ let controller = new ProfilerController(target);
+
+ controller.connect(function onControllerConnect() {
+ // If this callback is called, this means listTabs call worked.
+ // Which means that the transport worked. Time to finish up this
+ // test.
+
+ function onShutdown() {
+ window.removeEventListener("Debugger:Shutdown", onShutdown, true);
+ transport = client = null;
+ finish();
+ }
+
+ window.addEventListener("Debugger:Shutdown", onShutdown, true);
+
+ client.close(function () {
+ gBrowser.removeTab(tab);
+ });
+ });
+ });
+ });
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/browser_profiler_run.js b/browser/devtools/profiler/test/browser_profiler_run.js
new file mode 100644
index 000000000..80e162ffb
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_run.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel, gAttempts = 0;
+
+function test() {
+ waitForExplicitFinish();
+
+ setUp(URL, function onSetUp(tab, browser, panel) {
+ gTab = tab;
+ gPanel = panel;
+
+ panel.once("started", onStart);
+ panel.once("parsed", onParsed);
+
+ testUI();
+ });
+}
+
+function testUI() {
+ ok(gPanel, "Profiler panel exists");
+ ok(gPanel.activeProfile, "Active profile exists");
+
+ let [win, doc] = getProfileInternals();
+ let startButton = doc.querySelector(".controlPane #startWrapper button");
+ let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+
+ ok(startButton, "Start button exists");
+ ok(stopButton, "Stop button exists");
+
+ startButton.click();
+}
+
+function onStart() {
+ gPanel.controller.isActive(function (err, isActive) {
+ ok(isActive, "Profiler is running");
+
+ let [win, doc] = getProfileInternals();
+ let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+
+ setTimeout(function () stopButton.click(), 100);
+ });
+}
+
+function onParsed() {
+ function assertSample() {
+ let [win,doc] = getProfileInternals();
+ let sample = doc.getElementsByClassName("samplePercentage");
+
+ if (sample.length <= 0) {
+ return void setTimeout(assertSample, 100);
+ }
+
+ ok(sample.length > 0, "We have some items displayed");
+ is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
+
+ tearDown(gTab, function onTearDown() {
+ gPanel = null;
+ gTab = null;
+ });
+ }
+
+ assertSample();
+}
diff --git a/browser/devtools/profiler/test/head.js b/browser/devtools/profiler/test/head.js
new file mode 100644
index 000000000..8833f57cf
--- /dev/null
+++ b/browser/devtools/profiler/test/head.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let temp = {};
+
+const PROFILER_ENABLED = "devtools.profiler.enabled";
+const REMOTE_ENABLED = "devtools.debugger.remote-enabled";
+const PROFILE_IDLE = 0;
+const PROFILE_RUNNING = 1;
+const PROFILE_COMPLETED = 2;
+
+Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
+let gDevTools = temp.gDevTools;
+
+Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
+let TargetFactory = temp.devtools.TargetFactory;
+
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
+let DebuggerServer = temp.DebuggerServer;
+
+Cu.import("resource:///modules/HUDService.jsm", temp);
+let HUDService = temp.HUDService;
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+
+registerCleanupFunction(function () {
+ helpers = null;
+ Services.prefs.clearUserPref(PROFILER_ENABLED);
+ Services.prefs.clearUserPref(REMOTE_ENABLED);
+ DebuggerServer.destroy();
+});
+
+function getProfileInternals(uid) {
+ let profile = (uid != null) ? gPanel.profiles.get(uid) : gPanel.activeProfile;
+ let win = profile.iframe.contentWindow;
+ let doc = win.document;
+
+ return [win, doc];
+}
+
+function getSidebarItem(uid, panel=gPanel) {
+ let profile = panel.profiles.get(uid);
+ return panel.sidebar.getItemByProfile(profile);
+}
+
+function sendFromProfile(uid, msg) {
+ let [win, doc] = getProfileInternals(uid);
+ win.parent.postMessage({ uid: uid, status: msg }, "*");
+}
+
+function loadTab(url, callback) {
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ loadUrl(url, tab, callback);
+}
+
+function loadUrl(url, tab, callback) {
+ content.location.assign(url);
+ let browser = gBrowser.getBrowserForTab(tab);
+ if (browser.contentDocument.readyState === "complete") {
+ callback(tab, browser);
+ return;
+ }
+
+ let onLoad = function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ callback(tab, browser);
+ };
+
+ browser.addEventListener("load", onLoad, true);
+}
+
+function openProfiler(tab, callback) {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "jsprofiler").then(callback);
+}
+
+function openConsole(tab, cb=function(){}) {
+ // This function was borrowed from webconsole/test/head.js
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.showToolbox(target, "webconsole").then(function (toolbox) {
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ cb(hud);
+ });
+}
+
+function closeProfiler(tab, callback) {
+ let target = TargetFactory.forTab(tab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.destroy().then(callback);
+}
+
+function setUp(url, callback=function(){}) {
+ Services.prefs.setBoolPref(PROFILER_ENABLED, true);
+
+ loadTab(url, function onTabLoad(tab, browser) {
+ openProfiler(tab, function onProfilerOpen() {
+ let target = TargetFactory.forTab(tab);
+ let panel = gDevTools.getToolbox(target).getPanel("jsprofiler");
+ callback(tab, browser, panel);
+ });
+ });
+}
+
+function tearDown(tab, callback=function(){}) {
+ closeProfiler(tab, function onProfilerClose() {
+ callback();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+
+ finish();
+ });
+}
diff --git a/browser/devtools/profiler/test/mock_console_api.html b/browser/devtools/profiler/test/mock_console_api.html
new file mode 100644
index 000000000..2a626c9aa
--- /dev/null
+++ b/browser/devtools/profiler/test/mock_console_api.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>console.profile from content</title>
+ </head>
+
+ <body>
+ <script>
+ console.profile();
+ var a = new Array(500);
+ while (a.length) {
+ a.shift();
+ }
+ console.profileEnd();
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/browser/devtools/profiler/test/mock_profiler_bug_834878_page.html b/browser/devtools/profiler/test/mock_profiler_bug_834878_page.html
new file mode 100644
index 000000000..ad150b98a
--- /dev/null
+++ b/browser/devtools/profiler/test/mock_profiler_bug_834878_page.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>Profiler Script Linking Test</title>
+ <script type="text/javascript" src="mock_profiler_bug_834878_script.js">
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/devtools/profiler/test/mock_profiler_bug_834878_script.js b/browser/devtools/profiler/test/mock_profiler_bug_834878_script.js
new file mode 100644
index 000000000..0ef5f3772
--- /dev/null
+++ b/browser/devtools/profiler/test/mock_profiler_bug_834878_script.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function main() {
+ console.log("Hello, World!");
+ return 0;
+} \ No newline at end of file
diff --git a/browser/devtools/profiler/test/moz.build b/browser/devtools/profiler/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/profiler/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/responsivedesign/CmdResize.jsm b/browser/devtools/responsivedesign/CmdResize.jsm
new file mode 100644
index 000000000..dd49e9d59
--- /dev/null
+++ b/browser/devtools/responsivedesign/CmdResize.jsm
@@ -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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://branding/locale/brand.properties").
+ GetStringFromName("brandShortName");
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+
+/* Responsive Mode commands */
+gcli.addCommand({
+ name: 'resize',
+ description: gcli.lookup('resizeModeDesc')
+});
+
+gcli.addCommand({
+ name: 'resize on',
+ description: gcli.lookup('resizeModeOnDesc'),
+ manual: gcli.lookupFormat('resizeModeManual2', [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+});
+
+gcli.addCommand({
+ name: 'resize off',
+ description: gcli.lookup('resizeModeOffDesc'),
+ manual: gcli.lookupFormat('resizeModeManual2', [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+});
+
+gcli.addCommand({
+ name: 'resize toggle',
+ buttonId: "command-button-responsive",
+ buttonClass: "command-button",
+ tooltipText: gcli.lookup("resizeModeToggleTooltip"),
+ description: gcli.lookup('resizeModeToggleDesc'),
+ manual: gcli.lookupFormat('resizeModeManual2', [BRAND_SHORT_NAME]),
+ state: {
+ isChecked: function(aTarget) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
+ return mgr.isActiveForTab(aTarget.tab);
+ },
+ onChange: function(aTarget, aChangeHandler) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
+ mgr.on("on", aChangeHandler);
+ mgr.on("off", aChangeHandler);
+ },
+ offChange: function(aTarget, aChangeHandler) {
+ if (aTarget.tab) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
+ mgr.off("on", aChangeHandler);
+ mgr.off("off", aChangeHandler);
+ }
+ },
+ },
+ exec: gcli_cmd_resize
+});
+
+gcli.addCommand({
+ name: 'resize to',
+ description: gcli.lookup('resizeModeToDesc'),
+ params: [
+ {
+ name: 'width',
+ type: 'number',
+ description: gcli.lookup("resizePageArgWidthDesc"),
+ },
+ {
+ name: 'height',
+ type: 'number',
+ description: gcli.lookup("resizePageArgHeightDesc"),
+ },
+ ],
+ exec: gcli_cmd_resize
+});
+
+function gcli_cmd_resize(args, context) {
+ let browserDoc = context.environment.chromeDocument;
+ let browserWindow = browserDoc.defaultView;
+ let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
+ mgr.handleGcliCommand(browserWindow,
+ browserWindow.gBrowser.selectedTab,
+ this.name,
+ args);
+}
diff --git a/browser/devtools/responsivedesign/Makefile.in b/browser/devtools/responsivedesign/Makefile.in
new file mode 100644
index 000000000..fe75dad69
--- /dev/null
+++ b/browser/devtools/responsivedesign/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/responsivedesign/moz.build b/browser/devtools/responsivedesign/moz.build
new file mode 100644
index 000000000..5abe8b3be
--- /dev/null
+++ b/browser/devtools/responsivedesign/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
+
diff --git a/browser/devtools/responsivedesign/responsivedesign.jsm b/browser/devtools/responsivedesign/responsivedesign.jsm
new file mode 100644
index 000000000..b7122d3bb
--- /dev/null
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -0,0 +1,710 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
+
+const MIN_WIDTH = 50;
+const MIN_HEIGHT = 50;
+
+const MAX_WIDTH = 10000;
+const MAX_HEIGHT = 10000;
+
+this.ResponsiveUIManager = {
+ /**
+ * 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 (aTab.__responsiveUI) {
+ aTab.__responsiveUI.close();
+ } else {
+ new ResponsiveUI(aWindow, aTab);
+ }
+ },
+
+ /**
+ * Returns true if responsive view is active for the provided tab.
+ *
+ * @param aTab the tab targeted.
+ */
+ isActiveForTab: function(aTab) {
+ return !!aTab.__responsiveUI;
+ },
+
+ /**
+ * Handle gcli commands.
+ *
+ * @param aWindow the browser window.
+ * @param aTab the tab targeted.
+ * @param aCommand the command name.
+ * @param aArgs command arguments.
+ */
+ handleGcliCommand: function(aWindow, aTab, aCommand, aArgs) {
+ switch (aCommand) {
+ case "resize to":
+ if (!aTab.__responsiveUI) {
+ new ResponsiveUI(aWindow, aTab);
+ }
+ aTab.__responsiveUI.setSize(aArgs.width, aArgs.height);
+ break;
+ case "resize on":
+ if (!aTab.__responsiveUI) {
+ new ResponsiveUI(aWindow, aTab);
+ }
+ break;
+ case "resize off":
+ if (aTab.__responsiveUI) {
+ aTab.__responsiveUI.close();
+ }
+ break;
+ case "resize toggle":
+ this.toggle(aWindow, aTab);
+ default:
+ }
+ }
+}
+
+EventEmitter.decorate(ResponsiveUIManager);
+
+let presets = [
+ // 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.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();
+
+ // Try to load presets from prefs
+ if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
+ try {
+ presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
+ } catch(e) {
+ // User pref is malformated.
+ Cu.reportError("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 {
+ Cu.reportError("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.customPreset.key;
+ }
+
+ this.container.setAttribute("responsivemode", "true");
+ this.stack.setAttribute("responsivemode", "true");
+
+ // Let's bind some callbacks.
+ this.bound_presetSelected = this.presetSelected.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_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_onKeypress = this.onKeypress.bind(this);
+
+ // Events
+ this.tab.addEventListener("TabClose", this);
+ this.tabContainer.addEventListener("TabSelect", this);
+ this.mainWindow.document.addEventListener("keypress", this.bound_onKeypress, false);
+
+ this.buildUI();
+ this.checkMenus();
+
+ try {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
+ this.rotate();
+ }
+ } catch(e) {}
+
+ if (this._floatingScrollbars)
+ switchToFloatingScrollbars(this.tab);
+
+ this.tab.__responsiveUI = this;
+
+ this._telemetry.toolOpened("responsive");
+
+ ResponsiveUIManager.emit("on", this.tab, this);
+}
+
+ResponsiveUI.prototype = {
+ _transitionsEnabled: true,
+ _floatingScrollbars: Services.appinfo.OS != "Darwin",
+ get transitionsEnabled() 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");
+ }
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners. Reset the style.
+ */
+ close: function RUI_unload() {
+ if (this.closing)
+ return;
+ this.closing = true;
+
+ if (this._floatingScrollbars)
+ switchToNativeScrollbars(this.tab);
+
+ this.unCheckMenus();
+ // Reset style of the stack.
+ let style = "max-width: none;" +
+ "min-width: 0;" +
+ "max-height: none;" +
+ "min-height: 0;";
+ this.stack.setAttribute("style", style);
+
+ if (this.isResizing)
+ this.stopResizing();
+
+ // Remove listeners.
+ this.mainWindow.document.removeEventListener("keypress", this.bound_onKeypress, false);
+ this.menulist.removeEventListener("select", this.bound_presetSelected, true);
+ this.tab.removeEventListener("TabClose", this);
+ this.tabContainer.removeEventListener("TabSelect", this);
+ this.rotatebutton.removeEventListener("command", this.bound_rotate, 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);
+
+ // Removed elements.
+ this.container.removeChild(this.toolbar);
+ this.stack.removeChild(this.resizer);
+ this.stack.removeChild(this.resizeBar);
+
+ // Unset the responsive mode.
+ this.container.removeAttribute("responsivemode");
+ this.stack.removeAttribute("responsivemode");
+
+ delete this.tab.__responsiveUI;
+ this._telemetry.toolClosed("responsive");
+ ResponsiveUIManager.emit("off", this.tab, this);
+ },
+
+ /**
+ * Handle keypressed.
+ *
+ * @param aEvent
+ */
+ onKeypress: function RUI_onKeypress(aEvent) {
+ if (aEvent.keyCode == this.mainWindow.KeyEvent.DOM_VK_ESCAPE &&
+ this.mainWindow.gBrowser.selectedBrowser == this.browser) {
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ this.close();
+ }
+ },
+
+ /**
+ * Handle events
+ */
+ handleEvent: function (aEvent) {
+ switch (aEvent.type) {
+ case "TabClose":
+ this.close();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.checkMenus();
+ } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) {
+ this.unCheckMenus();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Check the menu items.
+ */
+ checkMenus: function RUI_checkMenus() {
+ this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "true");
+ },
+
+ /**
+ * Uncheck the menu items.
+ */
+ unCheckMenus: function RUI_unCheckMenus() {
+ this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "false");
+ },
+
+ /**
+ * Build the toolbar and the resizers.
+ *
+ * <vbox class="browserContainer"> From tabbrowser.xml
+ * <toolbar class="devtools-toolbar devtools-responsiveui-toolbar">
+ * <menulist class="devtools-menulist"/> // presets
+ * <toolbarbutton tabindex="0" class="devtools-toolbarbutton" label="rotate"/> // rotate
+ * <toolbarbutton tabindex="0" class="devtools-toolbarbutton devtools-closebutton" tooltiptext="Leave Responsive Design View"/> // close
+ * </toolbar>
+ * <stack class="browserStack"> From tabbrowser.xml
+ * <browser/>
+ * <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebar" top="0" right="0"/>
+ * </stack>
+ * </vbox>
+ */
+ buildUI: function RUI_buildUI() {
+ // Toolbar
+ this.toolbar = this.chromeDoc.createElement("toolbar");
+ this.toolbar.className = "devtools-toolbar devtools-responsiveui-toolbar";
+
+ this.menulist = this.chromeDoc.createElement("menulist");
+ this.menulist.className = "devtools-menulist";
+
+ this.menulist.addEventListener("select", this.bound_presetSelected, 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("label", this.strings.GetStringFromName("responsiveUI.rotate"));
+ this.rotatebutton.className = "devtools-toolbarbutton";
+ this.rotatebutton.addEventListener("command", this.bound_rotate, true);
+
+ this.closebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.closebutton.setAttribute("tabindex", "0");
+ this.closebutton.className = "devtools-toolbarbutton devtools-closebutton";
+ this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close"));
+ this.closebutton.addEventListener("command", this.bound_close, true);
+
+ this.toolbar.appendChild(this.closebutton);
+ this.toolbar.appendChild(this.menulist);
+ this.toolbar.appendChild(this.rotatebutton);
+
+ // Resizers
+ this.resizer = this.chromeDoc.createElement("box");
+ this.resizer.className = "devtools-responsiveui-resizehandle";
+ this.resizer.setAttribute("right", "0");
+ this.resizer.setAttribute("bottom", "0");
+ this.resizer.onmousedown = this.bound_startResizing;
+
+ this.resizeBar = this.chromeDoc.createElement("box");
+ this.resizeBar.className = "devtools-responsiveui-resizebar";
+ this.resizeBar.setAttribute("top", "0");
+ this.resizeBar.setAttribute("right", "0");
+ this.resizeBar.onmousedown = this.bound_startResizing;
+
+ this.container.insertBefore(this.toolbar, this.stack);
+ this.stack.appendChild(this.resizer);
+ this.stack.appendChild(this.resizeBar);
+ },
+
+ /**
+ * 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 = Math.round(aPreset.width) + "x" + Math.round(aPreset.height);
+ if (aPreset.custom) {
+ let str = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
+ aMenuitem.setAttribute("label", str);
+ } else if (aPreset.name != null && aPreset.name !== "") {
+ let str = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
+ aMenuitem.setAttribute("label", str);
+ } else {
+ 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.
+ *
+ * @param aPreset preset to apply.
+ */
+ loadPreset: function RUI_loadPreset(aPreset) {
+ this.setSize(aPreset.width, aPreset.height);
+ },
+
+ /**
+ * 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.customNamePromptTitle");
+ 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
+ let menuitem = this.customMenuitem;
+ this.menulist.selectedItem = menuitem;
+ this.currentPresetKey = this.customPreset.key;
+ 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.setSize(w, 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.setSize(height, width);
+
+ if (selectedPreset.custom) {
+ this.saveCustomSize();
+ } else {
+ this.rotateValue = !this.rotateValue;
+ this.saveCurrentPreset();
+ }
+ },
+
+ /**
+ * Change the size of the browser.
+ *
+ * @param aWidth width of the browser.
+ * @param aHeight height of the browser.
+ */
+ setSize: function RUI_setSize(aWidth, aHeight) {
+ aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
+ aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_WIDTH);
+
+ // We resize the containing stack.
+ let style = "max-width: %width;" +
+ "min-width: %width;" +
+ "max-height: %height;" +
+ "min-height: %height;";
+
+ style = style.replace(/%width/g, aWidth + "px");
+ style = style.replace(/%height/g, aHeight + "px");
+
+ this.stack.setAttribute("style", style);
+
+ if (!this.ignoreY)
+ this.resizeBar.setAttribute("top", Math.round(aHeight / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ // We uptate the custom menuitem if we are using it
+ if (selectedPreset.custom) {
+ selectedPreset.width = aWidth;
+ 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.resizeBar);
+
+ this.isResizing = true;
+ },
+
+ /**
+ * Resizing on mouse move.
+ *
+ * @param aEvent
+ */
+ onDrag: function RUI_onDrag(aEvent) {
+ let deltaX = aEvent.screenX - this.lastScreenX;
+ let deltaY = aEvent.screenY - this.lastScreenY;
+
+ if (this.ignoreY)
+ deltaY = 0;
+
+ let width = this.customPreset.width + deltaX;
+ let height = this.customPreset.height + deltaY;
+
+ if (width < MIN_WIDTH) {
+ width = MIN_WIDTH;
+ } else {
+ this.lastScreenX = aEvent.screenX;
+ }
+
+ if (height < MIN_HEIGHT) {
+ height = MIN_HEIGHT;
+ } else {
+ this.lastScreenY = aEvent.screenY;
+ }
+
+ this.setSize(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.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));
+ },
+}
+
+XPCOMUtils.defineLazyGetter(ResponsiveUI.prototype, "strings", function () {
+ return Services.strings.createBundle("chrome://browser/locale/devtools/responsiveUI.properties");
+});
diff --git a/browser/devtools/responsivedesign/test/Makefile.in b/browser/devtools/responsivedesign/test/Makefile.in
new file mode 100644
index 000000000..a01d9dba9
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_responsiveui.js \
+ browser_responsiveuiaddcustompreset.js \
+ browser_responsiveruleview.js \
+ browser_responsive_cmd.js \
+ browser_responsivecomputedview.js \
+ head.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/responsivedesign/test/browser_responsive_cmd.js b/browser/devtools/responsivedesign/test/browser_responsive_cmd.js
new file mode 100644
index 000000000..5b468e7d9
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsive_cmd.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ function isOpen() {
+ return !!gBrowser.selectedTab.__responsiveUI;
+ }
+
+ helpers.addTabWithToolbar("about:blank", function(options) {
+ return helpers.audit(options, [
+ {
+ setup: "resize toggle",
+ check: {
+ input: 'resize toggle',
+ hints: '',
+ markup: 'VVVVVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(isOpen(), "responsive mode is open");
+ },
+ },
+ {
+ setup: "resize toggle",
+ check: {
+ input: 'resize toggle',
+ hints: '',
+ markup: 'VVVVVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(!isOpen(), "responsive mode is closed");
+ },
+ },
+ {
+ setup: "resize on",
+ check: {
+ input: 'resize on',
+ hints: '',
+ markup: 'VVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(isOpen(), "responsive mode is open");
+ },
+ },
+ {
+ setup: "resize off",
+ check: {
+ input: 'resize off',
+ hints: '',
+ markup: 'VVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(!isOpen(), "responsive mode is closed");
+ },
+ },
+ {
+ setup: "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: function() {
+ ok(isOpen(), "responsive mode is open");
+ },
+ },
+ {
+ setup: "resize off",
+ check: {
+ input: 'resize off',
+ hints: '',
+ markup: 'VVVVVVVVVV',
+ status: 'VALID'
+ },
+ exec: {
+ output: ""
+ },
+ post: function() {
+ ok(!isOpen(), "responsive mode is closed");
+ },
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/responsivedesign/test/browser_responsivecomputedview.js b/browser/devtools/responsivedesign/test/browser_responsivecomputedview.js
new file mode 100644
index 000000000..1b51a8b3b
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsivecomputedview.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let instance;
+
+ let computedView;
+ let inspector;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>"
+
+ function computedWidth() {
+ for (let prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+ }
+
+ function startTest() {
+ document.getElementById("Tools:ResponsiveUI").doCommand();
+ executeSoon(onUIOpen);
+ }
+
+ function onUIOpen() {
+ instance = gBrowser.selectedTab.__responsiveUI;
+ ok(instance, "instance of the module is attached to the tab.");
+
+ instance.stack.setAttribute("notransition", "true");
+ registerCleanupFunction(function() {
+ instance.stack.removeAttribute("notransition");
+ });
+
+ instance.setSize(500, 500);
+
+ openInspector(onInspectorUIOpen);
+ }
+
+ function onInspectorUIOpen(aInspector) {
+ inspector = aInspector;
+ ok(inspector, "Got inspector instance");
+ inspector.sidebar.select("computedview");
+
+ let div = content.document.getElementsByTagName("div")[0];
+
+ inspector.sidebar.once("computedview-ready", function() {
+ Services.obs.addObserver(testShrink, "StyleInspector-populated", false);
+ inspector.selection.setNode(div);
+ });
+ }
+
+ function testShrink() {
+ Services.obs.removeObserver(testShrink, "StyleInspector-populated");
+
+ computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
+ ok(computedView, "We have access to the Computed View object");
+
+ is(computedWidth(), "500px", "Should show 500px initially.");
+
+ Services.obs.addObserver(function onShrink() {
+ Services.obs.removeObserver(onShrink, "StyleInspector-populated");
+ is(computedWidth(), "100px", "div should be 100px after shrinking.");
+ testGrow();
+ }, "StyleInspector-populated", false);
+
+ instance.setSize(100, 100);
+ }
+
+ function testGrow() {
+ Services.obs.addObserver(function onGrow() {
+ Services.obs.removeObserver(onGrow, "StyleInspector-populated");
+ is(computedWidth(), "500px", "Should be 500px after growing.");
+ finishUp();
+ }, "StyleInspector-populated", false);
+
+ instance.setSize(500, 500);
+ }
+
+ function finishUp() {
+ document.getElementById("Tools:ResponsiveUI").doCommand();
+
+ // Menus are correctly updated?
+ is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "false", "menu unchecked");
+
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/responsivedesign/test/browser_responsiveruleview.js b/browser/devtools/responsivedesign/test/browser_responsiveruleview.js
new file mode 100644
index 000000000..9aeb3147a
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsiveruleview.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let instance;
+
+ let ruleView;
+ let inspector;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>"
+
+ function numberOfRules() {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+ }
+
+ function startTest() {
+ document.getElementById("Tools:ResponsiveUI").doCommand();
+ executeSoon(onUIOpen);
+ }
+
+ function onUIOpen() {
+ instance = gBrowser.selectedTab.__responsiveUI;
+ ok(instance, "instance of the module is attached to the tab.");
+
+ instance.stack.setAttribute("notransition", "true");
+ registerCleanupFunction(function() {
+ instance.stack.removeAttribute("notransition");
+ });
+
+ instance.setSize(500, 500);
+
+ openInspector(onInspectorUIOpen);
+ }
+
+ function onInspectorUIOpen(aInspector) {
+ inspector = aInspector;
+ ok(inspector, "Got inspector instance");
+ inspector.sidebar.select("ruleview");
+
+ let div = content.document.getElementsByTagName("div")[0];
+
+ inspector.sidebar.once("ruleview-ready", function() {
+ Services.obs.addObserver(testShrink, "StyleInspector-populated", false);
+ inspector.selection.setNode(div);
+ });
+ }
+
+ function testShrink() {
+ Services.obs.removeObserver(testShrink, "StyleInspector-populated");
+
+ ruleView = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
+
+ is(numberOfRules(), 2, "Should have two rules initially.");
+
+ ruleView.element.addEventListener("CssRuleViewRefreshed", function refresh() {
+ ruleView.element.removeEventListener("CssRuleViewRefreshed", refresh, false);
+ is(numberOfRules(), 3, "Should have three rules after shrinking.");
+ testGrow();
+ }, false);
+
+ instance.setSize(100, 100);
+ }
+
+ function testGrow() {
+ ruleView.element.addEventListener("CssRuleViewRefreshed", function refresh() {
+ ruleView.element.removeEventListener("CssRuleViewRefreshed", refresh, false);
+ is(numberOfRules(), 2, "Should have two rules after growing.");
+ finishUp();
+ }, false);
+
+ instance.setSize(500, 500);
+ }
+
+ function finishUp() {
+ document.getElementById("Tools:ResponsiveUI").doCommand();
+
+ // Menus are correctly updated?
+ is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "false", "menu unchecked");
+
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/devtools/responsivedesign/test/browser_responsiveui.js b/browser/devtools/responsivedesign/test/browser_responsiveui.js
new file mode 100644
index 000000000..47cebfd39
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsiveui.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let instance, widthBeforeClose, heightBeforeClose;
+ let mgr = ResponsiveUI.ResponsiveUIManager;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,mop";
+
+ function startTest() {
+ document.getElementById("Tools:ResponsiveUI").removeAttribute("disabled");
+ mgr.once("on", function() {executeSoon(onUIOpen)});
+ synthesizeKeyFromKeyTag("key_responsiveUI");
+ }
+
+ function onUIOpen() {
+ // Is it open?
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+
+ // Menus are correctly updated?
+ is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "true", "menus checked");
+
+ instance = gBrowser.selectedTab.__responsiveUI;
+ ok(instance, "instance of the module is attached to the tab.");
+
+ if (instance._floatingScrollbars) {
+ ensureScrollbarsAreFloating();
+ }
+
+ instance.transitionsEnabled = false;
+
+ testPresets();
+ }
+
+ function ensureScrollbarsAreFloating() {
+ let body = gBrowser.contentDocument.body;
+ let html = gBrowser.contentDocument.documentElement;
+
+ let originalWidth = body.getBoundingClientRect().width;
+
+ html.style.overflowY = "scroll"; // Force scrollbars
+ // Flush. Should not be needed as getBoundingClientRect() should flush,
+ // but just in case.
+ gBrowser.contentWindow.getComputedStyle(html).overflowY;
+ let newWidth = body.getBoundingClientRect().width;
+ is(originalWidth, newWidth, "Floating scrollbars are presents");
+ }
+
+ function testPresets() {
+ function testOnePreset(c) {
+ if (c == 0) {
+ executeSoon(testCustom);
+ return;
+ }
+ instance.menulist.selectedIndex = c;
+ let item = instance.menulist.firstChild.childNodes[c];
+ let [width, height] = extractSizeFromString(item.getAttribute("label"));
+ is(content.innerWidth, width, "preset " + c + ": dimension valid (width)");
+ is(content.innerHeight, height, "preset " + c + ": dimension valid (height)");
+
+ testOnePreset(c - 1);
+ }
+ // Starting from length - 4 because last 3 items are not presets : separator, addbutton and removebutton
+ testOnePreset(instance.menulist.firstChild.childNodes.length - 4);
+ }
+
+ function extractSizeFromString(str) {
+ let numbers = str.match(/(\d+)[^\d]*(\d+)/);
+ if (numbers) {
+ return [numbers[1], numbers[2]];
+ } else {
+ return [null, null];
+ }
+ }
+
+ function testCustom() {
+ let initialWidth = content.innerWidth;
+ let initialHeight = content.innerHeight;
+
+ let x = 2, y = 2;
+ EventUtils.synthesizeMouse(instance.resizer, x, y, {type: "mousedown"}, window);
+ x += 20; y += 10;
+ EventUtils.synthesizeMouse(instance.resizer, x, y, {type: "mousemove"}, window);
+ EventUtils.synthesizeMouse(instance.resizer, x, y, {type: "mouseup"}, window);
+
+ let expectedWidth = initialWidth + 20;
+ let expectedHeight = initialHeight + 10;
+ info("initial width: " + initialWidth);
+ info("initial height: " + initialHeight);
+ is(content.innerWidth, expectedWidth, "Size correcty updated (width).");
+ is(content.innerHeight, expectedHeight, "Size correcty updated (height).");
+ is(instance.menulist.selectedIndex, 0, "Custom menuitem selected");
+ let [width, height] = extractSizeFromString(instance.menulist.firstChild.firstChild.getAttribute("label"));
+ is(width, expectedWidth, "Label updated (width).");
+ is(height, expectedHeight, "Label updated (height).");
+ rotate();
+ }
+
+ function rotate() {
+ let initialWidth = content.innerWidth;
+ let initialHeight = content.innerHeight;
+
+ info("rotate");
+ instance.rotate();
+
+ is(content.innerWidth, initialHeight, "The width is now the height.");
+ is(content.innerHeight, initialWidth, "The height is now the width.");
+ let [width, height] = extractSizeFromString(instance.menulist.firstChild.firstChild.getAttribute("label"));
+ is(width, initialHeight, "Label updated (width).");
+ is(height, initialWidth, "Label updated (height).");
+
+ widthBeforeClose = content.innerWidth;
+ heightBeforeClose = content.innerHeight;
+
+ info("XXX BUG 851296: instance.closing: " + !!instance.closing);
+
+ mgr.once("off", function() {
+ info("XXX BUG 851296: 'off' received.");
+ executeSoon(restart);
+ });
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ }
+
+ function restart() {
+ info("XXX BUG 851296: restarting.");
+ info("XXX BUG 851296: __responsiveUI: " + gBrowser.selectedTab.__responsiveUI);
+ mgr.once("on", function() {
+ info("XXX BUG 851296: 'on' received.");
+ executeSoon(onUIOpen2);
+ });
+ //XXX BUG 851296: synthesizeKeyFromKeyTag("key_responsiveUI");
+ mgr.toggle(window, gBrowser.selectedTab);
+ info("XXX BUG 851296: restart() finished.");
+ }
+
+ function onUIOpen2() {
+ info("XXX BUG 851296: onUIOpen2.");
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+
+ // Menus are correctly updated?
+ is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "true", "menus checked");
+
+ is(content.innerWidth, widthBeforeClose, "width restored.");
+ is(content.innerHeight, heightBeforeClose, "height restored.");
+
+ mgr.once("off", function() {executeSoon(finishUp)});
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ }
+
+ function finishUp() {
+
+ // Menus are correctly updated?
+ is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "false", "menu unchecked");
+
+ delete instance;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+
+ function synthesizeKeyFromKeyTag(aKeyId) {
+ let key = document.getElementById(aKeyId);
+ isnot(key, null, "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("ctrl"),
+ altKey: modifiersAttr.match("alt"),
+ metaKey: modifiersAttr.match("meta"),
+ accelKey: modifiersAttr.match("accel")
+ }
+
+ info("XXX BUG 851296: key name: " + name);
+ info("XXX BUG 851296: key modifiers: " + JSON.stringify(modifiers));
+ EventUtils.synthesizeKey(name, modifiers);
+ }
+}
diff --git a/browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js b/browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js
new file mode 100644
index 000000000..cba6f4fe7
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let instance, deletedPresetA, deletedPresetB, oldPrompt;
+ let mgr = ResponsiveUI.ResponsiveUIManager;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,test custom presets in responsive mode";
+
+ // This test uses executeSoon() when responsive mode is initialized and when
+ // it is destroyed such that we get out of the init/destroy loops. If we try
+ // to init/destroy immediately, without waiting for the next loop, we get
+ // intermittent test failures.
+
+ function startTest() {
+ // Mocking prompt
+ oldPrompt = Services.prompt;
+ Services.prompt = {
+ value: "",
+ returnBool: true,
+ prompt: function(aParent, aDialogTitle, aText, aValue, aCheckMsg, aCheckState) {
+ aValue.value = this.value;
+ return this.returnBool;
+ }
+ };
+
+ registerCleanupFunction(() => Services.prompt = oldPrompt);
+
+ info("test started, waiting for responsive mode to activate");
+
+ document.getElementById("Tools:ResponsiveUI").removeAttribute("disabled");
+ mgr.once("on", onUIOpen);
+ synthesizeKeyFromKeyTag("key_responsiveUI");
+ }
+
+ function onUIOpen() {
+ // Is it open?
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+
+ instance = gBrowser.selectedTab.__responsiveUI;
+ ok(instance, "instance of the module is attached to the tab.");
+
+ instance.transitionsEnabled = false;
+
+ testAddCustomPreset();
+ }
+
+ function testAddCustomPreset() {
+ // Tries to add a custom preset and cancel the prompt
+ let idx = instance.menulist.selectedIndex;
+ let presetCount = instance.presets.length;
+
+ Services.prompt.value = "";
+ Services.prompt.returnBool = false;
+ instance.addbutton.doCommand();
+
+ is(idx, instance.menulist.selectedIndex, "selected item didn't change after add preset and cancel");
+ is(presetCount, instance.presets.length, "number of presets didn't change after add preset and cancel");
+
+ let customHeight = 123, customWidth = 456;
+ instance.setSize(customWidth, customHeight);
+
+ // Adds the custom preset with "Testing preset"
+ Services.prompt.value = "Testing preset";
+ Services.prompt.returnBool = true;
+ instance.addbutton.doCommand();
+
+ instance.menulist.selectedIndex = 1;
+
+ info("waiting for responsive mode to turn off");
+ mgr.once("off", restart);
+
+ // Force document reflow to avoid intermittent failures.
+ info("document height " + document.height);
+
+ // We're still in the loop of initializing the responsive mode.
+ // Let's wait next loop to stop it.
+ executeSoon(function() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+ }
+
+ function restart() {
+ info("Restarting Responsive Mode");
+ mgr.once("on", function() {
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+
+ instance = gBrowser.selectedTab.__responsiveUI;
+
+ testCustomPresetInList();
+ });
+
+ // We're still in the loop of destroying the responsive mode.
+ // Let's wait next loop to start it.
+ executeSoon(function() {
+ synthesizeKeyFromKeyTag("key_responsiveUI");
+ });
+ }
+
+ function testCustomPresetInList() {
+ let customPresetIndex = getPresetIndex("456x123 (Testing preset)");
+ ok(customPresetIndex >= 0, "is the previously added preset (idx = " + customPresetIndex + ") in the list of items");
+
+ instance.menulist.selectedIndex = customPresetIndex;
+
+ is(content.innerWidth, 456, "add preset, and selected in the list, dimension valid (width)");
+ is(content.innerHeight, 123, "add preset, and selected in the list, dimension valid (height)");
+
+ testDeleteCustomPresets();
+ }
+
+ function testDeleteCustomPresets() {
+ instance.removebutton.doCommand();
+
+ instance.menulist.selectedIndex = 2;
+ deletedPresetA = instance.menulist.selectedItem.getAttribute("label");
+ instance.removebutton.doCommand();
+
+ instance.menulist.selectedIndex = 2;
+ deletedPresetB = instance.menulist.selectedItem.getAttribute("label");
+ instance.removebutton.doCommand();
+
+ info("waiting for responsive mode to turn off");
+ mgr.once("off", restartAgain);
+
+ // We're still in the loop of initializing the responsive mode.
+ // Let's wait next loop to stop it.
+ executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+ }
+
+ function restartAgain() {
+ info("waiting for responsive mode to turn on");
+ mgr.once("on", () => {
+ instance = gBrowser.selectedTab.__responsiveUI;
+ testCustomPresetsNotInListAnymore();
+ });
+
+ // We're still in the loop of destroying the responsive mode.
+ // Let's wait next loop to start it.
+ executeSoon(() => synthesizeKeyFromKeyTag("key_responsiveUI"));
+ }
+
+ function testCustomPresetsNotInListAnymore() {
+ let customPresetIndex = getPresetIndex(deletedPresetA);
+ is(customPresetIndex, -1, "deleted preset " + deletedPresetA + " is not in the list anymore");
+
+ customPresetIndex = getPresetIndex(deletedPresetB);
+ is(customPresetIndex, -1, "deleted preset " + deletedPresetB + " is not in the list anymore");
+
+ executeSoon(finishUp);
+ }
+
+ function finishUp() {
+ delete instance;
+ gBrowser.removeCurrentTab();
+
+ finish();
+ }
+
+ function getPresetIndex(presetLabel) {
+ function testOnePreset(c) {
+ if (c == 0) {
+ return -1;
+ }
+ instance.menulist.selectedIndex = c;
+
+ let item = instance.menulist.firstChild.childNodes[c];
+ if (item.getAttribute("label") === presetLabel) {
+ return c;
+ } else {
+ return testOnePreset(c - 1);
+ }
+ }
+ return testOnePreset(instance.menulist.firstChild.childNodes.length - 4);
+ }
+
+ function synthesizeKeyFromKeyTag(aKeyId) {
+ let key = document.getElementById(aKeyId);
+ isnot(key, null, "Successfully retrieved the <key> node");
+
+ 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");
+
+ key.doCommand();
+ }
+}
diff --git a/browser/devtools/responsivedesign/test/head.js b/browser/devtools/responsivedesign/test/head.js
new file mode 100644
index 000000000..c357f1a8a
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/head.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let {devtools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+
+function openInspector(callback)
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ callback(toolbox.getCurrentPanel());
+ });
+}
+
diff --git a/browser/devtools/responsivedesign/test/moz.build b/browser/devtools/responsivedesign/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/scratchpad/CmdScratchpad.jsm b/browser/devtools/scratchpad/CmdScratchpad.jsm
new file mode 100644
index 000000000..74e20d7e0
--- /dev/null
+++ b/browser/devtools/scratchpad/CmdScratchpad.jsm
@@ -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/. */
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
+
+/**
+ * 'scratchpad' command
+ */
+gcli.addCommand({
+ name: "scratchpad",
+ buttonId: "command-button-scratchpad",
+ buttonClass: "command-button",
+ tooltipText: gcli.lookup("scratchpadOpenTooltip"),
+ hidden: true,
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ chromeWindow.Scratchpad.ScratchpadManager.openScratchpad();
+ }
+});
diff --git a/browser/devtools/scratchpad/Makefile.in b/browser/devtools/scratchpad/Makefile.in
new file mode 100644
index 000000000..8890154f7
--- /dev/null
+++ b/browser/devtools/scratchpad/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/scratchpad/moz.build b/browser/devtools/scratchpad/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/scratchpad/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/scratchpad/scratchpad-manager.jsm b/browser/devtools/scratchpad/scratchpad-manager.jsm
new file mode 100644
index 000000000..c49e642dd
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad-manager.jsm
@@ -0,0 +1,166 @@
+/* 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://browser/content/devtools/scratchpad.xul";
+const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * 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: [],
+
+ /**
+ * 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, JSON.stringify(this._nextUid++));
+
+ 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);
+
+ // Only add the shutdown observer if we've opened a scratchpad window.
+ ShutdownObserver.init();
+
+ return win;
+ }
+};
+
+
+/**
+ * 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");
+ }
+}; \ No newline at end of file
diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js
new file mode 100644
index 000000000..08a0bc2a5
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -0,0 +1,1650 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
+Cu.import("resource://gre/modules/jsdebugger.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource:///modules/devtools/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+let Telemetry = devtools.require("devtools/shared/telemetry");
+
+const SCRATCHPAD_CONTEXT_CONTENT = 1;
+const SCRATCHPAD_CONTEXT_BROWSER = 2;
+const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
+const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
+const BUTTON_POSITION_SAVE = 0;
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+const BUTTON_POSITION_REVERT = 0;
+const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
+
+// Because we have no constructor / destructor where we can log metrics we need
+// to do so here.
+let telemetry = new Telemetry();
+telemetry.toolOpened("scratchpad");
+
+/**
+ * The scratchpad object handles the Scratchpad window functionality.
+ */
+var Scratchpad = {
+ _instanceId: null,
+ _initialWindowTitle: document.title,
+
+ /**
+ * 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;
+ },
+
+ /**
+ * 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,
+
+ /**
+ * Retrieve the xul:notificationbox DOM element. It notifies the user when
+ * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
+ */
+ get notificationBox() document.getElementById("scratchpad-notificationbox"),
+
+ /**
+ * Get the selected text from the editor.
+ *
+ * @return string
+ * The selected text.
+ */
+ get selectedText() this.editor.getSelectedText(),
+
+ /**
+ * 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)
+ {
+ return this.editor.getText(aStart, aEnd);
+ },
+
+ /**
+ * Replace text in the source editor with the given text, in the given range.
+ *
+ * @param string aText
+ * The text you want to put into the editor.
+ * @param number [aStart=0]
+ * Optional, the start offset, zero based, from where you want to start
+ * replacing text in the editor.
+ * @param number [aEnd=char count]
+ * Optional, the end offset, zero based, where you want to stop
+ * replacing text in the editor.
+ */
+ setText: function SP_setText(aText, aStart, aEnd)
+ {
+ this.editor.setText(aText, aStart, aEnd);
+ },
+
+ /**
+ * 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.editor && this.editor.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.editor.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);
+ }
+ if (this.editor) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ this.setBrowserContext();
+ }
+ else {
+ this.setContentContext();
+ }
+ },
+
+ /**
+ * Get the most recent chrome window of type navigator:browser.
+ */
+ get browserWindow() Services.wm.getMostRecentWindow("navigator:browser"),
+
+ /**
+ * Reference to the last chrome window of type navigator:browser. We use this
+ * to check if the chrome window changed since the last code evaluation.
+ */
+ _previousWindow: null,
+
+ /**
+ * Get the gBrowser object of the most recent browser window.
+ */
+ get gBrowser()
+ {
+ let recentWin = this.browserWindow;
+ return recentWin ? recentWin.gBrowser : null;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the active tab content window object.
+ */
+ _contentSandbox: 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;
+ },
+
+ /**
+ * Get the Cu.Sandbox object for the active tab content window object. Note
+ * that the returned object is cached for later reuse. The cached object is
+ * kept only for the current location in the current tab of the current
+ * browser window and it is reset for each context switch,
+ * navigator:browser window switch, tab switch or navigation.
+ */
+ get contentSandbox()
+ {
+ if (!this.browserWindow) {
+ Cu.reportError(this.strings.
+ GetStringFromName("browserWindow.unavailable"));
+ return;
+ }
+
+ if (!this._contentSandbox ||
+ this.browserWindow != this._previousBrowserWindow ||
+ this._previousBrowser != this.gBrowser.selectedBrowser ||
+ this._previousLocation != this.gBrowser.contentWindow.location.href) {
+ let contentWindow = this.gBrowser.selectedBrowser.contentWindow;
+ this._contentSandbox = new Cu.Sandbox(contentWindow,
+ { sandboxPrototype: contentWindow, wantXrays: false,
+ sandboxName: 'scratchpad-content'});
+ this._contentSandbox.__SCRATCHPAD__ = this;
+
+ this._previousBrowserWindow = this.browserWindow;
+ this._previousBrowser = this.gBrowser.selectedBrowser;
+ this._previousLocation = contentWindow.location.href;
+ }
+
+ return this._contentSandbox;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object.
+ */
+ _chromeSandbox: null,
+
+ /**
+ * Get the Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object. Note that the returned object is cached for later
+ * reuse. The cached object is kept only for the current browser window and it
+ * is reset for each context switch or navigator:browser window switch.
+ */
+ get chromeSandbox()
+ {
+ if (!this.browserWindow) {
+ Cu.reportError(this.strings.
+ GetStringFromName("browserWindow.unavailable"));
+ return;
+ }
+
+ if (!this._chromeSandbox ||
+ this.browserWindow != this._previousBrowserWindow) {
+ this._chromeSandbox = new Cu.Sandbox(this.browserWindow,
+ { sandboxPrototype: this.browserWindow, wantXrays: false,
+ sandboxName: 'scratchpad-chrome'});
+ this._chromeSandbox.__SCRATCHPAD__ = this;
+ addDebuggerToGlobal(this._chromeSandbox);
+
+ this._previousBrowserWindow = this.browserWindow;
+ }
+
+ return this._chromeSandbox;
+ },
+
+ /**
+ * Drop the editor selection.
+ */
+ deselect: function SP_deselect()
+ {
+ this.editor.dropSelection();
+ },
+
+ /**
+ * Select a specific range in the Scratchpad editor.
+ *
+ * @param number aStart
+ * Selection range start.
+ * @param number aEnd
+ * Selection range end.
+ */
+ selectRange: function SP_selectRange(aStart, aEnd)
+ {
+ this.editor.setSelection(aStart, aEnd);
+ },
+
+ /**
+ * Get the current selection range.
+ *
+ * @return object
+ * An object with two properties, start and end, that give the
+ * selection range (zero based offsets).
+ */
+ getSelectionRange: function SP_getSelection()
+ {
+ return this.editor.getSelection();
+ },
+
+ /**
+ * 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.
+ */
+ evalForContext: function SP_evaluateForContext(aString)
+ {
+ let deferred = Promise.defer();
+
+ // This setTimeout is temporary and will be replaced by DebuggerClient
+ // execution in a future patch (bug 825039). The purpose for using
+ // setTimeout is to ensure there is no accidental dependency on the
+ // promise being resolved synchronously, which can cause subtle bugs.
+ setTimeout(() => {
+ let chrome = this.executionContext != SCRATCHPAD_CONTEXT_CONTENT;
+ let sandbox = chrome ? this.chromeSandbox : this.contentSandbox;
+ let name = this.uniqueName;
+
+ try {
+ let result = Cu.evalInSandbox(aString, sandbox, "1.8", name, 1);
+ deferred.resolve([aString, undefined, result]);
+ }
+ catch (ex) {
+ deferred.resolve([aString, ex]);
+ }
+ }, 0);
+
+ 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()
+ {
+ let selection = this.selectedText || this.getText();
+ return this.evalForContext(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 promise = this.execute();
+ promise.then(([, aError, ]) => {
+ if (aError) {
+ this.writeAsErrorComment(aError);
+ }
+ else {
+ this.deselect();
+ }
+ });
+ return promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. If the result is primitive then it is written as a
+ * comment. Otherwise, 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);
+ resolve();
+ }
+ else if (!isObject(aResult)) {
+ this.writeAsComment(aResult);
+ resolve();
+ }
+ else {
+ this.deselect();
+ 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) {
+ Cu.reportError(this.strings.
+ GetStringFromName("scratchpadContext.invalid"));
+ return;
+ }
+
+ let browser = this.gBrowser.selectedBrowser;
+
+ this._reloadAndRunEvent = evt => {
+ if (evt.target !== browser.contentDocument) {
+ return;
+ }
+
+ browser.removeEventListener("load", this._reloadAndRunEvent, true);
+
+ this.run().then(aResults => deferred.resolve(aResults));
+ };
+
+ browser.addEventListener("load", this._reloadAndRunEvent, true);
+ browser.contentWindow.location.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 promise = this.execute();
+ promise.then(([aString, aError, aResult]) => {
+ if (aError) {
+ this.writeAsErrorComment(aError);
+ }
+ else {
+ this.writeAsComment(aResult);
+ }
+ });
+ return promise;
+ },
+
+ /**
+ * 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 selection = this.getSelectionRange();
+ let insertionPoint = selection.start != selection.end ?
+ selection.end : // after selected text
+ this.editor.getCharCount(); // after text end
+
+ let newComment = "\n/*\n" + aValue + "\n*/";
+
+ this.setText(newComment, insertionPoint, insertionPoint);
+
+ // Select the new comment.
+ this.selectRange(insertionPoint, insertionPoint + newComment.length);
+ },
+
+ /**
+ * 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
+ */
+ writeAsErrorComment: function SP_writeAsErrorComment(aError)
+ {
+ let stack = "";
+ if (aError.stack) {
+ stack = aError.stack;
+ }
+ else if (aError.fileName) {
+ if (aError.lineNumber) {
+ stack = "@" + aError.fileName + ":" + aError.lineNumber;
+ }
+ else {
+ stack = "@" + aError.fileName;
+ }
+ }
+ else if (aError.lineNumber) {
+ stack = "@" + aError.lineNumber;
+ }
+
+ let newComment = "Exception: " + ( aError.message || aError) + ( stack == "" ? stack : "\n" + stack.replace(/\n$/, "") );
+
+ this.writeAsComment(newComment);
+ },
+
+ // 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 promise = OS.File.writeAtomic(aFile.path, buffer,{tmpPath: aFile.path + ".tmp"});
+ promise.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);
+ }
+ });
+
+ },
+
+ /**
+ * 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(aFile);
+ channel.contentType = "application/javascript";
+
+ NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
+ let content = null;
+
+ if (Components.isSuccessCode(aStatus)) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ content = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ content = converter.ConvertToUnicode(content);
+
+ // 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.setText(content);
+ this.editor.resetUndo();
+ }
+ else if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("openFile.failed"));
+ }
+
+ 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.setFilename(file.path);
+ this.importFromFile(file, false);
+ this.setRecentFile(file);
+ }
+ });
+ };
+
+ 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.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.setAttribute("oncommand", "Scratchpad.openFile(" + 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.editor.dirty = false;
+ 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.editor.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.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()
+ {
+ this.browserWindow.HUDConsoleUI.toggleBrowserConsole();
+ },
+
+ /**
+ * Open the Web Console.
+ */
+ openWebConsole: function SP_openWebConsole()
+ {
+ let target = devtools.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);
+ this.resetContext();
+ },
+
+ /**
+ * 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);
+ this.resetContext();
+ },
+
+ /**
+ * Reset the cached Cu.Sandbox object for the current context.
+ */
+ resetContext: function SP_resetContext()
+ {
+ this._chromeSandbox = null;
+ this._contentSandbox = null;
+ this._previousWindow = null;
+ this._previousBrowser = null;
+ this._previousLocation = 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;
+ },
+
+ /**
+ * 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",
+ [LayoutHelpers.prettyKey(document.getElementById("sp-key-run")),
+ LayoutHelpers.prettyKey(document.getElementById("sp-key-inspect")),
+ LayoutHelpers.prettyKey(document.getElementById("sp-key-display"))],
+ 3);
+
+ let args = window.arguments;
+
+ if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
+ args = args[0];
+ } else {
+ // If this Scratchpad window doesn't have any arguments, horrible
+ // things might happen so we need to report an error.
+ Cu.reportError(this.strings. GetStringFromName("scratchpad.noargs"));
+ }
+
+ this._instanceId = args.GetString(0);
+
+ let state = args.GetString(1) || null;
+ if (state) {
+ state = JSON.parse(state);
+ this.setState(state);
+ initialText = state.text;
+ }
+
+ this.editor = new SourceEditor();
+
+ let config = {
+ mode: SourceEditor.MODES.JAVASCRIPT,
+ showLineNumbers: true,
+ initialText: initialText,
+ contextMenu: "scratchpad-text-popup",
+ };
+
+ let editorPlaceholder = document.getElementById("scratchpad-editor");
+ this.editor.init(editorPlaceholder, config,
+ this._onEditorLoad.bind(this, state));
+ },
+
+ /**
+ * The load event handler for the source editor. This method does post-load
+ * editor initialization.
+ *
+ * @private
+ * @param object aState
+ * The initial Scratchpad state object.
+ */
+ _onEditorLoad: function SP__onEditorLoad(aState)
+ {
+ this.editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ this.editor.focus();
+ this.editor.setCaretOffset(this.editor.getCharCount());
+ if (aState) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ this.initialized = true;
+
+ this._triggerObservers("Ready");
+
+ this.populateRecentFilesMenu();
+ PreferenceObserver.init();
+ },
+
+ /**
+ * Insert text at the current caret location.
+ *
+ * @param string aText
+ * The text you want to insert.
+ */
+ insertTextAtCaret: function SP_insertTextAtCaret(aText)
+ {
+ let caretOffset = this.editor.getCaretOffset();
+ this.setText(aText, caretOffset, caretOffset);
+ this.editor.setCaretOffset(caretOffset + aText.length);
+ },
+
+ /**
+ * The Source Editor DirtyChanged event handler. This function updates the
+ * Scratchpad window title to show an asterisk when there are unsaved changes.
+ *
+ * @private
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @param object aEvent
+ * The DirtyChanged event object.
+ */
+ _onDirtyChanged: function SP__onDirtyChanged(aEvent)
+ {
+ Scratchpad._updateTitle();
+ if (Scratchpad.filename) {
+ if (Scratchpad.editor.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.resetContext();
+
+ // This event is created only after user uses 'reload and run' feature.
+ if (this._reloadAndRunEvent) {
+ this.gBrowser.selectedBrowser.removeEventListener("load",
+ this._reloadAndRunEvent, true);
+ }
+
+ this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ PreferenceObserver.uninit();
+
+ this.editor.destroy();
+ this.editor = 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.editor.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)
+ {
+ this.promptSave((aShouldClose, aSaved, aStatus) => {
+ let shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ telemetry.toolClosed("scratchpad");
+ window.close();
+ }
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ },
+
+ _observers: [],
+
+ /**
+ * Add an observer for Scratchpad events.
+ *
+ * The observer implements IScratchpadObserver := {
+ * onReady: Called when the Scratchpad and its SourceEditor 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);
+ }
+ }
+ },
+
+ openDocumentationPage: function SP_openDocumentationPage()
+ {
+ let url = this.strings.GetStringFromName("help.openDocumentationPage");
+ let newTab = this.gBrowser.addTab(url);
+ this.browserWindow.focus();
+ this.gBrowser.selectedTab = newTab;
+ },
+};
+
+
+/**
+ * Encapsulates management of the sidebar containing the VariablesView for
+ * object inspection.
+ */
+function ScratchpadSidebar(aScratchpad)
+{
+ let ToolSidebar = devtools.require("devtools/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) {
+ 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")
+ });
+ }
+ 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, 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();
+ }
+ },
+
+ /**
+ * Update the object currently inspected by the sidebar.
+ *
+ * @param object aObject
+ * The object to inspect in the sidebar.
+ * @return Promise
+ * A promise that resolves when the update completes.
+ */
+ _update: function SS__update(aObject)
+ {
+ let deferred = Promise.defer();
+
+ this.variablesView.rawObject = aObject;
+
+ // In the future this will work on remote values (bug 825039).
+ setTimeout(() => deferred.resolve(), 0);
+ return deferred.promise;
+ }
+};
+
+
+/**
+ * Check whether a value is non-primitive.
+ */
+function isObject(aValue)
+{
+ let type = typeof aValue;
+ return type == "object" ? aValue != null : type == "function";
+}
+
+
+/**
+ * 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;
+ }
+};
+
+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/browser/devtools/scratchpad/scratchpad.xul b/browser/devtools/scratchpad/scratchpad.xul
new file mode 100644
index 000000000..af66b0832
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -0,0 +1,301 @@
+<?xml version="1.0"?>
+#ifdef 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
+<!DOCTYPE window [
+<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
+ %scratchpadDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/scratchpad.css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/devtools/source-editor-overlay.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" src="chrome://global/content/globalOverlay.js"/>
+<script type="application/javascript" src="chrome://browser/content/devtools/scratchpad.js"/>
+
+<commandset id="editMenuCommands"/>
+<commandset id="sourceEditorCommands"/>
+
+<commandset id="sp-commandset">
+ <command id="sp-cmd-newWindow" oncommand="Scratchpad.openScratchpad();"/>
+ <command id="sp-cmd-openFile" oncommand="Scratchpad.openFile();"/>
+ <command id="sp-cmd-clearRecentFiles" oncommand="Scratchpad.clearRecentFiles();"/>
+ <command id="sp-cmd-save" oncommand="Scratchpad.saveFile();"/>
+ <command id="sp-cmd-saveas" oncommand="Scratchpad.saveFileAs();"/>
+ <command id="sp-cmd-revert" oncommand="Scratchpad.promptRevert();" disabled="true"/>
+
+ <!-- TODO: bug 650340 - implement printFile()
+ <command id="sp-cmd-printFile" oncommand="Scratchpad.printFile();" disabled="true"/>
+ -->
+
+ <command id="sp-cmd-close" oncommand="Scratchpad.close();"/>
+ <command id="sp-cmd-run" oncommand="Scratchpad.run();"/>
+ <command id="sp-cmd-inspect" oncommand="Scratchpad.inspect();"/>
+ <command id="sp-cmd-display" oncommand="Scratchpad.display();"/>
+ <command id="sp-cmd-contentContext" oncommand="Scratchpad.setContentContext();"/>
+ <command id="sp-cmd-browserContext" oncommand="Scratchpad.setBrowserContext();" disabled="true"/>
+ <command id="sp-cmd-reloadAndRun" oncommand="Scratchpad.reloadAndRun();"/>
+ <command id="sp-cmd-resetContext" oncommand="Scratchpad.resetContext();"/>
+ <command id="sp-cmd-errorConsole" oncommand="Scratchpad.openErrorConsole();" disabled="true"/>
+ <command id="sp-cmd-webConsole" oncommand="Scratchpad.openWebConsole();"/>
+ <command id="sp-cmd-documentationLink" oncommand="Scratchpad.openDocumentationPage();"/>
+ <command id="sp-cmd-hideSidebar" oncommand="Scratchpad.sidebar.hide();"/>
+</commandset>
+
+<keyset id="sourceEditorKeys"/>
+
+<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"/>
+
+ <!-- TODO: bug 650340 - implement printFile
+ <key id="sp-key-printFile"
+ key="&printCmd.commandkey;"
+ command="sp-cmd-printFile"
+ 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-reloadAndRun"
+ key="&reloadAndRun.key;"
+ command="sp-cmd-reloadAndRun"
+ modifiers="accel,shift"/>
+ <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"/>
+</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/>
+
+ <!-- TODO: bug 650340 - implement printFile
+ <menuitem id="sp-menu-print"
+ label="&printCmd.label;"
+ accesskey="&printCmd.accesskey;"
+ command="sp-cmd-printFile"/>
+ <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"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <menuitem id="se-menu-undo"/>
+ <menuitem id="se-menu-redo"/>
+ <menuseparator/>
+ <menuitem id="se-menu-cut"/>
+ <menuitem id="se-menu-copy"/>
+ <menuitem id="se-menu-paste"/>
+ <menuseparator/>
+ <menuitem id="se-menu-selectAll"/>
+ <menuseparator/>
+ <menuitem id="se-menu-find"/>
+ <menuitem id="se-menu-findAgain"/>
+ <menuseparator/>
+ <menuitem id="se-menu-gotoLine"/>
+ </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-resetContext"
+ label="&resetContext2.label;"
+ accesskey="&resetContext2.accesskey;"
+ command="sp-cmd-resetContext"/>
+ </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>
+
+<popupset id="scratchpad-popups">
+ <menupopup id="scratchpad-text-popup"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <menuitem id="se-cMenu-cut"/>
+ <menuitem id="se-cMenu-copy"/>
+ <menuitem id="se-cMenu-paste"/>
+ <menuitem id="se-cMenu-delete"/>
+ <menuseparator/>
+ <menuitem id="se-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"/>
+ <menuseparator/>
+ <menuitem id="sp-text-resetContext"
+ label="&resetContext2.label;"
+ accesskey="&resetContext2.accesskey;"
+ command="sp-cmd-resetContext"/>
+ </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>
+</notificationbox>
+
+</window>
diff --git a/browser/devtools/scratchpad/test/Makefile.in b/browser/devtools/scratchpad/test/Makefile.in
new file mode 100644
index 000000000..e74af132a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_scratchpad_initialization.js \
+ browser_scratchpad_contexts.js \
+ browser_scratchpad_tab_switch.js \
+ browser_scratchpad_execute_print.js \
+ browser_scratchpad_inspect.js \
+ browser_scratchpad_files.js \
+ browser_scratchpad_ui.js \
+ browser_scratchpad_bug_646070_chrome_context_pref.js \
+ browser_scratchpad_bug_660560_tab.js \
+ browser_scratchpad_open.js \
+ browser_scratchpad_restore.js \
+ browser_scratchpad_bug_679467_falsy.js \
+ browser_scratchpad_bug_699130_edit_ui_updates.js \
+ browser_scratchpad_bug_669612_unsaved.js \
+ browser_scratchpad_bug684546_reset_undo.js \
+ browser_scratchpad_bug690552_display_outputs_errors.js \
+ browser_scratchpad_bug650345_find_ui.js \
+ browser_scratchpad_bug714942_goto_line_ui.js \
+ browser_scratchpad_bug_650760_help_key.js \
+ browser_scratchpad_bug_651942_recent_files.js \
+ browser_scratchpad_bug756681_display_non_error_exceptions.js \
+ browser_scratchpad_bug_751744_revert_to_saved.js \
+ browser_scratchpad_bug740948_reload_and_run.js \
+ browser_scratchpad_bug_661762_wrong_window_focus.js \
+ browser_scratchpad_bug_644413_modeline.js \
+ head.js \
+
+# Disable test due to bug 807234 becoming basically permanent
+# browser_scratchpad_bug_653427_confirm_close.js \
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js
new file mode 100644
index 000000000..81e45aeef
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js
@@ -0,0 +1,97 @@
+/* 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 browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the Find feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+
+ let needle = "foobar";
+ editor.setSelection(0, needle.length);
+
+ let oldPrompt = Services.prompt;
+ Services.prompt = {
+ prompt: function() { return true; },
+ };
+
+ let findKey = "F";
+ info("test Ctrl/Cmd-" + findKey + " (find)");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ let selection = editor.getSelection();
+ let newIndex = text.indexOf(needle, needle.length);
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_find");
+ aWindow.goDoCommand("cmd_find");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findNextKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findNextKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true} : {};
+
+ info("test " + findNextKey + " (findNext)");
+ EventUtils.synthesizeKey(findNextKey, findNextKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findAgain");
+ aWindow.goDoCommand("cmd_findAgain");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findPreviousKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findPreviousKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true, shiftKey: true} : {shiftKey: true};
+
+ info("test " + findPreviousKey + " (findPrevious)");
+ EventUtils.synthesizeKey(findPreviousKey, findPreviousKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findPrevious");
+ aWindow.goDoCommand("cmd_findPrevious");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ needle = "BAZbaz";
+ newIndex = text.toLowerCase().indexOf(needle.toLowerCase());
+
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = needle;
+ return true;
+ },
+ };
+
+ info("test Ctrl/Cmd-" + findKey + " (find) with a custom value");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
+
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js
new file mode 100644
index 000000000..d4ec88012
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js
@@ -0,0 +1,158 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad chrome window object.
+let gScratchpadWindow;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+let gFileA;
+let gFileB;
+
+// The temporary file content.
+let gFileAContent = "// File A ** Hello World!";
+let gFileBContent = "// File B ** Goodbye All";
+
+// Help track if one or both files are saved
+let 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, 0666);
+
+ gFileB = FileUtils.getFile("TmpD", ["fileBForBug684546.tmp"]);
+ gFileB.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ // 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,
+ 0644, 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,
+ 0644, 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.setText("new text", 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.setText("new text", 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/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js
new file mode 100644
index 000000000..637af088e
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.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/ */
+
+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 our 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 tests = [{
+ method: "display",
+ code: message,
+ result: message + openComment + "Hello World!" + closeComment,
+ label: "message display output"
+ },
+ {
+ method: "display",
+ code: error,
+ result: error + openComment + "Exception: Ouch!\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "error display output",
+ },
+ {
+ method: "run",
+ code: message,
+ result: message,
+ label: "message run output",
+ },
+ {
+ method: "run",
+ code: error,
+ result: error + openComment + "Exception: Ouch!\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "error run output",
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js
new file mode 100644
index 000000000..5cbc344db
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.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/ */
+
+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.setCaretOffset(0);
+
+ let oldPrompt = Services.prompt;
+ let desiredValue = null;
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = desiredValue;
+ return true;
+ },
+ };
+
+ desiredValue = 3;
+ EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
+ is(editor.getCaretOffset(), 34, "caret offset is correct");
+
+ desiredValue = 2;
+ aWindow.goDoCommand("cmd_gotoLine")
+ is(editor.getCaretOffset(), 17, "caret offset is correct (again)");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js
new file mode 100644
index 000000000..354ccbf75
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.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/ */
+
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+let EDITOR_TEXT = [
+ "var evt = new CustomEvent('foo', { bubbles: true });",
+ "document.body.innerHTML = 'Modified text';",
+ "window.dispatchEvent(evt);"
+].join("\n");
+
+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 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;
+
+ 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);
+ finish();
+ }, true);
+ }, true);
+
+ ok(browser.contentWindow.document.body.innerHTML !== "Modified text",
+ "Before reloading, HTML is intact.");
+ sp.reloadAndRun();
+}
+
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js
new file mode 100644
index 000000000..894af5ffd
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js
@@ -0,0 +1,107 @@
+/* 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 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: Ouch!\n@" + scratchpad.uniqueName + ":1" + 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: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@1" + 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: Ouch!\n@" + scratchpad.uniqueName + ":1" + 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: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@1" + closeComment,
+ label: "Alternative format error run output"
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+} \ No newline at end of file
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js
new file mode 100644
index 000000000..54d5e835a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js
@@ -0,0 +1,92 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+
+let gScratchpad; // Reference to the Scratchpad object.
+let gFile; // Reference to the temporary nsIFile we will work with.
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+// The temporary file content.
+let gFileContent = "function main() { return 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,<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);
+}); \ No newline at end of file
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js
new file mode 100644
index 000000000..28f6b08fe
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.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/ */
+
+let 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);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ 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/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js
new file mode 100644
index 000000000..faf5d2994
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.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/ */
+
+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);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ 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/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js
new file mode 100644
index 000000000..3ab397650
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js
@@ -0,0 +1,355 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// References to the temporary nsIFiles.
+let gFile01;
+let gFile02;
+let gFile03;
+let gFile04;
+
+// lists of recent files.
+var lists = {
+ recentFiles01: null,
+ recentFiles02: null,
+ recentFiles03: null,
+ recentFiles04: null,
+};
+
+// Temporary file names.
+let gFileName01 = "file01_ForBug651942.tmp"
+let gFileName02 = "☕" // See bug 783858 for more information
+let gFileName03 = "file03_ForBug651942.tmp"
+let gFileName04 = "file04_ForBug651942.tmp"
+
+// Content for the temporary files.
+let gFileContent;
+let gFileContent01 = "hello.world.01('bug651942');";
+let gFileContent02 = "hello.world.02('bug651942');";
+let gFileContent03 = "hello.world.03('bug651942');";
+let 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, 0666);
+
+ // 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,
+ 0644, 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/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js
new file mode 100644
index 000000000..cbcaf0ddf
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js
@@ -0,0 +1,227 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let 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, 0666);
+ 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,
+ 0644, 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(file);
+ 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/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
new file mode 100644
index 000000000..3687c8173
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.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/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ 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.setCaretOffset(0);
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), " window.foo;", "Tab key added 5 spaces");
+
+ is(sp.editor.getCaretOffset(), 5, "caret location is correct");
+
+ sp.editor.setCaretOffset(6);
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), " w indow.foo;",
+ "Tab key added 4 spaces");
+
+ is(sp.editor.getCaretOffset(), 10, "caret location is correct");
+
+ // Test the new insertTextAtCaret() method.
+
+ sp.insertTextAtCaret("omg");
+
+ is(sp.getText(), " w omgindow.foo;", "insertTextAtCaret() works");
+
+ is(sp.editor.getCaretOffset(), 13, "caret location is correct after update");
+
+ 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.setCaretOffset(0);
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), "\twindow.foo;", "Tab key added the tab character");
+
+ is(sp.editor.getCaretOffset(), 1, "caret location is correct");
+
+ Services.prefs.clearUserPref("devtools.editor.tabsize");
+ Services.prefs.clearUserPref("devtools.editor.expandtab");
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
new file mode 100644
index 000000000..94342b048
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
@@ -0,0 +1,94 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource:///modules/HUDService.jsm", tempScope);
+let HUDService = tempScope.HUDService;
+
+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;
+
+ openScratchpad(function () {
+ function onWebConsoleOpen(subj) {
+ Services.obs.removeObserver(onWebConsoleOpen,
+ "web-console-created");
+ subj.QueryInterface(Ci.nsISupportsString);
+
+ let hud = HUDService.getHudReferenceById(subj.data);
+ hud.jsterm.clearOutput(true);
+ executeSoon(testFocus.bind(null, sw, hud));
+ }
+
+ Services.obs.
+ addObserver(onWebConsoleOpen, "web-console-created", false);
+
+ HUDService.consoleUI.toggleHUD();
+ });
+ });
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test window focus for Scratchpad.";
+}
+
+function testFocus(sw, hud) {
+ let sp = sw.Scratchpad;
+
+ function onMessage(subj) {
+ Services.obs.removeObserver(onMessage, "web-console-message-created");
+
+ var loc = hud.jsterm.outputNode.querySelector(".webconsole-location");
+ ok(loc, "location element exists");
+ is(loc.value, sw.Scratchpad.uniqueName + ":1",
+ "location 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.
+ Services.obs.addObserver(onMessage, "web-console-message-created", false);
+
+ 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, undefined, "result is correct");
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
new file mode 100644
index 000000000..15d4bb615
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
@@ -0,0 +1,120 @@
+/* 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;
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+var ScratchpadManager = Scratchpad.ScratchpadManager;
+
+
+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 start if scratchpad text changes");
+
+ aScratchpad.editor.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.editor.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/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
new file mode 100644
index 000000000..08785a76b
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.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/ */
+
+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/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
new file mode 100644
index 000000000..4befa8d69
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
@@ -0,0 +1,187 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+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 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("se-menu-cut");
+ ok(cutItem, "the Cut menuitem");
+ let pasteItem = doc.getElementById("se-menu-paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ let anchor = doc.documentElement;
+ let isContextMenu = false;
+
+ 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 disabled");
+ closeMenu(firstHide);
+ };
+
+ let firstHide = function() {
+ sp.selectRange(0, 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.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
+ waitForFocus(function () {
+ let selectedText = sp.editor.getSelectedText();
+ ok(selectedText.length > 0, "non-empty selected text will be cut");
+
+ EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow);
+ }, gScratchpadWindow);
+ };
+
+ let onCut = function() {
+ sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
+ openMenu(12, 12, showAfterCut);
+ };
+
+ let showAfterCut = function() {
+ ok(cutItem.hasAttribute("disabled"), "cut menuitem is disabled after cut");
+ ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after cut");
+ closeMenu(hideAfterCut);
+ };
+
+ let hideAfterCut = function() {
+ sp.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+ waitForFocus(function () {
+ EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
+ }, gScratchpadWindow);
+ };
+
+ let onPaste = function() {
+ sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+ openMenu(13, 13, showAfterPaste);
+ };
+
+ let showAfterPaste = function() {
+ ok(cutItem.hasAttribute("disabled"), "cut menuitem is disabled after paste");
+ ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after paste");
+ closeMenu(hideAfterPaste);
+ };
+
+ let hideAfterPaste = function() {
+ if (pass == 0) {
+ pass++;
+ testContextMenu();
+ } else {
+ 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("se-cMenu-cut");
+ ok(cutItem, "the Cut menuitem");
+ pasteItem = doc.getElementById("se-cMenu-paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ sp.setText("bug 699130: hello world! (context menu)");
+ openMenu(10, 10, firstShow);
+ };
+
+ openMenu(10, 10, firstShow);
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js
new file mode 100644
index 000000000..9af0ad4f8
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js
@@ -0,0 +1,137 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFiles.
+let gFile;
+
+// Temporary file name.
+let gFileName = "testFileForBug751744.tmp"
+
+
+// Content for the temporary file.
+let gFileContent = "/* this file is already saved */\n" +
+ "function foo() { alert('bar') }";
+let gLength = gFileContent.length;
+
+// Reference to the menu entry.
+let menu;
+
+function startTest()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+ menu = gScratchpadWindow.document.getElementById("sp-menu-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("\nfoo();", gLength, gLength);
+ // 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("\nalert(foo.toSource());", gLength, gLength);
+ // 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("\nfoo();", gLength + 23, gLength + 23);
+
+ // 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 = null;
+ gScratchpad = null;
+}
+
+function createAndLoadTemporaryFile()
+{
+ // Create a temporary file.
+ gFile = FileUtils.getFile("TmpD", [gFileName]);
+ gFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ // 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,
+ 0644, 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()
+{
+ 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/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
new file mode 100644
index 000000000..6c0c684ae
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
@@ -0,0 +1,174 @@
+/* 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,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");
+
+ let dsp = sp.contentSandbox.__SCRATCHPAD__;
+
+ ok(sp.contentSandbox.__SCRATCHPAD__,
+ "there is a variable named __SCRATCHPAD__");
+
+ ok(sp.contentSandbox.__SCRATCHPAD__.editor,
+ "scratchpad is actually an instance of Scratchpad");
+
+ sp.setText("window.foobarBug636725 = 'aloha';");
+
+ ok(!content.wrappedJSObject.foobarBug636725,
+ "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(sp.chromeSandbox.__SCRATCHPAD__,
+ "there is a variable named __SCRATCHPAD__");
+
+ ok(sp.chromeSandbox.__SCRATCHPAD__.editor,
+ "scratchpad is actually an instance of Scratchpad");
+
+ ok(notificationBox.currentNotification,
+ "there is a notification in browser context");
+
+ sp.setText("2'", 31, 32);
+
+ 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.setText("gBrowser", 7);
+
+ is(sp.getText(), "window.gBrowser",
+ "setText() worked with no end for the replace range");
+ },
+ then: function([, , result]) {
+ is(typeof result.addTab, "function",
+ "chrome context has access to chrome objects");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ // Check that the sandbox is cached.
+ sp.setText("typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined", "global variable does not exist");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setText("var foobarBug636725cache = 'foo';" +
+ "typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "string",
+ "global variable exists across two different executions");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.resetContext();
+ sp.setText("typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined",
+ "global variable no longer exists after calling resetContext()");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setText("var 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.setText("typeof foobarBug636725cache2;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined",
+ "global variable no longer exists after changing the context");
+ }
+ }];
+
+ runAsyncCallbackTests(sp, tests).then(finish);
+} \ No newline at end of file
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
new file mode 100644
index 000000000..a2c43c3ca
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
@@ -0,0 +1,138 @@
+/* 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 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() {
+ content.wrappedJSObject.foobarBug636725 = 1;
+ sp.setText("++window.foobarBug636725");
+ },
+ then: function([code, , result]) {
+ is(code, sp.getText(), "code is correct");
+ is(result, content.wrappedJSObject.foobarBug636725,
+ "result is correct");
+
+ is(sp.getText(), "++window.foobarBug636725",
+ "run() does not change the editor content");
+
+ is(content.wrappedJSObject.foobarBug636725, 2,
+ "run() updated window.foobarBug636725");
+ }
+ },
+ {
+ method: "display",
+ prepare: function() {},
+ then: function() {
+ is(content.wrappedJSObject.foobarBug636725, 3,
+ "display() updated window.foobarBug636725");
+
+ is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/",
+ "display() shows evaluation result in the textbox");
+
+ is(sp.selectedText, "\n/*\n3\n*/", "selectedText is correct");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ let selection = sp.getSelectionRange();
+ is(selection.start, 24, "selection.start is correct");
+ is(selection.end, 32, "selection.end is correct");
+
+ // Test selection run() and display().
+
+ sp.setText("window.foobarBug636725 = 'a';\n" +
+ "window.foobarBug636725 = 'b';");
+
+ sp.selectRange(1, 2);
+
+ selection = sp.getSelectionRange();
+
+ is(selection.start, 1, "selection.start is 1");
+ is(selection.end, 2, "selection.end is 2");
+
+ sp.selectRange(0, 29);
+
+ selection = sp.getSelectionRange();
+
+ is(selection.start, 0, "selection.start is 0");
+ is(selection.end, 29, "selection.end is 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");
+
+ is(content.wrappedJSObject.foobarBug636725, "a",
+ "run() worked for the selected range");
+ }
+ },
+ {
+ method: "display",
+ prepare: function() {
+ sp.setText("window.foobarBug636725 = 'c';\n" +
+ "window.foobarBug636725 = 'b';");
+
+ sp.selectRange(0, 22);
+ },
+ then: function() {
+ is(content.wrappedJSObject.foobarBug636725, "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.selectedText, "\n/*\na\n*/", "selectedText is correct");
+ }
+ }]
+
+
+ runAsyncCallbackTests(sp, tests).then(function() {
+ let selection = sp.getSelectionRange();
+ is(selection.start, 22, "selection.start is correct");
+ is(selection.end, 30, "selection.end is correct");
+
+ sp.deselect();
+
+ ok(!sp.selectedText, "selectedText is empty");
+
+ selection = sp.getSelectionRange();
+ is(selection.start, selection.end, "deselect() works");
+
+ // Test undo/redo.
+
+ sp.setText("foo1");
+ sp.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/browser/devtools/scratchpad/test/browser_scratchpad_files.js b/browser/devtools/scratchpad/test/browser_scratchpad_files.js
new file mode 100644
index 000000000..e31af7946
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_files.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/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+let gFile;
+
+// The temporary file content.
+let 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");
+
+ // Save the file after changes.
+ gFileContent += "// omg, saved!";
+ gScratchpad.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.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(gFile);
+ 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/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
new file mode 100644
index 000000000..67bca826a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.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/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ 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");
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js b/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
new file mode 100644
index 000000000..b4e81a991
--- /dev/null
+++ b/browser/devtools/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 in sidebar.variablesView) {
+ for (let [, obj] in scope) {
+ for (let [, prop] in 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/browser/devtools/scratchpad/test/browser_scratchpad_open.js b/browser/devtools/scratchpad/test/browser_scratchpad_open.js
new file mode 100644
index 000000000..462d9ad2d
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.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/ */
+
+// only finish() when correct number of tests are done
+const expected = 3;
+var count = 0;
+var lastUniqueName = null;
+
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ testOpen();
+ testOpenWithState();
+ testOpenInvalidState();
+}
+
+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();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_restore.js b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
new file mode 100644
index 000000000..a83c4213c
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.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/ */
+
+var ScratchpadManager = Scratchpad.ScratchpadManager;
+
+/* 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/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js b/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
new file mode 100644
index 000000000..68314b930
--- /dev/null
+++ b/browser/devtools/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/ */
+
+let tab1;
+let tab2;
+let 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/browser/devtools/scratchpad/test/browser_scratchpad_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_ui.js
new file mode 100644
index 000000000..a41aea149
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_ui.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/ */
+
+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-resetContext": "resetContext",
+ "sp-menu-content": "setContentContext",
+ "sp-menu-browser": "setBrowserContext",
+ };
+
+ let lastMethodCalled = null;
+ sp.__noSuchMethod__ = function(aMethodName) {
+ lastMethodCalled = aMethodName;
+ };
+
+ for (let id in methodsAndItems) {
+ lastMethodCalled = null;
+
+ let methodName = methodsAndItems[id];
+ let oldMethod = sp[methodName];
+ ok(oldMethod, "found method " + methodName + " in Scratchpad object");
+
+ delete sp[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;
+ }
+
+ delete sp.__noSuchMethod__;
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/head.js b/browser/devtools/scratchpad/test/head.js
new file mode 100644
index 000000000..07dfd0239
--- /dev/null
+++ b/browser/devtools/scratchpad/test/head.js
@@ -0,0 +1,197 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope);
+
+
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+let Promise = tempScope.Promise;
+
+let gScratchpadWindow; // Reference to the Scratchpad chrome window object
+
+/**
+ * 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)
+{
+ aOptions = aOptions || {};
+
+ let win = aOptions.window ||
+ Scratchpad.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;
+}
+
+/**
+ * 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.
+ */
+function runAsyncCallbackTests(aScratchpad, aTests)
+{
+ let deferred = Promise.defer();
+
+ (function runTest() {
+ if (aTests.length) {
+ let test = aTests.shift();
+ test.prepare();
+ aScratchpad[test.method]().then(test.then.bind(test)).then(runTest);
+ } else {
+ deferred.resolve();
+ }
+ })();
+
+ return deferred.promise;
+}
+
+
+function cleanup()
+{
+ if (gScratchpadWindow) {
+ gScratchpadWindow.close();
+ gScratchpadWindow = null;
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+registerCleanupFunction(cleanup);
diff --git a/browser/devtools/scratchpad/test/moz.build b/browser/devtools/scratchpad/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/scratchpad/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/shared/AppCacheUtils.jsm b/browser/devtools/shared/AppCacheUtils.jsm
new file mode 100644
index 000000000..0c3579084
--- /dev/null
+++ b/browser/devtools/shared/AppCacheUtils.jsm
@@ -0,0 +1,630 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+
+let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+
+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 = Promise.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 = Promise.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 Iterator(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.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 {
+ 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 = Promise.defer();
+ let channelCharset = "";
+ let buffer = "";
+ let channel = Services.io.newChannel(uri, null, null);
+
+ // Avoid the cache:
+ channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ channel.asyncOpen({
+ 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
+ });
+ }
+ }
+ }, null);
+ return deferred.promise;
+ },
+
+ listEntries: function ACU_show(searchTerm) {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let entries = [];
+
+ Services.cache.visitEntries({
+ visitDevice: function(deviceID, deviceInfo) {
+ return true;
+ },
+
+ visitEntry: function(deviceID, entryInfo) {
+ if (entryInfo.deviceID == "offline") {
+ let entry = {};
+ let lowerKey = entryInfo.key.toLowerCase();
+
+ if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
+ return true;
+ }
+
+ for (let [key, value] of Iterator(entryInfo)) {
+ if (key == "QueryInterface") {
+ continue;
+ }
+ if (key == "clientID") {
+ entry.key = entryInfo.key;
+ }
+ if (key == "expirationTime" || key == "lastFetched" || key == "lastModified") {
+ value = new Date(value * 1000);
+ }
+ entry[key] = value;
+ }
+ entries.push(entry);
+ }
+ return true;
+ }
+ });
+
+ if (entries.length == 0) {
+ throw new Error(l10n.GetStringFromName("noResults"));
+ }
+ return entries;
+ },
+
+ viewEntry: function ACU_viewEntry(key) {
+ let uri;
+
+ Services.cache.visitEntries({
+ visitDevice: function(deviceID, deviceInfo) {
+ return true;
+ },
+
+ visitEntry: function(deviceID, entryInfo) {
+ if (entryInfo.deviceID == "offline" && entryInfo.key == key) {
+ uri = "about:cache-entry?client=" + entryInfo.clientID +
+ "&sb=1&key=" + entryInfo.key;
+ return false;
+ }
+ return true;
+ }
+ });
+
+ if (uri) {
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ let win = wm.getMostRecentWindow("navigator:browser");
+ win.gBrowser.selectedTab = win.gBrowser.addTab(uri);
+ } else {
+ return l10n.GetStringFromName("entryNotFound");
+ }
+ },
+
+ clearAll: function ACU_clearAll() {
+ Services.cache.evictEntries(Ci.nsICache.STORE_OFFLINE);
+ },
+
+ _getManifestURI: function ACU__getManifestURI() {
+ let deferred = Promise.defer();
+
+ let getURI = node => {
+ 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);
+ return origin + htmlNode.getAttribute("manifest");
+ }
+ };
+
+ if (this.doc) {
+ let uri = getURI(this.doc);
+ 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(this.doc);
+ 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].replace(/^\s+|\s+$/g);
+ 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", function() Services.strings
+ .createBundle("chrome://browser/locale/devtools/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/browser/devtools/shared/AutocompletePopup.jsm b/browser/devtools/shared/AutocompletePopup.jsm
new file mode 100644
index 000000000..523a13e6c
--- /dev/null
+++ b/browser/devtools/shared/AutocompletePopup.jsm
@@ -0,0 +1,500 @@
+/* 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 = Components.utils;
+
+// The XUL and XHTML namespace.
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
+
+/**
+ * Autocomplete popup UI implementation.
+ *
+ * @constructor
+ * @param nsIDOMDocument aDocument
+ * The document you want the popup attached to.
+ * @param Object aOptions
+ * An object consiting any of the following options:
+ * - panelId {String} The id for the popup panel.
+ * - listBoxId {String} The id for the richlistbox inside the panel.
+ * - position {String} The position for the popup panel.
+ * - 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.
+ * - fixedWidth {Boolean} Boolean to control dynamic width of the popup.
+ * - direction {String} The direction of the text in the panel. rtl or ltr
+ * - onSelect {String} The select event handler for the richlistbox
+ * - onClick {String} The click event handler for the richlistbox.
+ * - onKeypress {String} The keypress event handler for the richlistitems.
+ */
+this.AutocompletePopup =
+function AutocompletePopup(aDocument, aOptions = {})
+{
+ this._document = aDocument;
+
+ this.fixedWidth = aOptions.fixedWidth || false;
+ this.autoSelect = aOptions.autoSelect || false;
+ this.position = aOptions.position || "after_start";
+ this.direction = aOptions.direction || "ltr";
+
+ this.onSelect = aOptions.onSelect;
+ this.onClick = aOptions.onClick;
+ this.onKeypress = aOptions.onKeypress;
+
+ let id = aOptions.panelId || "devtools_autoCompletePopup";
+ let theme = aOptions.theme || "dark";
+ // Reuse the existing popup elements.
+ this._panel = this._document.getElementById(id);
+ if (!this._panel) {
+ this._panel = this._document.createElementNS(XUL_NS, "panel");
+ this._panel.setAttribute("id", id);
+ this._panel.className = "devtools-autocomplete-popup " + theme + "-theme";
+
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("level", "top");
+ if (!aOptions.onKeypress) {
+ this._panel.setAttribute("ignorekeys", "true");
+ }
+
+ let mainPopupSet = this._document.getElementById("mainPopupSet");
+ if (mainPopupSet) {
+ mainPopupSet.appendChild(this._panel);
+ }
+ else {
+ this._document.documentElement.appendChild(this._panel);
+ }
+ this._list = null;
+ }
+ else {
+ this._list = this._panel.firstChild;
+ }
+
+ if (!this._list) {
+ this._list = this._document.createElementNS(XUL_NS, "richlistbox");
+ this._panel.appendChild(this._list);
+
+ // Open and hide the panel, so we initialize the API of the richlistbox.
+ this._panel.openPopup(null, this.position, 0, 0);
+ this._panel.hidePopup();
+ }
+
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("seltype", "single");
+
+ if (aOptions.listBoxId) {
+ this._list.setAttribute("id", aOptions.listBoxId);
+ }
+ this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
+
+ if (this.onSelect) {
+ this._list.addEventListener("select", this.onSelect, false);
+ }
+
+ if (this.onClick) {
+ this._list.addEventListener("click", this.onClick, false);
+ }
+
+ if (this.onKeypress) {
+ this._list.addEventListener("keypress", this.onKeypress, false);
+ }
+}
+
+AutocompletePopup.prototype = {
+ _document: null,
+ _panel: null,
+ _list: null,
+
+ // Event handlers.
+ onSelect: null,
+ onClick: null,
+ onKeypress: null,
+
+ /**
+ * Open the autocomplete popup panel.
+ *
+ * @param nsIDOMNode aAnchor
+ * Optional node to anchor the panel to.
+ */
+ openPopup: function AP_openPopup(aAnchor)
+ {
+ this._panel.openPopup(aAnchor, this.position, 0, 0);
+
+ if (this.autoSelect) {
+ this.selectFirstItem();
+ }
+ if (!this.fixedWidth) {
+ this._updateSize();
+ }
+ },
+
+ /**
+ * Hide the autocomplete popup panel.
+ */
+ hidePopup: function AP_hidePopup()
+ {
+ this._panel.hidePopup();
+ },
+
+ /**
+ * Check if the autocomplete popup is open.
+ */
+ get isOpen() {
+ return this._panel.state == "open";
+ },
+
+ /**
+ * 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 AP_destroy()
+ {
+ if (this.isOpen) {
+ this.hidePopup();
+ }
+ this.clearItems();
+
+ if (this.onSelect) {
+ this._list.removeEventListener("select", this.onSelect, false);
+ }
+
+ if (this.onClick) {
+ this._list.removeEventListener("click", this.onClick, false);
+ }
+
+ if (this.onKeypress) {
+ this._list.removeEventListener("keypress", this.onKeypress, false);
+ }
+
+ this._document = null;
+ this._list = null;
+ this._panel = null;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @param Number aIndex The index of the item what is wanted.
+ *
+ * @return The autocomplete item at index aIndex.
+ */
+ getItemAtIndex: function AP_getItemAtIndex(aIndex)
+ {
+ return this._list.getItemAtIndex(aIndex)._autocompleteItem;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @return array
+ * The array of autocomplete items.
+ */
+ getItems: function AP_getItems()
+ {
+ let items = [];
+
+ Array.forEach(this._list.childNodes, function(aItem) {
+ items.push(aItem._autocompleteItem);
+ });
+
+ return items;
+ },
+
+ /**
+ * Set the autocomplete items list, in one go.
+ *
+ * @param array aItems
+ * The list of items you want displayed in the popup list.
+ */
+ setItems: function AP_setItems(aItems)
+ {
+ this.clearItems();
+ aItems.forEach(this.appendItem, this);
+
+ // Make sure that the new content is properly fitted by the XUL richlistbox.
+ if (this.isOpen) {
+ if (this.autoSelect) {
+ this.selectFirstItem();
+ }
+ if (!this.fixedWidth) {
+ this._updateSize();
+ }
+ }
+ },
+
+ /**
+ * Selects the first item of the richlistbox. Note that first item here is the
+ * item closes to the input element, which means that 0th index if position is
+ * below, and last index if position is above.
+ */
+ selectFirstItem: function AP_selectFirstItem()
+ {
+ if (this.position.contains("before")) {
+ this.selectedIndex = this.itemCount - 1;
+ }
+ else {
+ this.selectedIndex = 0;
+ }
+ },
+
+ /**
+ * Update the panel size to fit the content.
+ *
+ * @private
+ */
+ _updateSize: function AP__updateSize()
+ {
+ // We need the dispatch to allow the content to reflow. Attempting to
+ // update the richlistbox size too early does not work.
+ Services.tm.currentThread.dispatch({ run: () => {
+ if (!this._panel) {
+ return;
+ }
+ this._list.width = this._panel.clientWidth + this._scrollbarWidth;
+ // Height change is required, otherwise the panel is drawn at an offset
+ // the first time.
+ this._list.height = this._list.clientHeight;
+ // This brings the panel back at right position.
+ this._list.top = 0;
+ // Changing panel height might make the selected item out of view, so
+ // bring it back to view.
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }}, 0);
+ },
+
+ /**
+ * Clear all the items from the autocomplete list.
+ */
+ clearItems: function AP_clearItems()
+ {
+ // Reset the selectedIndex to -1 before clearing the list
+ this.selectedIndex = -1;
+
+ while (this._list.hasChildNodes()) {
+ this._list.removeChild(this._list.firstChild);
+ }
+
+ if (!this.fixedWidth) {
+ // Reset the panel and list dimensions. New dimensions are calculated when
+ // a new set of items is added to the autocomplete popup.
+ this._list.width = "";
+ this._list.height = "";
+ this._panel.width = "";
+ this._panel.height = "";
+ this._panel.top = "";
+ this._panel.left = "";
+ }
+ },
+
+ /**
+ * Getter for the index of the selected item.
+ *
+ * @type number
+ */
+ get selectedIndex() {
+ return this._list.selectedIndex;
+ },
+
+ /**
+ * Setter for the selected index.
+ *
+ * @param number aIndex
+ * The number (index) of the item you want to select in the list.
+ */
+ set selectedIndex(aIndex) {
+ this._list.selectedIndex = aIndex;
+ if (this.isOpen && this._list.ensureIndexIsVisible) {
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }
+ },
+
+ /**
+ * Getter for the selected item.
+ * @type object
+ */
+ get selectedItem() {
+ return this._list.selectedItem ?
+ this._list.selectedItem._autocompleteItem : null;
+ },
+
+ /**
+ * Setter for the selected item.
+ *
+ * @param object aItem
+ * The object you want selected in the list.
+ */
+ set selectedItem(aItem) {
+ this._list.selectedItem = this._findListItem(aItem);
+ if (this.isOpen) {
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }
+ },
+
+ /**
+ * Append an item into the autocomplete list.
+ *
+ * @param object aItem
+ * 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 AP_appendItem(aItem)
+ {
+ let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
+ if (this.direction) {
+ listItem.setAttribute("dir", this.direction);
+ }
+ let label = this._document.createElementNS(XUL_NS, "label");
+ label.setAttribute("value", aItem.label);
+ label.setAttribute("class", "autocomplete-value");
+ if (aItem.preLabel) {
+ let preDesc = this._document.createElementNS(XUL_NS, "label");
+ preDesc.setAttribute("value", aItem.preLabel);
+ preDesc.setAttribute("class", "initial-value");
+ listItem.appendChild(preDesc);
+ label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
+ }
+ listItem.appendChild(label);
+ if (aItem.count && aItem.count > 1) {
+ let countDesc = this._document.createElementNS(XUL_NS, "label");
+ countDesc.setAttribute("value", aItem.count);
+ countDesc.setAttribute("flex", "1");
+ countDesc.setAttribute("class", "autocomplete-count");
+ listItem.appendChild(countDesc);
+ }
+ listItem._autocompleteItem = aItem;
+
+ this._list.appendChild(listItem);
+ },
+
+ /**
+ * Find the richlistitem element that belongs to an item.
+ *
+ * @private
+ *
+ * @param object aItem
+ * The object you want found in the list.
+ *
+ * @return nsIDOMNode|null
+ * The nsIDOMNode that belongs to the given item object. This node is
+ * the richlistitem element.
+ */
+ _findListItem: function AP__findListItem(aItem)
+ {
+ for (let i = 0; i < this._list.childNodes.length; i++) {
+ let child = this._list.childNodes[i];
+ if (child._autocompleteItem == aItem) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Remove an item from the popup list.
+ *
+ * @param object aItem
+ * The item you want removed.
+ */
+ removeItem: function AP_removeItem(aItem)
+ {
+ let item = this._findListItem(aItem);
+ if (!item) {
+ throw new Error("Item not found!");
+ }
+ this._list.removeChild(item);
+ },
+
+ /**
+ * Getter for the number of items in the popup.
+ * @type number
+ */
+ get itemCount() {
+ return this._list.childNodes.length;
+ },
+
+ /**
+ * Select the next item in the list.
+ *
+ * @return object
+ * The newly selected item object.
+ */
+ selectNextItem: function AP_selectNextItem()
+ {
+ if (this.selectedIndex < (this.itemCount - 1)) {
+ this.selectedIndex++;
+ }
+ else {
+ this.selectedIndex = -1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the previous item in the list.
+ *
+ * @return object
+ * The newly selected item object.
+ */
+ selectPreviousItem: function AP_selectPreviousItem()
+ {
+ if (this.selectedIndex > -1) {
+ this.selectedIndex--;
+ }
+ else {
+ this.selectedIndex = this.itemCount - 1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Focuses the richlistbox.
+ */
+ focus: function AP_focus()
+ {
+ this._list.focus();
+ },
+
+ /**
+ * Determine the scrollbar width in the current document.
+ *
+ * @private
+ */
+ get _scrollbarWidth()
+ {
+ if (this.__scrollbarWidth) {
+ return this.__scrollbarWidth;
+ }
+
+ let hbox = this._document.createElementNS(XUL_NS, "hbox");
+ hbox.setAttribute("style", "height: 0%; overflow: hidden");
+
+ let scrollbar = this._document.createElementNS(XUL_NS, "scrollbar");
+ scrollbar.setAttribute("orient", "vertical");
+ hbox.appendChild(scrollbar);
+
+ this._document.documentElement.appendChild(hbox);
+ this.__scrollbarWidth = scrollbar.clientWidth;
+ this._document.documentElement.removeChild(hbox);
+
+ return this.__scrollbarWidth;
+ },
+};
+
diff --git a/browser/devtools/shared/DOMHelpers.jsm b/browser/devtools/shared/DOMHelpers.jsm
new file mode 100644
index 000000000..754632ff9
--- /dev/null
+++ b/browser/devtools/shared/DOMHelpers.jsm
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.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) {
+ 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 = Components.interfaces.nsIDOMNodeFilter.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;
+ }
+};
diff --git a/browser/devtools/shared/DeveloperToolbar.jsm b/browser/devtools/shared/DeveloperToolbar.jsm
new file mode 100644
index 000000000..f5b19139c
--- /dev/null
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -0,0 +1,1248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If 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 = [ "DeveloperToolbar", "CommandUtils" ];
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/Commands.jsm");
+
+const Node = Ci.nsIDOMNode;
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "gcli",
+ "resource://gre/modules/devtools/gcli.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands",
+ "resource:///modules/devtools/BuiltinCommands.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleServiceListener",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "require",
+ "resource://gre/modules/devtools/Require.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
+ let prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+ return prefService.getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+});
+
+XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () {
+ return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
+});
+
+let Telemetry = devtools.require("devtools/shared/telemetry");
+
+const converters = require("gcli/converters");
+
+/**
+ * A collection of utilities to help working with commands
+ */
+let CommandUtils = {
+ /**
+ * Read a toolbarSpec from preferences
+ * @param aPref The name of the preference to read
+ */
+ getCommandbarSpec: function CU_getCommandbarSpec(aPref) {
+ let value = prefBranch.getComplexValue(aPref, Ci.nsISupportsString).data;
+ return JSON.parse(value);
+ },
+
+ /**
+ * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of
+ * strings each of which is a GCLI command (including args if needed).
+ *
+ * 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 CU_createButtons(toolbarSpec, target, document, requisition) {
+ let reply = [];
+
+ toolbarSpec.forEach(function(buttonSpec) {
+ let button = document.createElement("toolbarbutton");
+ reply.push(button);
+
+ if (typeof buttonSpec == "string") {
+ buttonSpec = { typed: buttonSpec };
+ }
+ // Ask GCLI to parse the typed string (doesn't execute it)
+ requisition.update(buttonSpec.typed);
+
+ // Ignore invalid commands
+ let command = requisition.commandAssignment.value;
+ if (command == null) {
+ // TODO: Have a broken icon
+ // button.icon = 'Broken';
+ button.setAttribute("label", "X");
+ button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed);
+ button.setAttribute("disabled", "true");
+ }
+ else {
+ if (command.buttonId != null) {
+ button.id = command.buttonId;
+ }
+ if (command.buttonClass != null) {
+ button.className = command.buttonClass;
+ }
+ if (command.tooltipText != null) {
+ button.setAttribute("tooltiptext", command.tooltipText);
+ }
+ else if (command.description != null) {
+ button.setAttribute("tooltiptext", command.description);
+ }
+
+ button.addEventListener("click", function() {
+ requisition.update(buttonSpec.typed);
+ //if (requisition.getStatus() == Status.VALID) {
+ requisition.exec();
+ /*
+ }
+ else {
+ console.error('incomplete commands not yet supported');
+ }
+ */
+ }, false);
+
+ // Allow the command button to be toggleable
+ if (command.state) {
+ button.setAttribute("autocheck", false);
+ let onChange = function(event, eventTab) {
+ if (eventTab == target.tab) {
+ if (command.state.isChecked(target)) {
+ button.setAttribute("checked", true);
+ }
+ else if (button.hasAttribute("checked")) {
+ button.removeAttribute("checked");
+ }
+ }
+ };
+ command.state.onChange(target, onChange);
+ onChange(null, target.tab);
+ document.defaultView.addEventListener("unload", function() {
+ command.state.offChange(target, onChange);
+ }, false);
+ }
+ }
+ });
+
+ requisition.update('');
+
+ return reply;
+ },
+
+ /**
+ * A helper function to create the environment object that is passed to
+ * GCLI commands.
+ */
+ createEnvironment: function(chromeDocument, contentDocument) {
+ let environment = {
+ chromeDocument: chromeDocument,
+ chromeWindow: chromeDocument.defaultView,
+
+ document: contentDocument,
+ window: contentDocument != null ? contentDocument.defaultView : undefined
+ };
+
+ Object.defineProperty(environment, "target", {
+ get: function() {
+ let tab = chromeDocument.defaultView.getBrowser().selectedTab;
+ return devtools.TargetFactory.forTab(tab);
+ },
+ enumerable: true
+ });
+
+ return environment;
+ },
+};
+
+this.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.
+ */
+XPCOMUtils.defineLazyGetter(this, "isLinux", function () {
+ return OS == "Linux";
+});
+
+XPCOMUtils.defineLazyGetter(this, "OS", function () {
+ let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ return os;
+});
+
+/**
+ * A component to manage the global developer toolbar, which contains a GCLI
+ * and buttons for various developer tools.
+ * @param aChromeWindow The browser window to which this toolbar is attached
+ * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar">
+ */
+this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement)
+{
+ this._chromeWindow = aChromeWindow;
+
+ this._element = aToolbarElement;
+ this._element.hidden = true;
+ this._doc = this._element.ownerDocument;
+
+ this._telemetry = new Telemetry();
+ this._lastState = NOTIFICATIONS.HIDE;
+ this._pendingShowCallback = undefined;
+ this._pendingHide = false;
+ this._errorsCount = {};
+ this._warningsCount = {};
+ this._errorListeners = {};
+ this._errorCounterButton = this._doc
+ .getElementById("developer-toolbar-toolbox-button");
+ this._errorCounterButton._defaultTooltipText =
+ this._errorCounterButton.getAttribute("tooltiptext");
+
+ EventEmitter.decorate(this);
+
+ try {
+ CmdCommands.refreshAutoCommands(aChromeWindow);
+ }
+ catch (ex) {
+ console.error(ex);
+ }
+}
+
+/**
+ * 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 DT_visible() {
+ return !this._element.hidden;
+ },
+ enumerable: true
+});
+
+let _gSequenceId = 0;
+
+/**
+ * Getter for a unique ID.
+ */
+Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', {
+ get: function DT_visible() {
+ return _gSequenceId++;
+ },
+ enumerable: true
+});
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.toggle = function DT_toggle()
+{
+ if (this.visible) {
+ this.hide();
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focus = function DT_focus()
+{
+ if (this.visible) {
+ this._input.focus();
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focusToggle = function DT_focusToggle()
+{
+ 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 & Node.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
+ * @param aCallback show events can be asynchronous. If supplied aCallback will
+ * be called when the DeveloperToolbar is visible
+ */
+DeveloperToolbar.prototype.show = function DT_show(aFocus, aCallback)
+{
+ if (this._lastState != NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", true);
+
+ this._telemetry.toolOpened("developertoolbar");
+
+ this._notify(NOTIFICATIONS.LOAD);
+ this._pendingShowCallback = aCallback;
+ this._pendingHide = false;
+
+ let checkLoad = function() {
+ if (this.tooltipPanel && this.tooltipPanel.loaded &&
+ this.outputPanel && this.outputPanel.loaded) {
+ this._onload(aFocus);
+ }
+ }.bind(this);
+
+ this._input = this._doc.querySelector(".gclitoolbar-input-node");
+ this.tooltipPanel = new TooltipPanel(this._doc, this._input, checkLoad);
+ this.outputPanel = new OutputPanel(this, checkLoad);
+};
+
+/**
+ * Initializing GCLI can only be done when we've got content windows to write
+ * to, so this needs to be done asynchronously.
+ */
+DeveloperToolbar.prototype._onload = function DT_onload(aFocus)
+{
+ this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
+
+ let contentDocument = this._chromeWindow.getBrowser().contentDocument;
+
+ this.display = gcli.createDisplay({
+ contentDocument: contentDocument,
+ chromeDocument: this._doc,
+ chromeWindow: this._chromeWindow,
+ hintElement: this.tooltipPanel.hintElement,
+ inputElement: this._input,
+ completeElement: this._doc.querySelector(".gclitoolbar-complete-node"),
+ backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
+ outputDocument: this.outputPanel.document,
+ environment: CommandUtils.createEnvironment(this._doc, contentDocument),
+ tooltipClass: 'gcliterm-tooltip',
+ eval: null,
+ scratchpad: null
+ });
+
+ this.display.focusManager.addMonitoredElement(this.outputPanel._frame);
+ this.display.focusManager.addMonitoredElement(this._element);
+
+ this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
+
+ let tabbrowser = this._chromeWindow.getBrowser();
+ tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.addEventListener("TabClose", this, false);
+ tabbrowser.addEventListener("load", this, true);
+ tabbrowser.addEventListener("beforeunload", this, true);
+
+ this._initErrorsCount(tabbrowser.selectedTab);
+
+ this._element.hidden = false;
+
+ if (aFocus) {
+ this._input.focus();
+ }
+
+ this._notify(NOTIFICATIONS.SHOW);
+ if (this._pendingShowCallback) {
+ this._pendingShowCallback.call();
+ this._pendingShowCallback = undefined;
+ }
+
+ // If a hide event happened while we were loading, then we need to hide.
+ // We could make this check earlier, but then cleanup would be complex so
+ // we're being inefficient for now.
+ if (this._pendingHide) {
+ this.hide();
+ return;
+ }
+
+ if (!DeveloperToolbar.introShownThisSession) {
+ this.display.maybeShowIntro();
+ DeveloperToolbar.introShownThisSession = true;
+ }
+};
+
+/**
+ * Initialize the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode aTab the xul:tab for which you want to track the number of
+ * errors.
+ */
+DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab)
+{
+ let tabId = aTab.linkedPanel;
+ if (tabId in this._errorsCount) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ let window = aTab.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 aTab the xul:tab for which you want to stop tracking the
+ * number of errors.
+ */
+DeveloperToolbar.prototype._stopErrorsCount = function DT__stopErrorsCount(aTab)
+{
+ let tabId = aTab.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.hide = function DT_hide()
+{
+ if (this._lastState == NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ if (this._lastState == NOTIFICATIONS.LOAD) {
+ this._pendingHide = true;
+ return;
+ }
+
+ this._element.hidden = true;
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", false);
+
+ this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false");
+ this.destroy();
+
+ this._telemetry.toolClosed("developertoolbar");
+ this._notify(NOTIFICATIONS.HIDE);
+};
+
+/**
+ * Hide the developer toolbar
+ */
+DeveloperToolbar.prototype.destroy = function DT_destroy()
+{
+ if (this._lastState == NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ let tabbrowser = this._chromeWindow.getBrowser();
+ tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
+ tabbrowser.removeEventListener("load", this, true);
+ tabbrowser.removeEventListener("beforeunload", this, true);
+
+ Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+
+ this.display.focusManager.removeMonitoredElement(this.outputPanel._frame);
+ this.display.focusManager.removeMonitoredElement(this._element);
+
+ this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
+ this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
+ this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
+ this.display.destroy();
+ this.outputPanel.destroy();
+ this.tooltipPanel.destroy();
+ delete this._input;
+
+ // We could "delete this.display" etc if we have hard-to-track-down memory
+ // leaks as a belt-and-braces approach, however this prevents our DOM node
+ // hunter from looking in all the nooks and crannies, so it's better if we
+ // can be leak-free without
+ /*
+ delete this.display;
+ delete this.outputPanel;
+ delete this.tooltipPanel;
+ */
+
+ this._lastState = NOTIFICATIONS.HIDE;
+};
+
+/**
+ * Utility for sending notifications
+ * @param aTopic a NOTIFICATION constant
+ */
+DeveloperToolbar.prototype._notify = function DT_notify(aTopic)
+{
+ this._lastState = aTopic;
+
+ let data = { toolbar: this };
+ data.wrappedJSObject = data;
+ Services.obs.notifyObservers(data, aTopic, null);
+};
+
+/**
+ * Update various parts of the UI when the current tab changes
+ * @param aEvent
+ */
+DeveloperToolbar.prototype.handleEvent = function DT_handleEvent(aEvent)
+{
+ if (aEvent.type == "TabSelect" || aEvent.type == "load") {
+ if (this.visible) {
+ let contentDocument = this._chromeWindow.getBrowser().contentDocument;
+
+ this.display.reattach({
+ contentDocument: contentDocument,
+ chromeWindow: this._chromeWindow,
+ environment: CommandUtils.createEnvironment(this._doc, contentDocument),
+ });
+
+ if (aEvent.type == "TabSelect") {
+ this._initErrorsCount(aEvent.target);
+ }
+ }
+ }
+ else if (aEvent.type == "TabClose") {
+ this._stopErrorsCount(aEvent.target);
+ }
+ else if (aEvent.type == "beforeunload") {
+ this._onPageBeforeUnload(aEvent);
+ }
+};
+
+/**
+ * Count a page error received for the currently selected tab. This
+ * method counts the JavaScript exceptions received and CSS errors/warnings.
+ *
+ * @private
+ * @param string aTabId the ID of the tab from where the page error comes.
+ * @param object aPageError the page error object received from the
+ * PageErrorListener.
+ */
+DeveloperToolbar.prototype._onPageError =
+function DT__onPageError(aTabId, aPageError)
+{
+ if (aPageError.category == "CSS Parser" ||
+ aPageError.category == "CSS Loader") {
+ return;
+ }
+ if ((aPageError.flags & aPageError.warningFlag) ||
+ (aPageError.flags & aPageError.strictFlag)) {
+ this._warningsCount[aTabId]++;
+ } else {
+ this._errorsCount[aTabId]++;
+ }
+ this._updateErrorsCount(aTabId);
+};
+
+/**
+ * The |beforeunload| event handler. This function resets the errors count when
+ * a different page starts loading.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent the beforeunload DOM event.
+ */
+DeveloperToolbar.prototype._onPageBeforeUnload =
+function DT__onPageBeforeUnload(aEvent)
+{
+ let window = aEvent.target.defaultView;
+ if (window.top !== window) {
+ return;
+ }
+
+ let tabs = this._chromeWindow.getBrowser().tabs;
+ Array.prototype.some.call(tabs, function(aTab) {
+ if (aTab.linkedBrowser.contentWindow === window) {
+ let tabId = aTab.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 [aChangedTabId] 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 DT__updateErrorsCount(aChangedTabId)
+{
+ let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
+ if (aChangedTabId && tabId != aChangedTabId) {
+ return;
+ }
+
+ let errors = this._errorsCount[tabId];
+ let warnings = this._warningsCount[tabId];
+ let btn = this._errorCounterButton;
+ if (errors) {
+ let errorsText = toolboxStrings
+ .GetStringFromName("toolboxToggleButton.errors");
+ errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
+
+ let warningsText = toolboxStrings
+ .GetStringFromName("toolboxToggleButton.warnings");
+ warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
+
+ let tooltiptext = toolboxStrings
+ .formatStringFromName("toolboxToggleButton.tooltip",
+ [errorsText, warningsText], 2);
+
+ 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 aTab The xul:tab for which you want to reset the page
+ * errors counters.
+ */
+DeveloperToolbar.prototype.resetErrorsCount =
+function DT_resetErrorsCount(aTab)
+{
+ let tabId = aTab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+};
+
+/**
+ * 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 aChromeDoc document from which we can pull the parts we need.
+ * @param aInput the input element that should get focus.
+ * @param aLoadCallback called when the panel is loaded properly.
+ */
+function OutputPanel(aDevToolbar, aLoadCallback)
+{
+ this._devtoolbar = aDevToolbar;
+ this._input = this._devtoolbar._input;
+ this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
+
+ this._loadCallback = aLoadCallback;
+
+ /*
+ <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://browser/content/devtools/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://browser/content/devtools/commandlineoutput.xhtml");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this.displayedOutput = undefined;
+
+ this._onload = this._onload.bind(this);
+ this._update = this._update.bind(this);
+ this._frame.addEventListener("load", this._onload, true);
+
+ this.loaded = false;
+}
+
+/**
+ * Wire up the element from the iframe, and inform the _loadCallback.
+ */
+OutputPanel.prototype._onload = function OP_onload()
+{
+ this._frame.removeEventListener("load", this._onload, true);
+ delete this._onload;
+
+ this.document = this._frame.contentDocument;
+
+ 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);
+
+ this.loaded = true;
+ if (this._loadCallback) {
+ this._loadCallback();
+ delete this._loadCallback;
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+OutputPanel.prototype._onpopuphiding = function OP_onpopuphiding(aEvent)
+{
+ // 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) {
+ aEvent.preventDefault();
+ }
+};
+
+/**
+ * Display the OutputPanel.
+ */
+OutputPanel.prototype.show = function OP_show()
+{
+ 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._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 CLP_resize()
+{
+ 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(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 OP_outputChanged(aEvent)
+{
+ if (aEvent.output.hidden) {
+ return;
+ }
+
+ this.remove();
+
+ this.displayedOutput = aEvent.output;
+ this.displayedOutput.onClose.add(this.remove, this);
+
+ 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 OP_update()
+{
+ // 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 requisition = this._devtoolbar.display.requisition;
+ let nodePromise = converters.convert(this.displayedOutput.data,
+ this.displayedOutput.type, 'dom',
+ requisition.conversionContext);
+ nodePromise.then(function(node) {
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ var links = node.ownerDocument.querySelectorAll('*[href]');
+ for (var i = 0; i < links.length; i++) {
+ links[i].setAttribute('target', '_blank');
+ }
+
+ this._div.appendChild(node);
+ }.bind(this));
+ this.show();
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.remove = function OP_remove()
+{
+ if (isLinux) {
+ this.canHide = true;
+ }
+
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+
+ if (this.displayedOutput) {
+ this.displayedOutput.onClose.remove(this.remove, this);
+ delete this.displayedOutput;
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.destroy = function OP_destroy()
+{
+ 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._onload;
+ 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 OP_visibilityChanged(aEvent)
+{
+ if (aEvent.outputVisible === true) {
+ // this.show is called by _outputChanged
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
+
+
+/**
+ * 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 aChromeDoc document from which we can pull the parts we need.
+ * @param aInput the input element that should get focus.
+ * @param aLoadCallback called when the panel is loaded properly.
+ */
+function TooltipPanel(aChromeDoc, aInput, aLoadCallback)
+{
+ this._input = aInput;
+ this._toolbar = aChromeDoc.getElementById("developer-toolbar");
+ this._dimensions = { start: 0, end: 0 };
+
+ this._onload = this._onload.bind(this);
+ this._loadCallback = aLoadCallback;
+ /*
+ <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://browser/content/devtools/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 = aChromeDoc.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 = aChromeDoc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-tooltip-frame";
+ this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml");
+ this._frame.setAttribute("flex", "1");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this._frame.addEventListener("load", this._onload, true);
+
+ this.loaded = false;
+}
+
+/**
+ * Wire up the element from the iframe, and inform the _loadCallback.
+ */
+TooltipPanel.prototype._onload = function TP_onload()
+{
+ this._frame.removeEventListener("load", this._onload, true);
+
+ this.document = this._frame.contentDocument;
+ 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);
+
+ this.loaded = true;
+
+ if (this._loadCallback) {
+ this._loadCallback();
+ delete this._loadCallback;
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+TooltipPanel.prototype._onpopuphiding = function TP_onpopuphiding(aEvent)
+{
+ // 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) {
+ aEvent.preventDefault();
+ }
+};
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype.show = function TP_show(aDimensions)
+{
+ if (!aDimensions) {
+ aDimensions = { start: 0, end: 0 };
+ }
+ this._dimensions = aDimensions;
+
+ // 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(function() {
+ this._resize();
+ }.bind(this), 0);
+
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ this._resize();
+ this._panel.openPopup(this._input, "before_start", aDimensions.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 TP_resize()
+{
+ 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 TP_remove()
+{
+ if (isLinux) {
+ this.canHide = true;
+ }
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.destroy = function TP_destroy()
+{
+ 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._onload;
+ 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 TP_visibilityChanged(aEvent)
+{
+ if (aEvent.tooltipVisible === true) {
+ this.show(aEvent.dimensions);
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
diff --git a/browser/devtools/shared/FloatingScrollbars.jsm b/browser/devtools/shared/FloatingScrollbars.jsm
new file mode 100644
index 000000000..a47a784bd
--- /dev/null
+++ b/browser/devtools/shared/FloatingScrollbars.jsm
@@ -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/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = [ "switchToFloatingScrollbars", "switchToNativeScrollbars" ];
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let URL = Services.io.newURI("chrome://browser/skin/devtools/floating-scrollbars.css", null, null);
+
+let trackedTabs = new WeakMap();
+
+/**
+ * Switch to floating scrollbars, à la mobile.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+this.switchToFloatingScrollbars = function switchToFloatingScrollbars(aTab) {
+ let mgr = trackedTabs.get(aTab);
+ if (!mgr) {
+ mgr = new ScrollbarManager(aTab);
+ }
+ mgr.switchToFloating();
+}
+
+/**
+ * Switch to original native scrollbars.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+this.switchToNativeScrollbars = function switchToNativeScrollbars(aTab) {
+ let mgr = trackedTabs.get(aTab);
+ if (mgr) {
+ mgr.reset();
+ }
+}
+
+function ScrollbarManager(aTab) {
+ trackedTabs.set(aTab, this);
+
+ this.attachedTab = aTab;
+ this.attachedBrowser = aTab.linkedBrowser;
+
+ this.reset = this.reset.bind(this);
+ this.switchToFloating = this.switchToFloating.bind(this);
+
+ this.attachedTab.addEventListener("TabClose", this.reset, true);
+ this.attachedBrowser.addEventListener("DOMContentLoaded", this.switchToFloating, true);
+}
+
+ScrollbarManager.prototype = {
+ get win() {
+ return this.attachedBrowser.contentWindow;
+ },
+
+ /*
+ * Change the look of the scrollbars.
+ */
+ switchToFloating: function() {
+ let windows = this.getInnerWindows(this.win);
+ windows.forEach(this.injectStyleSheet);
+ this.forceStyle();
+ },
+
+
+ /*
+ * Reset the look of the scrollbars.
+ */
+ reset: function() {
+ let windows = this.getInnerWindows(this.win);
+ windows.forEach(this.removeStyleSheet);
+ this.forceStyle(this.attachedBrowser);
+ this.attachedBrowser.removeEventListener("DOMContentLoaded", this.switchToFloating, true);
+ this.attachedTab.removeEventListener("TabClose", this.reset, true);
+ trackedTabs.delete(this.attachedTab);
+ },
+
+ /*
+ * Toggle the display property of the window to force the style to be applied.
+ */
+ forceStyle: function() {
+ let parentWindow = this.attachedBrowser.ownerDocument.defaultView;
+ let display = parentWindow.getComputedStyle(this.attachedBrowser).display; // Save display value
+ this.attachedBrowser.style.display = "none";
+ parentWindow.getComputedStyle(this.attachedBrowser).display; // Flush
+ this.attachedBrowser.style.display = display; // Restore
+ },
+
+ /*
+ * return all the window objects present in the hiearchy of a window.
+ */
+ getInnerWindows: function(win) {
+ let iframes = win.document.querySelectorAll("iframe");
+ let innerWindows = [];
+ for (let iframe of iframes) {
+ innerWindows = innerWindows.concat(this.getInnerWindows(iframe.contentWindow));
+ }
+ return [win].concat(innerWindows);
+ },
+
+ /*
+ * Append the new scrollbar style.
+ */
+ injectStyleSheet: function(win) {
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.loadSheet(URL, win.AGENT_SHEET);
+ }catch(e) {}
+ },
+
+ /*
+ * Remove the injected stylesheet.
+ */
+ removeStyleSheet: function(win) {
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.removeSheet(URL, win.AGENT_SHEET);
+ }catch(e) {}
+ },
+}
diff --git a/browser/devtools/shared/Jsbeautify.jsm b/browser/devtools/shared/Jsbeautify.jsm
new file mode 100644
index 000000000..4171d5746
--- /dev/null
+++ b/browser/devtools/shared/Jsbeautify.jsm
@@ -0,0 +1,1303 @@
+/*jslint onevar: false, plusplus: false */
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+ JS Beautifier
+---------------
+
+
+ Written by Einar Lielmanis, <einar@jsbeautifier.org>
+ http://jsbeautifier.org/
+
+ Originally converted to javascript by Vital, <vital76@gmail.com>
+ "End braces on own line" added by Chris J. Shull, <chrisjshull@gmail.com>
+
+ You are free to use this in any way you want, in case you find this useful or working for you.
+
+ Usage:
+ js_beautify(js_source_text);
+ js_beautify(js_source_text, options);
+
+ The options are:
+ indent_size (default 4) - indentation size,
+ indent_char (default space) - character to indent with,
+ preserve_newlines (default true) - whether existing line breaks should be preserved,
+ max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk,
+
+ jslint_happy (default false) - if true, then jslint-stricter mode is enforced.
+
+ jslint_happy !jslint_happy
+ ---------------------------------
+ function () function()
+
+ brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "expand-strict"
+ put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line.
+
+ expand-strict: put brace on own line even in such cases:
+
+ var a =
+ {
+ a: 5,
+ b: 6
+ }
+ This mode may break your scripts - e.g "return { a: 1 }" will be broken into two lines, so beware.
+
+ space_before_conditional (default true) - should the space before conditional statement be added, "if(true)" vs "if (true)",
+
+ unescape_strings (default false) - should printable characters in strings encoded in \xNN notation be unescaped, "example" vs "\x65\x78\x61\x6d\x70\x6c\x65"
+
+ e.g
+
+ js_beautify(js_source_text, {
+ 'indent_size': 1,
+ 'indent_char': '\t'
+ });
+
+
+*/
+
+this.EXPORTED_SYMBOLS = ["js_beautify"];
+
+this.js_beautify = function js_beautify(js_source_text, options) {
+
+ var input, output, token_text, last_type, last_text, last_last_text, last_word, flags, flag_store, indent_string;
+ var whitespace, wordchar, punct, parser_pos, line_starters, digits;
+ var prefix, token_type, do_block_just_closed;
+ var wanted_newline, just_added_newline, n_newlines;
+ var preindent_string = '';
+
+
+ // Some interpreters have unexpected results with foo = baz || bar;
+ options = options ? options : {};
+
+ var opt_brace_style;
+
+ // compatibility
+ if (options.space_after_anon_function !== undefined && options.jslint_happy === undefined) {
+ options.jslint_happy = options.space_after_anon_function;
+ }
+ if (options.braces_on_own_line !== undefined) { //graceful handling of deprecated option
+ opt_brace_style = options.braces_on_own_line ? "expand" : "collapse";
+ }
+ opt_brace_style = options.brace_style ? options.brace_style : (opt_brace_style ? opt_brace_style : "collapse");
+
+
+ var opt_indent_size = options.indent_size ? options.indent_size : 4;
+ var opt_indent_char = options.indent_char ? options.indent_char : ' ';
+ var opt_preserve_newlines = typeof options.preserve_newlines === 'undefined' ? true : options.preserve_newlines;
+ var opt_max_preserve_newlines = typeof options.max_preserve_newlines === 'undefined' ? false : options.max_preserve_newlines;
+ var opt_jslint_happy = options.jslint_happy === 'undefined' ? false : options.jslint_happy;
+ var opt_keep_array_indentation = typeof options.keep_array_indentation === 'undefined' ? false : options.keep_array_indentation;
+ var opt_space_before_conditional = typeof options.space_before_conditional === 'undefined' ? true : options.space_before_conditional;
+ var opt_indent_case = typeof options.indent_case === 'undefined' ? false : options.indent_case;
+ var opt_unescape_strings = typeof options.unescape_strings === 'undefined' ? false : options.unescape_strings;
+
+ just_added_newline = false;
+
+ // cache the source's length.
+ var input_length = js_source_text.length;
+
+ function trim_output(eat_newlines) {
+ eat_newlines = typeof eat_newlines === 'undefined' ? false : eat_newlines;
+ while (output.length && (output[output.length - 1] === ' '
+ || output[output.length - 1] === indent_string
+ || output[output.length - 1] === preindent_string
+ || (eat_newlines && (output[output.length - 1] === '\n' || output[output.length - 1] === '\r')))) {
+ output.pop();
+ }
+ }
+
+ function trim(s) {
+ return s.replace(/^\s\s*|\s\s*$/, '');
+ }
+
+ // we could use just string.split, but
+ // IE doesn't like returning empty strings
+ function split_newlines(s) {
+ return s.split(/\x0d\x0a|\x0a/);
+ }
+
+ function force_newline() {
+ var old_keep_array_indentation = opt_keep_array_indentation;
+ opt_keep_array_indentation = false;
+ print_newline();
+ opt_keep_array_indentation = old_keep_array_indentation;
+ }
+
+ function print_newline(ignore_repeated) {
+
+ flags.eat_next_space = false;
+ if (opt_keep_array_indentation && is_array(flags.mode)) {
+ return;
+ }
+
+ ignore_repeated = typeof ignore_repeated === 'undefined' ? true : ignore_repeated;
+
+ flags.if_line = false;
+ trim_output();
+
+ if (!output.length) {
+ return; // no newline on start of file
+ }
+
+ if (output[output.length - 1] !== "\n" || !ignore_repeated) {
+ just_added_newline = true;
+ output.push("\n");
+ }
+ if (preindent_string) {
+ output.push(preindent_string);
+ }
+ for (var i = 0; i < flags.indentation_level; i += 1) {
+ output.push(indent_string);
+ }
+ if (flags.var_line && flags.var_line_reindented) {
+ output.push(indent_string); // skip space-stuffing, if indenting with a tab
+ }
+ if (flags.case_body) {
+ output.push(indent_string);
+ }
+ }
+
+
+
+ function print_single_space() {
+
+ if (last_type === 'TK_COMMENT') {
+ return print_newline();
+ }
+ if (flags.eat_next_space) {
+ flags.eat_next_space = false;
+ return;
+ }
+ var last_output = ' ';
+ if (output.length) {
+ last_output = output[output.length - 1];
+ }
+ if (last_output !== ' ' && last_output !== '\n' && last_output !== indent_string) { // prevent occassional duplicate space
+ output.push(' ');
+ }
+ }
+
+
+ function print_token() {
+ just_added_newline = false;
+ flags.eat_next_space = false;
+ output.push(token_text);
+ }
+
+ function indent() {
+ flags.indentation_level += 1;
+ }
+
+
+ function remove_indent() {
+ if (output.length && output[output.length - 1] === indent_string) {
+ output.pop();
+ }
+ }
+
+ function set_mode(mode) {
+ if (flags) {
+ flag_store.push(flags);
+ }
+ flags = {
+ previous_mode: flags ? flags.mode : 'BLOCK',
+ mode: mode,
+ var_line: false,
+ var_line_tainted: false,
+ var_line_reindented: false,
+ in_html_comment: false,
+ if_line: false,
+ in_case_statement: false, // switch(..){ INSIDE HERE }
+ in_case: false, // we're on the exact line with "case 0:"
+ case_body: false, // the indented case-action block
+ eat_next_space: false,
+ indentation_baseline: -1,
+ indentation_level: (flags ? flags.indentation_level + (flags.case_body ? 1 : 0) + ((flags.var_line && flags.var_line_reindented) ? 1 : 0) : 0),
+ ternary_depth: 0
+ };
+ }
+
+ function is_array(mode) {
+ return mode === '[EXPRESSION]' || mode === '[INDENTED-EXPRESSION]';
+ }
+
+ function is_expression(mode) {
+ return in_array(mode, ['[EXPRESSION]', '(EXPRESSION)', '(FOR-EXPRESSION)', '(COND-EXPRESSION)']);
+ }
+
+ function restore_mode() {
+ do_block_just_closed = flags.mode === 'DO_BLOCK';
+ if (flag_store.length > 0) {
+ var mode = flags.mode;
+ flags = flag_store.pop();
+ flags.previous_mode = mode;
+ }
+ }
+
+ function all_lines_start_with(lines, c) {
+ for (var i = 0; i < lines.length; i++) {
+ var line = trim(lines[i]);
+ if (line.charAt(0) !== c) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function is_special_word(word) {
+ return in_array(word, ['case', 'return', 'do', 'if', 'throw', 'else']);
+ }
+
+ function in_array(what, arr) {
+ for (var i = 0; i < arr.length; i += 1) {
+ if (arr[i] === what) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function look_up(exclude) {
+ var local_pos = parser_pos;
+ var c = input.charAt(local_pos);
+ while (in_array(c, whitespace) && c !== exclude) {
+ local_pos++;
+ if (local_pos >= input_length) {
+ return 0;
+ }
+ c = input.charAt(local_pos);
+ }
+ return c;
+ }
+
+ function get_next_token() {
+ var i;
+ var resulting_string;
+
+ n_newlines = 0;
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ wanted_newline = false;
+
+ var c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+
+ var keep_whitespace = opt_keep_array_indentation && is_array(flags.mode);
+
+ if (keep_whitespace) {
+
+ //
+ // slight mess to allow nice preservation of array indentation and reindent that correctly
+ // first time when we get to the arrays:
+ // var a = [
+ // ....'something'
+ // we make note of whitespace_count = 4 into flags.indentation_baseline
+ // so we know that 4 whitespaces in original source match indent_level of reindented source
+ //
+ // and afterwards, when we get to
+ // 'something,
+ // .......'something else'
+ // we know that this should be indented to indent_level + (7 - indentation_baseline) spaces
+ //
+ var whitespace_count = 0;
+
+ while (in_array(c, whitespace)) {
+
+ if (c === "\n") {
+ trim_output();
+ output.push("\n");
+ just_added_newline = true;
+ whitespace_count = 0;
+ } else {
+ if (c === '\t') {
+ whitespace_count += 4;
+ } else if (c === '\r') {
+ // nothing
+ } else {
+ whitespace_count += 1;
+ }
+ }
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ }
+ if (flags.indentation_baseline === -1) {
+ flags.indentation_baseline = whitespace_count;
+ }
+
+ if (just_added_newline) {
+ for (i = 0; i < flags.indentation_level + 1; i += 1) {
+ output.push(indent_string);
+ }
+ if (flags.indentation_baseline !== -1) {
+ for (i = 0; i < whitespace_count - flags.indentation_baseline; i++) {
+ output.push(' ');
+ }
+ }
+ }
+
+ } else {
+ while (in_array(c, whitespace)) {
+
+ if (c === "\n") {
+ n_newlines += ((opt_max_preserve_newlines) ? (n_newlines <= opt_max_preserve_newlines) ? 1 : 0 : 1);
+ }
+
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ }
+
+ if (opt_preserve_newlines) {
+ if (n_newlines > 1) {
+ for (i = 0; i < n_newlines; i += 1) {
+ print_newline(i === 0);
+ just_added_newline = true;
+ }
+ }
+ }
+ wanted_newline = n_newlines > 0;
+ }
+
+
+ if (in_array(c, wordchar)) {
+ if (parser_pos < input_length) {
+ while (in_array(input.charAt(parser_pos), wordchar)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos === input_length) {
+ break;
+ }
+ }
+ }
+
+ // small and surprisingly unugly hack for 1E-10 representation
+ if (parser_pos !== input_length && c.match(/^[0-9]+[Ee]$/) && (input.charAt(parser_pos) === '-' || input.charAt(parser_pos) === '+')) {
+
+ var sign = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ var t = get_next_token();
+ c += sign + t[0];
+ return [c, 'TK_WORD'];
+ }
+
+ if (c === 'in') { // hack for 'in' operator
+ return [c, 'TK_OPERATOR'];
+ }
+ if (wanted_newline && last_type !== 'TK_OPERATOR'
+ && last_type !== 'TK_EQUALS'
+ && !flags.if_line && (opt_preserve_newlines || last_text !== 'var')) {
+ print_newline();
+ }
+ return [c, 'TK_WORD'];
+ }
+
+ if (c === '(' || c === '[') {
+ return [c, 'TK_START_EXPR'];
+ }
+
+ if (c === ')' || c === ']') {
+ return [c, 'TK_END_EXPR'];
+ }
+
+ if (c === '{') {
+ return [c, 'TK_START_BLOCK'];
+ }
+
+ if (c === '}') {
+ return [c, 'TK_END_BLOCK'];
+ }
+
+ if (c === ';') {
+ return [c, 'TK_SEMICOLON'];
+ }
+
+ if (c === '/') {
+ var comment = '';
+ // peek for comment /* ... */
+ var inline_comment = true;
+ if (input.charAt(parser_pos) === '*') {
+ parser_pos += 1;
+ if (parser_pos < input_length) {
+ while (parser_pos < input_length &&
+ ! (input.charAt(parser_pos) === '*' && input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) === '/')) {
+ c = input.charAt(parser_pos);
+ comment += c;
+ if (c === "\n" || c === "\r") {
+ inline_comment = false;
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+ }
+ parser_pos += 2;
+ if (inline_comment && n_newlines === 0) {
+ return ['/*' + comment + '*/', 'TK_INLINE_COMMENT'];
+ } else {
+ return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT'];
+ }
+ }
+ // peek for comment // ...
+ if (input.charAt(parser_pos) === '/') {
+ comment = c;
+ while (input.charAt(parser_pos) !== '\r' && input.charAt(parser_pos) !== '\n') {
+ comment += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+ if (wanted_newline) {
+ print_newline();
+ }
+ return [comment, 'TK_COMMENT'];
+ }
+
+ }
+
+ if (c === "'" || // string
+ c === '"' || // string
+ (c === '/' &&
+ ((last_type === 'TK_WORD' && is_special_word(last_text)) ||
+ (last_text === ')' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) ||
+ (last_type === 'TK_COMMA' || last_type === 'TK_COMMENT' || last_type === 'TK_START_EXPR' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_OPERATOR' || last_type === 'TK_EQUALS' || last_type === 'TK_EOF' || last_type === 'TK_SEMICOLON')))) { // regexp
+ var sep = c;
+ var esc = false;
+ var esc1 = 0;
+ var esc2 = 0;
+ resulting_string = c;
+
+ if (parser_pos < input_length) {
+ if (sep === '/') {
+ //
+ // handle regexp separately...
+ //
+ var in_char_class = false;
+ while (esc || in_char_class || input.charAt(parser_pos) !== sep) {
+ resulting_string += input.charAt(parser_pos);
+ if (!esc) {
+ esc = input.charAt(parser_pos) === '\\';
+ if (input.charAt(parser_pos) === '[') {
+ in_char_class = true;
+ } else if (input.charAt(parser_pos) === ']') {
+ in_char_class = false;
+ }
+ } else {
+ esc = false;
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ // incomplete string/rexp when end-of-file reached.
+ // bail out with what had been received so far.
+ return [resulting_string, 'TK_STRING'];
+ }
+ }
+
+ } else {
+ //
+ // and handle string also separately
+ //
+ while (esc || input.charAt(parser_pos) !== sep) {
+ resulting_string += input.charAt(parser_pos);
+ if (esc1 && esc1 >= esc2) {
+ esc1 = parseInt(resulting_string.substr(-esc2), 16);
+ if (esc1 && esc1 >= 0x20 && esc1 <= 0x7e) {
+ esc1 = String.fromCharCode(esc1);
+ resulting_string = resulting_string.substr(0, resulting_string.length - esc2 - 2) + (((esc1 === sep) || (esc1 === '\\')) ? '\\' : '') + esc1;
+ }
+ esc1 = 0;
+ }
+ if (esc1) {
+ esc1++;
+ } else if (!esc) {
+ esc = input.charAt(parser_pos) === '\\';
+ } else {
+ esc = false;
+ if (opt_unescape_strings) {
+ if (input.charAt(parser_pos) === 'x') {
+ esc1++;
+ esc2 = 2;
+ } else if (input.charAt(parser_pos) === 'u') {
+ esc1++;
+ esc2 = 4;
+ }
+ }
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ // incomplete string/rexp when end-of-file reached.
+ // bail out with what had been received so far.
+ return [resulting_string, 'TK_STRING'];
+ }
+ }
+ }
+
+
+
+ }
+
+ parser_pos += 1;
+
+ resulting_string += sep;
+
+ if (sep === '/') {
+ // regexps may have modifiers /regexp/MOD , so fetch those, too
+ while (parser_pos < input_length && in_array(input.charAt(parser_pos), wordchar)) {
+ resulting_string += input.charAt(parser_pos);
+ parser_pos += 1;
+ }
+ }
+ return [resulting_string, 'TK_STRING'];
+ }
+
+ if (c === '#') {
+
+
+ if (output.length === 0 && input.charAt(parser_pos) === '!') {
+ // shebang
+ resulting_string = c;
+ while (parser_pos < input_length && c !== '\n') {
+ c = input.charAt(parser_pos);
+ resulting_string += c;
+ parser_pos += 1;
+ }
+ output.push(trim(resulting_string) + '\n');
+ print_newline();
+ return get_next_token();
+ }
+
+
+
+ // Spidermonkey-specific sharp variables for circular references
+ // https://developer.mozilla.org/En/Sharp_variables_in_JavaScript
+ // http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935
+ var sharp = '#';
+ if (parser_pos < input_length && in_array(input.charAt(parser_pos), digits)) {
+ do {
+ c = input.charAt(parser_pos);
+ sharp += c;
+ parser_pos += 1;
+ } while (parser_pos < input_length && c !== '#' && c !== '=');
+ if (c === '#') {
+ //
+ } else if (input.charAt(parser_pos) === '[' && input.charAt(parser_pos + 1) === ']') {
+ sharp += '[]';
+ parser_pos += 2;
+ } else if (input.charAt(parser_pos) === '{' && input.charAt(parser_pos + 1) === '}') {
+ sharp += '{}';
+ parser_pos += 2;
+ }
+ return [sharp, 'TK_WORD'];
+ }
+ }
+
+ if (c === '<' && input.substring(parser_pos - 1, parser_pos + 3) === '<!--') {
+ parser_pos += 3;
+ c = '<!--';
+ while (input.charAt(parser_pos) !== '\n' && parser_pos < input_length) {
+ c += input.charAt(parser_pos);
+ parser_pos++;
+ }
+ flags.in_html_comment = true;
+ return [c, 'TK_COMMENT'];
+ }
+
+ if (c === '-' && flags.in_html_comment && input.substring(parser_pos - 1, parser_pos + 2) === '-->') {
+ flags.in_html_comment = false;
+ parser_pos += 2;
+ if (wanted_newline) {
+ print_newline();
+ }
+ return ['-->', 'TK_COMMENT'];
+ }
+
+ if (in_array(c, punct)) {
+ while (parser_pos < input_length && in_array(c + input.charAt(parser_pos), punct)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+
+ if (c === ',') {
+ return [c, 'TK_COMMA'];
+ } else if (c === '=') {
+ return [c, 'TK_EQUALS'];
+ } else {
+ return [c, 'TK_OPERATOR'];
+ }
+ }
+
+ return [c, 'TK_UNKNOWN'];
+ }
+
+ //----------------------------------
+ indent_string = '';
+ while (opt_indent_size > 0) {
+ indent_string += opt_indent_char;
+ opt_indent_size -= 1;
+ }
+
+ while (js_source_text && (js_source_text.charAt(0) === ' ' || js_source_text.charAt(0) === '\t')) {
+ preindent_string += js_source_text.charAt(0);
+ js_source_text = js_source_text.substring(1);
+ }
+ input = js_source_text;
+
+ last_word = ''; // last 'TK_WORD' passed
+ last_type = 'TK_START_EXPR'; // last token type
+ last_text = ''; // last token text
+ last_last_text = ''; // pre-last token text
+ output = [];
+
+ do_block_just_closed = false;
+
+ whitespace = "\n\r\t ".split('');
+ wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split('');
+ digits = '0123456789'.split('');
+
+ punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::';
+ punct += ' <%= <% %> <?= <? ?>'; // try to be a good boy and try not to break the markup language identifiers
+ punct = punct.split(' ');
+
+ // words which should always start on new line.
+ line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',');
+
+ // states showing if we are currently in expression (i.e. "if" case) - 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'.
+ // some formatting depends on that.
+ flag_store = [];
+ set_mode('BLOCK');
+
+ parser_pos = 0;
+ while (true) {
+ var t = get_next_token();
+ token_text = t[0];
+ token_type = t[1];
+ if (token_type === 'TK_EOF') {
+ break;
+ }
+
+ switch (token_type) {
+
+ case 'TK_START_EXPR':
+
+ if (token_text === '[') {
+
+ if (last_type === 'TK_WORD' || last_text === ')') {
+ // this is array index specifier, break immediately
+ // a[x], fn()[x]
+ if (in_array(last_text, line_starters)) {
+ print_single_space();
+ }
+ set_mode('(EXPRESSION)');
+ print_token();
+ break;
+ }
+
+ if (flags.mode === '[EXPRESSION]' || flags.mode === '[INDENTED-EXPRESSION]') {
+ if (last_last_text === ']' && last_text === ',') {
+ // ], [ goes to new line
+ if (flags.mode === '[EXPRESSION]') {
+ flags.mode = '[INDENTED-EXPRESSION]';
+ if (!opt_keep_array_indentation) {
+ indent();
+ }
+ }
+ set_mode('[EXPRESSION]');
+ if (!opt_keep_array_indentation) {
+ print_newline();
+ }
+ } else if (last_text === '[') {
+ if (flags.mode === '[EXPRESSION]') {
+ flags.mode = '[INDENTED-EXPRESSION]';
+ if (!opt_keep_array_indentation) {
+ indent();
+ }
+ }
+ set_mode('[EXPRESSION]');
+
+ if (!opt_keep_array_indentation) {
+ print_newline();
+ }
+ } else {
+ set_mode('[EXPRESSION]');
+ }
+ } else {
+ set_mode('[EXPRESSION]');
+ }
+
+
+
+ } else {
+ if (last_word === 'for') {
+ set_mode('(FOR-EXPRESSION)');
+ } else if (in_array(last_word, ['if', 'while'])) {
+ set_mode('(COND-EXPRESSION)');
+ } else {
+ set_mode('(EXPRESSION)');
+ }
+ }
+
+ if (last_text === ';' || last_type === 'TK_START_BLOCK') {
+ print_newline();
+ } else if (last_type === 'TK_END_EXPR' || last_type === 'TK_START_EXPR' || last_type === 'TK_END_BLOCK' || last_text === '.') {
+ if (wanted_newline) {
+ print_newline();
+ }
+ // do nothing on (( and )( and ][ and ]( and .(
+ } else if (last_type !== 'TK_WORD' && last_type !== 'TK_OPERATOR') {
+ print_single_space();
+ } else if (last_word === 'function' || last_word === 'typeof') {
+ // function() vs function ()
+ if (opt_jslint_happy) {
+ print_single_space();
+ }
+ } else if (in_array(last_text, line_starters) || last_text === 'catch') {
+ if (opt_space_before_conditional) {
+ print_single_space();
+ }
+ }
+ print_token();
+
+ break;
+
+ case 'TK_END_EXPR':
+ if (token_text === ']') {
+ if (opt_keep_array_indentation) {
+ if (last_text === '}') {
+ // trim_output();
+ // print_newline(true);
+ remove_indent();
+ print_token();
+ restore_mode();
+ break;
+ }
+ } else {
+ if (flags.mode === '[INDENTED-EXPRESSION]') {
+ if (last_text === ']') {
+ restore_mode();
+ print_newline();
+ print_token();
+ break;
+ }
+ }
+ }
+ }
+ restore_mode();
+ print_token();
+ break;
+
+ case 'TK_START_BLOCK':
+
+ if (last_word === 'do') {
+ set_mode('DO_BLOCK');
+ } else {
+ set_mode('BLOCK');
+ }
+ if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") {
+ var empty_braces = false;
+ if (opt_brace_style === "expand-strict") {
+ empty_braces = (look_up() === '}');
+ if (!empty_braces) {
+ print_newline(true);
+ }
+ } else {
+ if (last_type !== 'TK_OPERATOR') {
+ if (last_text === '=' || (is_special_word(last_text) && last_text !== 'else')) {
+ print_single_space();
+ } else {
+ print_newline(true);
+ }
+ }
+ }
+ print_token();
+ if (!empty_braces) {
+ indent();
+ }
+ } else {
+ if (last_type !== 'TK_OPERATOR' && last_type !== 'TK_START_EXPR') {
+ if (last_type === 'TK_START_BLOCK') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ } else {
+ // if TK_OPERATOR or TK_START_EXPR
+ if (is_array(flags.previous_mode) && last_text === ',') {
+ if (last_last_text === '}') {
+ // }, { in array context
+ print_single_space();
+ } else {
+ print_newline(); // [a, b, c, {
+ }
+ }
+ }
+ indent();
+ print_token();
+ }
+
+ break;
+
+ case 'TK_END_BLOCK':
+ restore_mode();
+ if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") {
+ if (last_text !== '{') {
+ print_newline();
+ }
+ print_token();
+ } else {
+ if (last_type === 'TK_START_BLOCK') {
+ // nothing
+ if (just_added_newline) {
+ remove_indent();
+ } else {
+ // {}
+ trim_output();
+ }
+ } else {
+ if (is_array(flags.mode) && opt_keep_array_indentation) {
+ // we REALLY need a newline here, but newliner would skip that
+ opt_keep_array_indentation = false;
+ print_newline();
+ opt_keep_array_indentation = true;
+
+ } else {
+ print_newline();
+ }
+ }
+ print_token();
+ }
+ break;
+
+ case 'TK_WORD':
+
+ // no, it's not you. even I have problems understanding how this works
+ // and what does what.
+ if (do_block_just_closed) {
+ // do {} ## while ()
+ print_single_space();
+ print_token();
+ print_single_space();
+ do_block_just_closed = false;
+ break;
+ }
+
+ prefix = 'NONE';
+
+ if (token_text === 'function') {
+ if (flags.var_line && last_type !== 'TK_EQUALS' ) {
+ flags.var_line_reindented = true;
+ }
+ if ((just_added_newline || last_text === ';') && last_text !== '{'
+ && last_type !== 'TK_BLOCK_COMMENT' && last_type !== 'TK_COMMENT') {
+ // make sure there is a nice clean space of at least one blank line
+ // before a new function definition
+ n_newlines = just_added_newline ? n_newlines : 0;
+ if (!opt_preserve_newlines) {
+ n_newlines = 1;
+ }
+
+ for (var i = 0; i < 2 - n_newlines; i++) {
+ print_newline(false);
+ }
+ }
+ if (last_type === 'TK_WORD') {
+ if (last_text === 'get' || last_text === 'set' || last_text === 'new' || last_text === 'return') {
+ print_single_space();
+ } else {
+ print_newline();
+ }
+ } else if (last_type === 'TK_OPERATOR' || last_text === '=') {
+ // foo = function
+ print_single_space();
+ } else if (is_expression(flags.mode)) {
+ //ää print nothing
+ } else {
+ print_newline();
+ }
+
+ print_token();
+ last_word = token_text;
+ break;
+ }
+
+ if (token_text === 'case' || (token_text === 'default' && flags.in_case_statement)) {
+ if (last_text === ':' || flags.case_body) {
+ // switch cases following one another
+ remove_indent();
+ } else {
+ // case statement starts in the same line where switch
+ if (!opt_indent_case) {
+ flags.indentation_level--;
+ }
+ print_newline();
+ if (!opt_indent_case) {
+ flags.indentation_level++;
+ }
+ }
+ print_token();
+ flags.in_case = true;
+ flags.in_case_statement = true;
+ flags.case_body = false;
+ break;
+ }
+
+ if (last_type === 'TK_END_BLOCK') {
+
+ if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+ prefix = 'NEWLINE';
+ } else {
+ if (opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") {
+ prefix = 'NEWLINE';
+ } else {
+ prefix = 'SPACE';
+ print_single_space();
+ }
+ }
+ } else if (last_type === 'TK_SEMICOLON' && (flags.mode === 'BLOCK' || flags.mode === 'DO_BLOCK')) {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_SEMICOLON' && is_expression(flags.mode)) {
+ prefix = 'SPACE';
+ } else if (last_type === 'TK_STRING') {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_WORD') {
+ if (last_text === 'else') {
+ // eat newlines between ...else *** some_op...
+ // won't preserve extra newlines in this place (if any), but don't care that much
+ trim_output(true);
+ }
+ prefix = 'SPACE';
+ } else if (last_type === 'TK_START_BLOCK') {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_END_EXPR') {
+ print_single_space();
+ prefix = 'NEWLINE';
+ }
+
+ if (in_array(token_text, line_starters) && last_text !== ')') {
+ if (last_text === 'else') {
+ prefix = 'SPACE';
+ } else {
+ prefix = 'NEWLINE';
+ }
+
+ }
+
+ if (flags.if_line && last_type === 'TK_END_EXPR') {
+ flags.if_line = false;
+ }
+ if (in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+ if (last_type !== 'TK_END_BLOCK' || opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") {
+ print_newline();
+ } else {
+ trim_output(true);
+ print_single_space();
+ }
+ } else if (prefix === 'NEWLINE') {
+ if (is_special_word(last_text)) {
+ // no newline between 'return nnn'
+ print_single_space();
+ } else if (last_type !== 'TK_END_EXPR') {
+ if ((last_type !== 'TK_START_EXPR' || token_text !== 'var') && last_text !== ':') {
+ // no need to force newline on 'var': for (var x = 0...)
+ if (token_text === 'if' && last_word === 'else' && last_text !== '{') {
+ // no newline for } else if {
+ print_single_space();
+ } else {
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ print_newline();
+ }
+ }
+ } else if (in_array(token_text, line_starters) && last_text !== ')') {
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ print_newline();
+ }
+ } else if (is_array(flags.mode) && last_text === ',' && last_last_text === '}') {
+ print_newline(); // }, in lists get a newline treatment
+ } else if (prefix === 'SPACE') {
+ print_single_space();
+ }
+ print_token();
+ last_word = token_text;
+
+ if (token_text === 'var') {
+ flags.var_line = true;
+ flags.var_line_reindented = false;
+ flags.var_line_tainted = false;
+ }
+
+ if (token_text === 'if') {
+ flags.if_line = true;
+ }
+ if (token_text === 'else') {
+ flags.if_line = false;
+ }
+
+ break;
+
+ case 'TK_SEMICOLON':
+
+ print_token();
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ if (flags.mode === 'OBJECT') {
+ // OBJECT mode is weird and doesn't get reset too well.
+ flags.mode = 'BLOCK';
+ }
+ break;
+
+ case 'TK_STRING':
+
+ if (last_type === 'TK_END_EXPR' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) {
+ print_single_space();
+ } else if (last_type === 'TK_COMMENT' || last_type === 'TK_STRING' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_SEMICOLON') {
+ print_newline();
+ } else if (last_type === 'TK_WORD') {
+ print_single_space();
+ }
+ print_token();
+ break;
+
+ case 'TK_EQUALS':
+ if (flags.var_line) {
+ // just got an '=' in a var-line, different formatting/line-breaking, etc will now be done
+ flags.var_line_tainted = true;
+ }
+ print_single_space();
+ print_token();
+ print_single_space();
+ break;
+
+ case 'TK_COMMA':
+ if (flags.var_line) {
+ if (is_expression(flags.mode) || last_type === 'TK_END_BLOCK' ) {
+ // do not break on comma, for(var a = 1, b = 2)
+ flags.var_line_tainted = false;
+ }
+ if (flags.var_line_tainted) {
+ print_token();
+ flags.var_line_reindented = true;
+ flags.var_line_tainted = false;
+ print_newline();
+ break;
+ } else {
+ flags.var_line_tainted = false;
+ }
+
+ print_token();
+ print_single_space();
+ break;
+ }
+
+ if (last_type === 'TK_COMMENT') {
+ print_newline();
+ }
+
+ if (last_type === 'TK_END_BLOCK' && flags.mode !== "(EXPRESSION)") {
+ print_token();
+ if (flags.mode === 'OBJECT' && last_text === '}') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ } else {
+ if (flags.mode === 'OBJECT') {
+ print_token();
+ print_newline();
+ } else {
+ // EXPR or DO_BLOCK
+ print_token();
+ print_single_space();
+ }
+ }
+ break;
+
+
+ case 'TK_OPERATOR':
+
+ var space_before = true;
+ var space_after = true;
+
+ if (is_special_word(last_text)) {
+ // "return" had a special handling in TK_WORD. Now we need to return the favor
+ print_single_space();
+ print_token();
+ break;
+ }
+
+ // hack for actionscript's import .*;
+ if (token_text === '*' && last_type === 'TK_UNKNOWN' && !last_last_text.match(/^\d+$/)) {
+ print_token();
+ break;
+ }
+
+ if (token_text === ':' && flags.in_case) {
+ if (opt_indent_case) {
+ flags.case_body = true;
+ }
+ print_token(); // colon really asks for separate treatment
+ print_newline();
+ flags.in_case = false;
+ break;
+ }
+
+ if (token_text === '::') {
+ // no spaces around exotic namespacing syntax operator
+ print_token();
+ break;
+ }
+
+ if (in_array(token_text, ['--', '++', '!']) || (in_array(token_text, ['-', '+']) && (in_array(last_type, ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) || in_array(last_text, line_starters)))) {
+ // unary operators (and binary +/- pretending to be unary) special cases
+
+ space_before = false;
+ space_after = false;
+
+ if (last_text === ';' && is_expression(flags.mode)) {
+ // for (;; ++i)
+ // ^^^
+ space_before = true;
+ }
+ if (last_type === 'TK_WORD' && in_array(last_text, line_starters)) {
+ space_before = true;
+ }
+
+ if (flags.mode === 'BLOCK' && (last_text === '{' || last_text === ';')) {
+ // { foo; --i }
+ // foo(); --bar;
+ print_newline();
+ }
+ } else if (token_text === '.') {
+ // decimal digits or object.property
+ space_before = false;
+
+ } else if (token_text === ':') {
+ if (flags.ternary_depth === 0) {
+ if (flags.mode === 'BLOCK') {
+ flags.mode = 'OBJECT';
+ }
+ space_before = false;
+ } else {
+ flags.ternary_depth -= 1;
+ }
+ } else if (token_text === '?') {
+ flags.ternary_depth += 1;
+ }
+ if (space_before) {
+ print_single_space();
+ }
+
+ print_token();
+
+ if (space_after) {
+ print_single_space();
+ }
+
+ break;
+
+ case 'TK_BLOCK_COMMENT':
+
+ var lines = split_newlines(token_text);
+ var j; // iterator for this case
+
+ if (all_lines_start_with(lines.slice(1), '*')) {
+ // javadoc: reformat and reindent
+ print_newline();
+ output.push(lines[0]);
+ for (j = 1; j < lines.length; j++) {
+ print_newline();
+ output.push(' ');
+ output.push(trim(lines[j]));
+ }
+
+ } else {
+
+ // simple block comment: leave intact
+ if (lines.length > 1) {
+ // multiline comment block starts with a new line
+ print_newline();
+ } else {
+ // single-line /* comment */ stays where it is
+ if (last_type === 'TK_END_BLOCK') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+
+ }
+
+ for (j = 0; j < lines.length; j++) {
+ output.push(lines[j]);
+ output.push("\n");
+ }
+
+ }
+ if (look_up('\n') !== '\n') {
+ print_newline();
+ }
+ break;
+
+ case 'TK_INLINE_COMMENT':
+ print_single_space();
+ print_token();
+ if (is_expression(flags.mode)) {
+ print_single_space();
+ } else {
+ force_newline();
+ }
+ break;
+
+ case 'TK_COMMENT':
+
+ if (last_text === ',' && !wanted_newline) {
+ trim_output(true);
+ }
+ if (last_type !== 'TK_COMMENT') {
+ if (wanted_newline) {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ }
+ print_token();
+ print_newline();
+ break;
+
+ case 'TK_UNKNOWN':
+ if (is_special_word(last_text)) {
+ print_single_space();
+ }
+ print_token();
+ break;
+ }
+
+ last_last_text = last_text;
+ last_type = token_type;
+ last_text = token_text;
+ }
+
+ var sweet_code = preindent_string + output.join('').replace(/[\r\n ]+$/, '');
+ return sweet_code;
+
+}
diff --git a/browser/devtools/shared/LayoutHelpers.jsm b/browser/devtools/shared/LayoutHelpers.jsm
new file mode 100644
index 000000000..0c174b92e
--- /dev/null
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -0,0 +1,384 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
+ return Services.strings.createBundle(
+ "chrome://global-platform/locale/platformKeys.properties");
+});
+
+this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
+
+this.LayoutHelpers = LayoutHelpers = {
+
+ /**
+ * Compute the position and the dimensions for the visible portion
+ * of a node, relativalely to the root window.
+ *
+ * @param nsIDOMNode aNode
+ * a DOM element to be highlighted
+ */
+ getDirtyRect: function LH_getDirectyRect(aNode) {
+ let frameWin = aNode.ownerDocument.defaultView;
+ let clientRect = aNode.getBoundingClientRect();
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ rect = {top: clientRect.top,
+ left: clientRect.left,
+ width: clientRect.width,
+ height: clientRect.height};
+
+ // We iterate through all the parent windows.
+ while (true) {
+
+ // Does the selection overflow on the right of its window?
+ let diffx = frameWin.innerWidth - (rect.left + rect.width);
+ if (diffx < 0) {
+ rect.width += diffx;
+ }
+
+ // Does the selection overflow on the bottom of its window?
+ let diffy = frameWin.innerHeight - (rect.top + rect.height);
+ if (diffy < 0) {
+ rect.height += diffy;
+ }
+
+ // Does the selection overflow on the left of its window?
+ if (rect.left < 0) {
+ rect.width += rect.left;
+ rect.left = 0;
+ }
+
+ // Does the selection overflow on the top of its window?
+ if (rect.top < 0) {
+ rect.height += rect.top;
+ rect.top = 0;
+ }
+
+ // Selection has been clipped to fit in its own window.
+
+ // Are we in the top-level window?
+ if (frameWin.parent === frameWin || !frameWin.frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+ let [offsetTop, offsetLeft] =
+ this.getIframeContentOffset(frameWin.frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+ },
+
+ /**
+ * Compute the absolute position and the dimensions of a node, relativalely
+ * to the root window.
+ *
+ * @param nsIDOMNode aNode
+ * a DOM element to get the bounds for
+ * @param nsIWindow aContentWindow
+ * the content window holding the node
+ */
+ getRect: function LH_getRect(aNode, aContentWindow) {
+ let frameWin = aNode.ownerDocument.defaultView;
+ let clientRect = aNode.getBoundingClientRect();
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ rect = {top: clientRect.top + aContentWindow.pageYOffset,
+ left: clientRect.left + aContentWindow.pageXOffset,
+ width: clientRect.width,
+ height: clientRect.height};
+
+ // We iterate through all the parent windows.
+ while (true) {
+
+ // Are we in the top-level window?
+ if (frameWin.parent === frameWin || !frameWin.frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+ let [offsetTop, offsetLeft] =
+ this.getIframeContentOffset(frameWin.frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+ },
+
+ /**
+ * Returns iframe content offset (iframe border + padding).
+ * Note: this function shouldn't need to exist, had the platform provided a
+ * suitable API for determining the offset between the iframe's content and
+ * its bounding client rect. Bug 626359 should provide us with such an API.
+ *
+ * @param aIframe
+ * The iframe.
+ * @returns array [offsetTop, offsetLeft]
+ * offsetTop is the distance from the top of the iframe and the
+ * top of the content document.
+ * offsetLeft is the distance from the left of the iframe and the
+ * left of the content document.
+ */
+ getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
+ let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+
+ // In some cases, the computed style is null
+ if (!style) {
+ return [0, 0];
+ }
+
+ let paddingTop = parseInt(style.getPropertyValue("padding-top"));
+ let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
+
+ let borderTop = parseInt(style.getPropertyValue("border-top-width"));
+ let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
+
+ return [borderTop + paddingTop, borderLeft + paddingLeft];
+ },
+
+ /**
+ * Apply the page zoom factor.
+ */
+ getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
+ // get page zoom factor, if any
+ let zoom =
+ aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .fullZoom;
+
+ // adjust rect for zoom scaling
+ let aRectScaled = {};
+ for (let prop in aRect) {
+ aRectScaled[prop] = aRect[prop] * zoom;
+ }
+
+ return aRectScaled;
+ },
+
+
+ /**
+ * Find an element from the given coordinates. This method descends through
+ * frames to find the element the user clicked inside frames.
+ *
+ * @param DOMDocument aDocument the document to look into.
+ * @param integer aX
+ * @param integer aY
+ * @returns Node|null the element node found at the given coordinates.
+ */
+ getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
+ let node = aDocument.elementFromPoint(aX, aY);
+ if (node && node.contentDocument) {
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
+ let rect = node.getBoundingClientRect();
+
+ // Gap between the iframe and its content window.
+ let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
+
+ aX -= rect.left + offsetLeft;
+ aY -= rect.top + offsetTop;
+
+ if (aX < 0 || aY < 0) {
+ // Didn't reach the content document, still over the iframe.
+ return node;
+ }
+ }
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
+ node instanceof Ci.nsIDOMHTMLFrameElement) {
+ let subnode = this.getElementFromPoint(node.contentDocument, aX, aY);
+ if (subnode) {
+ node = subnode;
+ }
+ }
+ }
+ return node;
+ },
+
+ /**
+ * Scroll the document so that the element "elem" appears in the viewport.
+ *
+ * @param Element elem the element that needs to appear in the viewport.
+ * @param bool 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.
+ */
+ scrollIntoViewIfNeeded:
+ function LH_scrollIntoViewIfNeeded(elem, centered) {
+ // We want to default to centering the element in the page,
+ // so as to keep the context of the element.
+ centered = centered === undefined? true: !!centered;
+
+ let win = elem.ownerDocument.defaultView;
+ let clientRect = elem.getBoundingClientRect();
+
+ // The following are always from the {top, bottom, left, right}
+ // 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;
+ let leftToRight = clientRect.right;
+ let rightToLeft = clientRect.left - win.innerWidth;
+ let xAllowed = true; // We allow one translation on the x axis,
+ let yAllowed = true; // and one on the y axis.
+
+ // 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 ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) {
+ if (xAllowed) {
+ win.scrollBy(leftToRight - elem.offsetWidth, 0);
+ xAllowed = false;
+ }
+ } else
+ if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) {
+ if (xAllowed) {
+ win.scrollBy(rightToLeft + elem.offsetWidth, 0);
+ xAllowed = 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);
+ }
+
+ if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
+ win.scroll(win.scrollX + clientRect.left
+ - (win.innerWidth - elem.offsetWidth) / 2,
+ win.scrollY);
+ }
+ }
+
+ if (win.parent !== win) {
+ // We are inside an iframe.
+ LH_scrollIntoViewIfNeeded(win.frameElement, centered);
+ }
+ },
+
+ /**
+ * Check if a node and its document are still alive
+ * and attached to the window.
+ *
+ * @param aNode
+ */
+ isNodeConnected: function LH_isNodeConnected(aNode)
+ {
+ try {
+ let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
+ !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
+ aNode.DOCUMENT_POSITION_DISCONNECTED));
+ return connected;
+ } catch (e) {
+ // "can't access dead object" error
+ return false;
+ }
+ },
+
+ /**
+ * Prettifies the modifier keys for an element.
+ *
+ * @param Node aElemKey
+ * The key element to get the modifiers from.
+ * @param boolean aAllowCloverleaf
+ * Pass true to use the cloverleaf symbol instead of a descriptive string.
+ * @return string
+ * A prettified and properly separated modifier keys string.
+ */
+ prettyKey: function LH_prettyKey(aElemKey, aAllowCloverleaf)
+ {
+ let elemString = "";
+ let elemMod = aElemKey.getAttribute("modifiers");
+
+ if (elemMod.match("accel")) {
+ if (Services.appinfo.OS == "Darwin") {
+ // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
+ // Orion adds variable height lines.
+ if (!aAllowCloverleaf) {
+ elemString += "Cmd-";
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_META") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("access")) {
+ if (Services.appinfo.OS == "Darwin") {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("shift")) {
+ elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("alt")) {
+ elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("ctrl") || elemMod.match("control")) {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("meta")) {
+ elemString += PlatformKeys.GetStringFromName("VK_META") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+
+ return elemString +
+ (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
+ aElemKey.getAttribute("key")).toUpperCase();
+ }
+};
diff --git a/browser/devtools/shared/Makefile.in b/browser/devtools/shared/Makefile.in
new file mode 100644
index 000000000..94eba0663
--- /dev/null
+++ b/browser/devtools/shared/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/widgets/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/shared
diff --git a/browser/devtools/shared/Parser.jsm b/browser/devtools/shared/Parser.jsm
new file mode 100644
index 000000000..42b32c07c
--- /dev/null
+++ b/browser/devtools/shared/Parser.jsm
@@ -0,0 +1,2293 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "Reflect", "resource://gre/modules/reflect.jsm");
+
+this.EXPORTED_SYMBOLS = ["Parser"];
+
+/**
+ * A JS parser using the reflection API.
+ */
+this.Parser = function Parser() {
+ this._cache = new Map();
+};
+
+Parser.prototype = {
+ /**
+ * Gets a collection of parser methods for a specified source.
+ *
+ * @param string aUrl [optional]
+ * The source url. The AST nodes will be cached, so you can use this
+ * identifier to avoid parsing the whole source again.
+ * @param string aSource
+ * The source text content.
+ */
+ get: function P_get(aUrl, aSource) {
+ // Try to use the cached AST nodes, to avoid useless parsing operations.
+ if (this._cache.has(aUrl)) {
+ return this._cache.get(aUrl);
+ }
+
+ // The source may not necessarily be JS, in which case we need to extract
+ // all the scripts. Fastest/easiest way is with a regular expression.
+ // Don't worry, the rules of using a <script> tag are really strict,
+ // this will work.
+ let regexp = /<script[^>]*>([^]*?)<\/script\s*>/gim;
+ let syntaxTrees = [];
+ let scriptMatches = [];
+ let scriptMatch;
+
+ if (aSource.match(/^\s*</)) {
+ // First non whitespace character is &lt, so most definitely HTML.
+ while (scriptMatch = regexp.exec(aSource)) {
+ scriptMatches.push(scriptMatch[1]); // Contents are captured at index 1.
+ }
+ }
+
+ // If there are no script matches, send the whole source directly to the
+ // reflection API to generate the AST nodes.
+ if (!scriptMatches.length) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ let nodes = Reflect.parse(aSource);
+ let length = aSource.length;
+ syntaxTrees.push(new SyntaxTree(nodes, aUrl, length));
+ } catch (e) {
+ log(aUrl, e);
+ }
+ }
+ // Generate the AST nodes for each script.
+ else {
+ for (let script of scriptMatches) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ let nodes = Reflect.parse(script);
+ let offset = aSource.indexOf(script);
+ let length = script.length;
+ syntaxTrees.push(new SyntaxTree(nodes, aUrl, length, offset));
+ } catch (e) {
+ log(aUrl, e);
+ }
+ }
+ }
+
+ let pool = new SyntaxTreesPool(syntaxTrees);
+ this._cache.set(aUrl, pool);
+ return pool;
+ },
+
+ /**
+ * Clears all the parsed sources from cache.
+ */
+ clearCache: function P_clearCache() {
+ this._cache.clear();
+ },
+
+ _cache: null
+};
+
+/**
+ * A pool handling a collection of AST nodes generated by the reflection API.
+ *
+ * @param object aSyntaxTrees
+ * A collection of AST nodes generated for a source.
+ */
+function SyntaxTreesPool(aSyntaxTrees) {
+ this._trees = aSyntaxTrees;
+ this._cache = new Map();
+}
+
+SyntaxTreesPool.prototype = {
+ /**
+ * @see SyntaxTree.prototype.getNamedFunctionDefinitions
+ */
+ getNamedFunctionDefinitions: function STP_getNamedFunctionDefinitions(aSubstring) {
+ return this._call("getNamedFunctionDefinitions", aSubstring);
+ },
+
+ /**
+ * @see SyntaxTree.prototype.getFunctionAtLocation
+ */
+ getFunctionAtLocation: function STP_getFunctionAtLocation(aLine, aColumn) {
+ return this._call("getFunctionAtLocation", [aLine, aColumn]);
+ },
+
+ /**
+ * Finds the offset and length of the script containing the specified offset
+ * relative to its parent source.
+ *
+ * @param number aOffset
+ * The offset relative to the parent source.
+ * @return array
+ * The offset and length relative to the enclosing script.
+ */
+ getScriptInfo: function STP_getScriptInfo(aOffset) {
+ for (let { offset, length } of this._trees) {
+ if (offset <= aOffset && offset + length >= aOffset) {
+ return [offset, length];
+ }
+ }
+ return [-1, -1];
+ },
+
+ /**
+ * Handles a request for all known syntax trees.
+ *
+ * @param string aFunction
+ * The function name to call on the SyntaxTree instances.
+ * @param any aParams
+ * Any kind params to pass to the request function.
+ * @return array
+ * The results given by all known syntax trees.
+ */
+ _call: function STP__call(aFunction, aParams) {
+ let results = [];
+ let requestId = aFunction + aParams; // Cache all the things!
+
+ if (this._cache.has(requestId)) {
+ return this._cache.get(requestId);
+ }
+ for (let syntaxTree of this._trees) {
+ try {
+ results.push({
+ sourceUrl: syntaxTree.url,
+ scriptLength: syntaxTree.length,
+ scriptOffset: syntaxTree.offset,
+ parseResults: syntaxTree[aFunction](aParams)
+ });
+ } catch (e) {
+ // Can't guarantee that the tree traversal logic is forever perfect :)
+ // Language features may be added, in which case the recursive methods
+ // need to be updated. If an exception is thrown here, file a bug.
+ log("syntax tree", e);
+ }
+ }
+ this._cache.set(requestId, results);
+ return results;
+ },
+
+ _trees: null,
+ _cache: null
+};
+
+/**
+ * A collection of AST nodes generated by the reflection API.
+ *
+ * @param object aNodes
+ * The AST nodes.
+ * @param string aUrl
+ * The source url.
+ * @param number aLength
+ * The total number of chars of the parsed script in the parent source.
+ * @param number aOffset [optional]
+ * The char offset of the parsed script in the parent source.
+ */
+function SyntaxTree(aNodes, aUrl, aLength, aOffset = 0) {
+ this.AST = aNodes;
+ this.url = aUrl;
+ this.length = aLength;
+ this.offset = aOffset;
+};
+
+SyntaxTree.prototype = {
+ /**
+ * Searches for all function definitions (declarations and expressions)
+ * whose names (or inferred names) contain a string.
+ *
+ * @param string aSubstring
+ * The string to be contained in the function name (or inferred name).
+ * Can be an empty string to match all functions.
+ * @return array
+ * All the matching function declarations and expressions, as
+ * { functionName, functionLocation ... } object hashes.
+ */
+ getNamedFunctionDefinitions: function ST_getNamedFunctionDefinitions(aSubstring) {
+ let lowerCaseToken = aSubstring.toLowerCase();
+ let store = [];
+
+ SyntaxTreeVisitor.walk(this.AST, {
+ /**
+ * Callback invoked for each function declaration node.
+ * @param Node aNode
+ */
+ onFunctionDeclaration: function STW_onFunctionDeclaration(aNode) {
+ let functionName = aNode.id.name;
+ if (functionName.toLowerCase().contains(lowerCaseToken)) {
+ store.push({
+ functionName: functionName,
+ functionLocation: aNode.loc
+ });
+ }
+ },
+
+ /**
+ * Callback invoked for each function expression node.
+ * @param Node aNode
+ */
+ onFunctionExpression: function STW_onFunctionExpression(aNode) {
+ let parent = aNode._parent;
+ let functionName, inferredName, inferredChain, inferredLocation;
+
+ // Function expressions don't necessarily have a name.
+ if (aNode.id) {
+ functionName = aNode.id.name;
+ }
+ // Infer the function's name from an enclosing syntax tree node.
+ if (parent) {
+ let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
+ inferredName = inferredInfo.name;
+ inferredChain = inferredInfo.chain;
+ inferredLocation = inferredInfo.loc;
+ }
+ // Current node may be part of a larger assignment expression stack.
+ if (parent.type == "AssignmentExpression") {
+ this.onFunctionExpression(parent);
+ }
+
+ if ((functionName && functionName.toLowerCase().contains(lowerCaseToken)) ||
+ (inferredName && inferredName.toLowerCase().contains(lowerCaseToken))) {
+ store.push({
+ functionName: functionName,
+ functionLocation: aNode.loc,
+ inferredName: inferredName,
+ inferredChain: inferredChain,
+ inferredLocation: inferredLocation
+ });
+ }
+ },
+
+ /**
+ * Callback invoked for each arrow expression node.
+ * @param Node aNode
+ */
+ onArrowExpression: function STW_onArrowExpression(aNode) {
+ let parent = aNode._parent;
+ let inferredName, inferredChain, inferredLocation;
+
+ // Infer the function's name from an enclosing syntax tree node.
+ let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
+ inferredName = inferredInfo.name;
+ inferredChain = inferredInfo.chain;
+ inferredLocation = inferredInfo.loc;
+
+ // Current node may be part of a larger assignment expression stack.
+ if (parent.type == "AssignmentExpression") {
+ this.onFunctionExpression(parent);
+ }
+
+ if (inferredName && inferredName.toLowerCase().contains(lowerCaseToken)) {
+ store.push({
+ inferredName: inferredName,
+ inferredChain: inferredChain,
+ inferredLocation: inferredLocation
+ });
+ }
+ }
+ });
+
+ return store;
+ },
+
+ /**
+ * Gets the "new" or "call" expression at the specified location.
+ *
+ * @param number aLine
+ * The line in the source.
+ * @param number aColumn
+ * The column in the source.
+ * @return object
+ * An { functionName, functionLocation } object hash,
+ * or null if nothing is found at the specified location.
+ */
+ getFunctionAtLocation: function STW_getFunctionAtLocation([aLine, aColumn]) {
+ let self = this;
+ let func = null;
+
+ SyntaxTreeVisitor.walk(this.AST, {
+ /**
+ * Callback invoked for each node.
+ * @param Node aNode
+ */
+ onNode: function STW_onNode(aNode) {
+ // Make sure the node is part of a branch that's guaranteed to be
+ // hovered. Otherwise, return true to abruptly halt walking this
+ // syntax tree branch. This is a really efficient optimization.
+ return ParserHelpers.isWithinLines(aNode, aLine);
+ },
+
+ /**
+ * Callback invoked for each identifier node.
+ * @param Node aNode
+ */
+ onIdentifier: function STW_onIdentifier(aNode) {
+ // Make sure the identifier itself is hovered.
+ let hovered = ParserHelpers.isWithinBounds(aNode, aLine, aColumn);
+ if (!hovered) {
+ return;
+ }
+
+ // Make sure the identifier is part of a "new" expression or
+ // "call" expression node.
+ let expression = ParserHelpers.getEnclosingFunctionExpression(aNode);
+ if (!expression) {
+ return;
+ }
+
+ // Found an identifier node that is part of a "new" expression or
+ // "call" expression node. However, it may be an argument, not a callee.
+ if (ParserHelpers.isFunctionCalleeArgument(aNode)) {
+ // It's an argument.
+ if (self.functionIdentifiersCache.has(aNode.name)) {
+ // It's a function as an argument.
+ func = {
+ functionName: aNode.name,
+ functionLocation: aNode.loc || aNode._parent.loc
+ };
+ }
+ return;
+ }
+
+ // Found a valid "new" expression or "call" expression node.
+ func = {
+ functionName: aNode.name,
+ functionLocation: ParserHelpers.getFunctionCalleeInfo(expression).loc
+ };
+
+ // Abruptly halt walking the syntax tree.
+ this.break = true;
+ }
+ });
+
+ return func;
+ },
+
+ /**
+ * Gets all the function identifiers in this syntax tree (both the
+ * function names and their inferred names).
+ *
+ * @return array
+ * An array of strings.
+ */
+ get functionIdentifiersCache() {
+ if (this._functionIdentifiersCache) {
+ return this._functionIdentifiersCache;
+ }
+ let functionDefinitions = this.getNamedFunctionDefinitions("");
+ let functionIdentifiers = new Set();
+
+ for (let { functionName, inferredName } of functionDefinitions) {
+ functionIdentifiers.add(functionName);
+ functionIdentifiers.add(inferredName);
+ }
+ return this._functionIdentifiersCache = functionIdentifiers;
+ },
+
+ AST: null,
+ url: "",
+ length: 0,
+ offset: 0
+};
+
+/**
+ * Parser utility methods.
+ */
+let ParserHelpers = {
+ /**
+ * Checks if a node's bounds contains a specified line.
+ *
+ * @param Node aNode
+ * The node's bounds used as reference.
+ * @param number aLine
+ * The line number to check.
+ * @return boolean
+ * True if the line and column is contained in the node's bounds.
+ */
+ isWithinLines: function PH_isWithinLines(aNode, aLine) {
+ // Not all nodes have location information attached.
+ if (!aNode.loc) {
+ return this.isWithinLines(aNode._parent, aLine);
+ }
+ return aNode.loc.start.line <= aLine && aNode.loc.end.line >= aLine;
+ },
+
+ /**
+ * Checks if a node's bounds contains a specified line and column.
+ *
+ * @param Node aNode
+ * The node's bounds used as reference.
+ * @param number aLine
+ * The line number to check.
+ * @param number aColumn
+ * The column number to check.
+ * @return boolean
+ * True if the line and column is contained in the node's bounds.
+ */
+ isWithinBounds: function PH_isWithinBounds(aNode, aLine, aColumn) {
+ // Not all nodes have location information attached.
+ if (!aNode.loc) {
+ return this.isWithinBounds(aNode._parent, aLine, aColumn);
+ }
+ return aNode.loc.start.line == aLine && aNode.loc.end.line == aLine &&
+ aNode.loc.start.column <= aColumn && aNode.loc.end.column >= aColumn;
+ },
+
+ /**
+ * Try to infer a function expression's name & other details based on the
+ * enclosing VariableDeclarator, AssignmentExpression or ObjectExpression node.
+ *
+ * @param Node aNode
+ * The function expression node to get the name for.
+ * @return object
+ * The inferred function name, or empty string can't infer name,
+ * along with the chain (a generic "context", like a prototype chain)
+ * and location if available.
+ */
+ inferFunctionExpressionInfo: function PH_inferFunctionExpressionInfo(aNode) {
+ let parent = aNode._parent;
+
+ // A function expression may be defined in a variable declarator,
+ // e.g. var foo = function(){}, in which case it is possible to infer
+ // the variable name.
+ if (parent.type == "VariableDeclarator") {
+ return {
+ name: parent.id.name,
+ chain: null,
+ loc: parent.loc
+ };
+ }
+
+ // Function expressions can also be defined in assignment expressions,
+ // e.g. foo = function(){} or foo.bar = function(){}, in which case it is
+ // possible to infer the assignee name ("foo" and "bar" respectively).
+ if (parent.type == "AssignmentExpression") {
+ let assigneeChain = this.getAssignmentExpressionAssigneeChain(parent);
+ let assigneeLeaf = assigneeChain.pop();
+ return {
+ name: assigneeLeaf,
+ chain: assigneeChain,
+ loc: parent.left.loc
+ };
+ }
+
+ // If a function expression is defined in an object expression,
+ // e.g. { foo: function(){} }, then it is possible to infer the name
+ // from the corresponding property.
+ if (parent.type == "ObjectExpression") {
+ let propertyDetails = this.getObjectExpressionPropertyKeyForValue(aNode);
+ let propertyChain = this.getObjectExpressionPropertyChain(parent);
+ let propertyLeaf = propertyDetails.name;
+ return {
+ name: propertyLeaf,
+ chain: propertyChain,
+ loc: propertyDetails.loc
+ };
+ }
+
+ // Can't infer the function expression's name.
+ return {
+ name: "",
+ chain: null,
+ loc: null
+ };
+ },
+
+ /**
+ * Gets details about an object expression's property to which a specified
+ * value is assigned. For example, the node returned for the value 42 in
+ * "{ foo: { bar: 42 } }" is "bar".
+ *
+ * @param Node aNode
+ * The value node assigned to a property in an object expression.
+ * @return object
+ * The details about the assignee property node.
+ */
+ getObjectExpressionPropertyKeyForValue:
+ function PH_getObjectExpressionPropertyKeyForValue(aNode) {
+ let parent = aNode._parent;
+ if (parent.type != "ObjectExpression") {
+ return null;
+ }
+ for (let property of parent.properties) {
+ if (property.value == aNode) {
+ return property.key;
+ }
+ }
+ },
+
+ /**
+ * Gets an object expression property chain to its parent variable declarator.
+ * For example, the chain to "baz" in "foo = { bar: { baz: { } } }" is
+ * ["foo", "bar", "baz"].
+ *
+ * @param Node aNode
+ * The object expression node to begin the scan from.
+ * @param array aStore [optional]
+ * The chain to store the nodes into.
+ * @return array
+ * The chain to the parent variable declarator, as strings.
+ */
+ getObjectExpressionPropertyChain:
+ function PH_getObjectExpressionPropertyChain(aNode, aStore = []) {
+ switch (aNode.type) {
+ case "ObjectExpression":
+ this.getObjectExpressionPropertyChain(aNode._parent, aStore);
+
+ let propertyDetails = this.getObjectExpressionPropertyKeyForValue(aNode);
+ if (propertyDetails) {
+ aStore.push(this.getObjectExpressionPropertyKeyForValue(aNode).name);
+ }
+ break;
+ // Handle "foo.bar = { ... }" since it's commonly used when defining an
+ // object's prototype methods; for example: "Foo.prototype = { ... }".
+ case "AssignmentExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode, aStore);
+ break;
+ // Additionally handle stuff like "foo = bar.baz({ ... })", because it's
+ // commonly used in prototype-based inheritance in many libraries;
+ // for example: "Foo.Bar = Baz.extend({ ... })".
+ case "NewExpression":
+ case "CallExpression":
+ this.getObjectExpressionPropertyChain(aNode._parent, aStore);
+ break;
+ // End of the chain.
+ case "VariableDeclarator":
+ aStore.push(aNode.id.name);
+ break;
+ }
+ return aStore;
+ },
+
+ /**
+ * Gets the assignee property chain in an assignment expression.
+ * For example, the chain in "foo.bar.baz = 42" is ["foo", "bar", "baz"].
+ *
+ * @param Node aNode
+ * The assignment expression node to begin the scan from.
+ * @param array aStore
+ * The chain to store the nodes into.
+ * @param array aStore [optional]
+ * The chain to store the nodes into.
+ * @return array
+ * The full assignee chain, as strings.
+ */
+ getAssignmentExpressionAssigneeChain:
+ function PH_getAssignmentExpressionAssigneeChain(aNode, aStore = []) {
+ switch (aNode.type) {
+ case "AssignmentExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode.left, aStore);
+ break;
+ case "MemberExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode.object, aStore);
+ this.getAssignmentExpressionAssigneeChain(aNode.property, aStore);
+ break;
+ case "ThisExpression":
+ // Such expressions may appear in an assignee chain, for example
+ // "this.foo.bar = baz", however it seems better to ignore such nodes
+ // and limit the chain to ["foo", "bar"].
+ break;
+ case "Identifier":
+ aStore.push(aNode.name);
+ break;
+ }
+ return aStore;
+ },
+
+ /**
+ * Gets the "new" expression or "call" expression containing the specified
+ * node. If the node is not enclosed in either of these expression types,
+ * null is returned.
+ *
+ * @param Node aNode
+ * The child node of an enclosing "new" expression or "call" expression.
+ * @return Node
+ * The enclosing "new" expression or "call" expression node, or
+ * null if nothing is found.
+ */
+ getEnclosingFunctionExpression:
+ function PH_getEnclosingFunctionExpression(aNode) {
+ switch (aNode.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return aNode;
+ case "MemberExpression":
+ case "Identifier":
+ return this.getEnclosingFunctionExpression(aNode._parent);
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Gets the name and { line, column } location of a "new" expression or
+ * "call" expression's callee node.
+ *
+ * @param Node aNode
+ * The "new" expression or "call" expression to get the callee info for.
+ * @return object
+ * An object containing the name and location as properties, or
+ * null if nothing is found.
+ */
+ getFunctionCalleeInfo: function PH_getFunctionCalleeInfo(aNode) {
+ switch (aNode.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return this.getFunctionCalleeInfo(aNode.callee);
+ case "MemberExpression":
+ return this.getFunctionCalleeInfo(aNode.property);
+ case "Identifier":
+ return {
+ name: aNode.name,
+ loc: aNode.loc || (aNode._parent || {}).loc
+ };
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Determines if an identifier node is part of a "new" expression or
+ * "call" expression's callee arguments.
+ *
+ * @param Node aNode
+ * The node to determine if part of a function's arguments.
+ * @return boolean
+ * True if the identifier is an argument, false otherwise.
+ */
+ isFunctionCalleeArgument: function PH_isFunctionCalleeArgument(aNode) {
+ if (!aNode._parent) {
+ return false;
+ }
+ switch (aNode._parent.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return aNode._parent.arguments.indexOf(aNode) != -1;
+ default:
+ return this.isFunctionCalleeArgument(aNode._parent);
+ }
+ }
+};
+
+/**
+ * A visitor for a syntax tree generated by the reflection API.
+ * See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API.
+ *
+ * All node types implement the following interface:
+ * interface Node {
+ * type: string;
+ * loc: SourceLocation | null;
+ * }
+ */
+let SyntaxTreeVisitor = {
+ /**
+ * Walks a syntax tree.
+ *
+ * @param object aTree
+ * The AST nodes generated by the reflection API
+ * @param object aCallbacks
+ * A map of all the callbacks to invoke when passing through certain
+ * types of noes (e.g: onFunctionDeclaration, onBlockStatement etc.).
+ */
+ walk: function STV_walk(aTree, aCallbacks) {
+ this[aTree.type](aTree, aCallbacks);
+ },
+
+ /**
+ * A flag checked on each node in the syntax tree. If true, walking is
+ * abruptly halted.
+ */
+ break: false,
+
+ /**
+ * A complete program source tree.
+ *
+ * interface Program <: Node {
+ * type: "Program";
+ * body: [ Statement ];
+ * }
+ */
+ Program: function STV_Program(aNode, aCallbacks) {
+ if (aCallbacks.onProgram) {
+ aCallbacks.onProgram(aNode);
+ }
+ for (let statement of aNode.body) {
+ this[statement.type](statement, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * Any statement.
+ *
+ * interface Statement <: Node { }
+ */
+ Statement: function STV_Statement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onStatement) {
+ aCallbacks.onStatement(aNode);
+ }
+ },
+
+ /**
+ * An empty statement, i.e., a solitary semicolon.
+ *
+ * interface EmptyStatement <: Statement {
+ * type: "EmptyStatement";
+ * }
+ */
+ EmptyStatement: function STV_EmptyStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onEmptyStatement) {
+ aCallbacks.onEmptyStatement(aNode);
+ }
+ },
+
+ /**
+ * A block statement, i.e., a sequence of statements surrounded by braces.
+ *
+ * interface BlockStatement <: Statement {
+ * type: "BlockStatement";
+ * body: [ Statement ];
+ * }
+ */
+ BlockStatement: function STV_BlockStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBlockStatement) {
+ aCallbacks.onBlockStatement(aNode);
+ }
+ for (let statement of aNode.body) {
+ this[statement.type](statement, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An expression statement, i.e., a statement consisting of a single expression.
+ *
+ * interface ExpressionStatement <: Statement {
+ * type: "ExpressionStatement";
+ * expression: Expression;
+ * }
+ */
+ ExpressionStatement: function STV_ExpressionStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onExpressionStatement) {
+ aCallbacks.onExpressionStatement(aNode);
+ }
+ this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
+ },
+
+ /**
+ * An if statement.
+ *
+ * interface IfStatement <: Statement {
+ * type: "IfStatement";
+ * test: Expression;
+ * consequent: Statement;
+ * alternate: Statement | null;
+ * }
+ */
+ IfStatement: function STV_IfStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onIfStatement) {
+ aCallbacks.onIfStatement(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
+ if (aNode.alternate) {
+ this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A labeled statement, i.e., a statement prefixed by a break/continue label.
+ *
+ * interface LabeledStatement <: Statement {
+ * type: "LabeledStatement";
+ * label: Identifier;
+ * body: Statement;
+ * }
+ */
+ LabeledStatement: function STV_LabeledStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLabeledStatement) {
+ aCallbacks.onLabeledStatement(aNode);
+ }
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A break statement.
+ *
+ * interface BreakStatement <: Statement {
+ * type: "BreakStatement";
+ * label: Identifier | null;
+ * }
+ */
+ BreakStatement: function STV_BreakStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBreakStatement) {
+ aCallbacks.onBreakStatement(aNode);
+ }
+ if (aNode.label) {
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A continue statement.
+ *
+ * interface ContinueStatement <: Statement {
+ * type: "ContinueStatement";
+ * label: Identifier | null;
+ * }
+ */
+ ContinueStatement: function STV_ContinueStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onContinueStatement) {
+ aCallbacks.onContinueStatement(aNode);
+ }
+ if (aNode.label) {
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A with statement.
+ *
+ * interface WithStatement <: Statement {
+ * type: "WithStatement";
+ * object: Expression;
+ * body: Statement;
+ * }
+ */
+ WithStatement: function STV_WithStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onWithStatement) {
+ aCallbacks.onWithStatement(aNode);
+ }
+ this[aNode.object.type](aNode.object, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A switch statement. The lexical flag is metadata indicating whether the
+ * switch statement contains any unnested let declarations (and therefore
+ * introduces a new lexical scope).
+ *
+ * interface SwitchStatement <: Statement {
+ * type: "SwitchStatement";
+ * discriminant: Expression;
+ * cases: [ SwitchCase ];
+ * lexical: boolean;
+ * }
+ */
+ SwitchStatement: function STV_SwitchStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSwitchStatement) {
+ aCallbacks.onSwitchStatement(aNode);
+ }
+ this[aNode.discriminant.type](aNode.discriminant, aNode, aCallbacks);
+ for (let _case of aNode.cases) {
+ this[_case.type](_case, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A return statement.
+ *
+ * interface ReturnStatement <: Statement {
+ * type: "ReturnStatement";
+ * argument: Expression | null;
+ * }
+ */
+ ReturnStatement: function STV_ReturnStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onReturnStatement) {
+ aCallbacks.onReturnStatement(aNode);
+ }
+ if (aNode.argument) {
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A throw statement.
+ *
+ * interface ThrowStatement <: Statement {
+ * type: "ThrowStatement";
+ * argument: Expression;
+ * }
+ */
+ ThrowStatement: function STV_ThrowStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onThrowStatement) {
+ aCallbacks.onThrowStatement(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A try statement.
+ *
+ * interface TryStatement <: Statement {
+ * type: "TryStatement";
+ * block: BlockStatement;
+ * handler: CatchClause | null;
+ * guardedHandlers: [ CatchClause ];
+ * finalizer: BlockStatement | null;
+ * }
+ */
+ TryStatement: function STV_TryStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onTryStatement) {
+ aCallbacks.onTryStatement(aNode);
+ }
+ this[aNode.block.type](aNode.block, aNode, aCallbacks);
+ if (aNode.handler) {
+ this[aNode.handler.type](aNode.handler, aNode, aCallbacks);
+ }
+ for (let guardedHandler of aNode.guardedHandlers) {
+ this[guardedHandler.type](guardedHandler, aNode, aCallbacks);
+ }
+ if (aNode.finalizer) {
+ this[aNode.finalizer.type](aNode.finalizer, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A while statement.
+ *
+ * interface WhileStatement <: Statement {
+ * type: "WhileStatement";
+ * test: Expression;
+ * body: Statement;
+ * }
+ */
+ WhileStatement: function STV_WhileStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onWhileStatement) {
+ aCallbacks.onWhileStatement(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A do/while statement.
+ *
+ * interface DoWhileStatement <: Statement {
+ * type: "DoWhileStatement";
+ * body: Statement;
+ * test: Expression;
+ * }
+ */
+ DoWhileStatement: function STV_DoWhileStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDoWhileStatement) {
+ aCallbacks.onDoWhileStatement(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ },
+
+ /**
+ * A for statement.
+ *
+ * interface ForStatement <: Statement {
+ * type: "ForStatement";
+ * init: VariableDeclaration | Expression | null;
+ * test: Expression | null;
+ * update: Expression | null;
+ * body: Statement;
+ * }
+ */
+ ForStatement: function STV_ForStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForStatement) {
+ aCallbacks.onForStatement(aNode);
+ }
+ if (aNode.init) {
+ this[aNode.init.type](aNode.init, aNode, aCallbacks);
+ }
+ if (aNode.test) {
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ }
+ if (aNode.update) {
+ this[aNode.update.type](aNode.update, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for/in statement, or, if each is true, a for each/in statement.
+ *
+ * interface ForInStatement <: Statement {
+ * type: "ForInStatement";
+ * left: VariableDeclaration | Expression;
+ * right: Expression;
+ * body: Statement;
+ * each: boolean;
+ * }
+ */
+ ForInStatement: function STV_ForInStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForInStatement) {
+ aCallbacks.onForInStatement(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for/of statement.
+ *
+ * interface ForOfStatement <: Statement {
+ * type: "ForOfStatement";
+ * left: VariableDeclaration | Expression;
+ * right: Expression;
+ * body: Statement;
+ * }
+ */
+ ForOfStatement: function STV_ForOfStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForOfStatement) {
+ aCallbacks.onForOfStatement(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A let statement.
+ *
+ * interface LetStatement <: Statement {
+ * type: "LetStatement";
+ * head: [ { id: Pattern, init: Expression | null } ];
+ * body: Statement;
+ * }
+ */
+ LetStatement: function STV_LetStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLetStatement) {
+ aCallbacks.onLetStatement(aNode);
+ }
+ for (let { id, init } of aNode.head) {
+ this[id.type](id, aNode, aCallbacks);
+ if (init) {
+ this[init.type](init, aNode, aCallbacks);
+ }
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A debugger statement.
+ *
+ * interface DebuggerStatement <: Statement {
+ * type: "DebuggerStatement";
+ * }
+ */
+ DebuggerStatement: function STV_DebuggerStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDebuggerStatement) {
+ aCallbacks.onDebuggerStatement(aNode);
+ }
+ },
+
+ /**
+ * Any declaration node. Note that declarations are considered statements;
+ * this is because declarations can appear in any statement context in the
+ * language recognized by the SpiderMonkey parser.
+ *
+ * interface Declaration <: Statement { }
+ */
+ Declaration: function STV_Declaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDeclaration) {
+ aCallbacks.onDeclaration(aNode);
+ }
+ },
+
+ /**
+ * A function declaration.
+ *
+ * interface FunctionDeclaration <: Function, Declaration {
+ * type: "FunctionDeclaration";
+ * id: Identifier;
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ FunctionDeclaration: function STV_FunctionDeclaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onFunctionDeclaration) {
+ aCallbacks.onFunctionDeclaration(aNode);
+ }
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A variable declaration, via one of var, let, or const.
+ *
+ * interface VariableDeclaration <: Declaration {
+ * type: "VariableDeclaration";
+ * declarations: [ VariableDeclarator ];
+ * kind: "var" | "let" | "const";
+ * }
+ */
+ VariableDeclaration: function STV_VariableDeclaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onVariableDeclaration) {
+ aCallbacks.onVariableDeclaration(aNode);
+ }
+ for (let declaration of aNode.declarations) {
+ this[declaration.type](declaration, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A variable declarator.
+ *
+ * interface VariableDeclarator <: Node {
+ * type: "VariableDeclarator";
+ * id: Pattern;
+ * init: Expression | null;
+ * }
+ */
+ VariableDeclarator: function STV_VariableDeclarator(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onVariableDeclarator) {
+ aCallbacks.onVariableDeclarator(aNode);
+ }
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ if (aNode.init) {
+ this[aNode.init.type](aNode.init, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * Any expression node. Since the left-hand side of an assignment may be any
+ * expression in general, an expression can also be a pattern.
+ *
+ * interface Expression <: Node, Pattern { }
+ */
+ Expression: function STV_Expression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onExpression) {
+ aCallbacks.onExpression(aNode);
+ }
+ },
+
+ /**
+ * A this expression.
+ *
+ * interface ThisExpression <: Expression {
+ * type: "ThisExpression";
+ * }
+ */
+ ThisExpression: function STV_ThisExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onThisExpression) {
+ aCallbacks.onThisExpression(aNode);
+ }
+ },
+
+ /**
+ * An array expression.
+ *
+ * interface ArrayExpression <: Expression {
+ * type: "ArrayExpression";
+ * elements: [ Expression | null ];
+ * }
+ */
+ ArrayExpression: function STV_ArrayExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrayExpression) {
+ aCallbacks.onArrayExpression(aNode);
+ }
+ for (let element of aNode.elements) {
+ if (element) {
+ this[element.type](element, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * An object expression. A literal property in an object expression can have
+ * either a string or number as its value. Ordinary property initializers
+ * have a kind value "init"; getters and setters have the kind values "get"
+ * and "set", respectively.
+ *
+ * interface ObjectExpression <: Expression {
+ * type: "ObjectExpression";
+ * properties: [ { key: Literal | Identifier,
+ * value: Expression,
+ * kind: "init" | "get" | "set" } ];
+ * }
+ */
+ ObjectExpression: function STV_ObjectExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onObjectExpression) {
+ aCallbacks.onObjectExpression(aNode);
+ }
+ for (let { key, value } of aNode.properties) {
+ this[key.type](key, aNode, aCallbacks);
+ this[value.type](value, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A function expression.
+ *
+ * interface FunctionExpression <: Function, Expression {
+ * type: "FunctionExpression";
+ * id: Identifier | null;
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ FunctionExpression: function STV_FunctionExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onFunctionExpression) {
+ aCallbacks.onFunctionExpression(aNode);
+ }
+ if (aNode.id) {
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ }
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * An arrow expression.
+ *
+ * interface ArrowExpression <: Function, Expression {
+ * type: "ArrowExpression";
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ ArrowExpression: function STV_ArrowExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrowExpression) {
+ aCallbacks.onArrowExpression(aNode);
+ }
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A sequence expression, i.e., a comma-separated sequence of expressions.
+ *
+ * interface SequenceExpression <: Expression {
+ * type: "SequenceExpression";
+ * expressions: [ Expression ];
+ * }
+ */
+ SequenceExpression: function STV_SequenceExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSequenceExpression) {
+ aCallbacks.onSequenceExpression(aNode);
+ }
+ for (let expression of aNode.expressions) {
+ this[expression.type](expression, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A unary operator expression.
+ *
+ * interface UnaryExpression <: Expression {
+ * type: "UnaryExpression";
+ * operator: UnaryOperator;
+ * prefix: boolean;
+ * argument: Expression;
+ * }
+ */
+ UnaryExpression: function STV_UnaryExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onUnaryExpression) {
+ aCallbacks.onUnaryExpression(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A binary operator expression.
+ *
+ * interface BinaryExpression <: Expression {
+ * type: "BinaryExpression";
+ * operator: BinaryOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ BinaryExpression: function STV_BinaryExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBinaryExpression) {
+ aCallbacks.onBinaryExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An assignment operator expression.
+ *
+ * interface AssignmentExpression <: Expression {
+ * type: "AssignmentExpression";
+ * operator: AssignmentOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ AssignmentExpression: function STV_AssignmentExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onAssignmentExpression) {
+ aCallbacks.onAssignmentExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An update (increment or decrement) operator expression.
+ *
+ * interface UpdateExpression <: Expression {
+ * type: "UpdateExpression";
+ * operator: UpdateOperator;
+ * argument: Expression;
+ * prefix: boolean;
+ * }
+ */
+ UpdateExpression: function STV_UpdateExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onUpdateExpression) {
+ aCallbacks.onUpdateExpression(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A logical operator expression.
+ *
+ * interface LogicalExpression <: Expression {
+ * type: "LogicalExpression";
+ * operator: LogicalOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ LogicalExpression: function STV_LogicalExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLogicalExpression) {
+ aCallbacks.onLogicalExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * A conditional expression, i.e., a ternary ?/: expression.
+ *
+ * interface ConditionalExpression <: Expression {
+ * type: "ConditionalExpression";
+ * test: Expression;
+ * alternate: Expression;
+ * consequent: Expression;
+ * }
+ */
+ ConditionalExpression: function STV_ConditionalExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onConditionalExpression) {
+ aCallbacks.onConditionalExpression(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
+ this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
+ },
+
+ /**
+ * A new expression.
+ *
+ * interface NewExpression <: Expression {
+ * type: "NewExpression";
+ * callee: Expression;
+ * arguments: [ Expression | null ];
+ * }
+ */
+ NewExpression: function STV_NewExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onNewExpression) {
+ aCallbacks.onNewExpression(aNode);
+ }
+ this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
+ for (let argument of aNode.arguments) {
+ if (argument) {
+ this[argument.type](argument, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A function or method call expression.
+ *
+ * interface CallExpression <: Expression {
+ * type: "CallExpression";
+ * callee: Expression;
+ * arguments: [ Expression | null ];
+ * }
+ */
+ CallExpression: function STV_CallExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onCallExpression) {
+ aCallbacks.onCallExpression(aNode);
+ }
+ this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
+ for (let argument of aNode.arguments) {
+ if (argument) {
+ this[argument.type](argument, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A member expression. If computed is true, the node corresponds to a
+ * computed e1[e2] expression and property is an Expression. If computed is
+ * false, the node corresponds to a static e1.x expression and property is an
+ * Identifier.
+ *
+ * interface MemberExpression <: Expression {
+ * type: "MemberExpression";
+ * object: Expression;
+ * property: Identifier | Expression;
+ * computed: boolean;
+ * }
+ */
+ MemberExpression: function STV_MemberExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onMemberExpression) {
+ aCallbacks.onMemberExpression(aNode);
+ }
+ this[aNode.object.type](aNode.object, aNode, aCallbacks);
+ this[aNode.property.type](aNode.property, aNode, aCallbacks);
+ },
+
+ /**
+ * A yield expression.
+ *
+ * interface YieldExpression <: Expression {
+ * argument: Expression | null;
+ * }
+ */
+ YieldExpression: function STV_YieldExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onYieldExpression) {
+ aCallbacks.onYieldExpression(aNode);
+ }
+ if (aNode.argument) {
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An array comprehension. The blocks array corresponds to the sequence of
+ * for and for each blocks. The optional filter expression corresponds to the
+ * final if clause, if present.
+ *
+ * interface ComprehensionExpression <: Expression {
+ * body: Expression;
+ * blocks: [ ComprehensionBlock ];
+ * filter: Expression | null;
+ * }
+ */
+ ComprehensionExpression: function STV_ComprehensionExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onComprehensionExpression) {
+ aCallbacks.onComprehensionExpression(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ for (let block of aNode.blocks) {
+ this[block.type](block, aNode, aCallbacks);
+ }
+ if (aNode.filter) {
+ this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A generator expression. As with array comprehensions, the blocks array
+ * corresponds to the sequence of for and for each blocks, and the optional
+ * filter expression corresponds to the final if clause, if present.
+ *
+ * interface GeneratorExpression <: Expression {
+ * body: Expression;
+ * blocks: [ ComprehensionBlock ];
+ * filter: Expression | null;
+ * }
+ */
+ GeneratorExpression: function STV_GeneratorExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGeneratorExpression) {
+ aCallbacks.onGeneratorExpression(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ for (let block of aNode.blocks) {
+ this[block.type](block, aNode, aCallbacks);
+ }
+ if (aNode.filter) {
+ this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A graph expression, aka "sharp literal," such as #1={ self: #1# }.
+ *
+ * interface GraphExpression <: Expression {
+ * index: uint32;
+ * expression: Literal;
+ * }
+ */
+ GraphExpression: function STV_GraphExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGraphExpression) {
+ aCallbacks.onGraphExpression(aNode);
+ }
+ this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
+ },
+
+ /**
+ * A graph index expression, aka "sharp variable," such as #1#.
+ *
+ * interface GraphIndexExpression <: Expression {
+ * index: uint32;
+ * }
+ */
+ GraphIndexExpression: function STV_GraphIndexExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGraphIndexExpression) {
+ aCallbacks.onGraphIndexExpression(aNode);
+ }
+ },
+
+ /**
+ * A let expression.
+ *
+ * interface LetExpression <: Expression {
+ * type: "LetExpression";
+ * head: [ { id: Pattern, init: Expression | null } ];
+ * body: Expression;
+ * }
+ */
+ LetExpression: function STV_LetExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLetExpression) {
+ aCallbacks.onLetExpression(aNode);
+ }
+ for (let { id, init } of aNode.head) {
+ this[id.type](id, aNode, aCallbacks);
+ if (init) {
+ this[init.type](init, aNode, aCallbacks);
+ }
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * Any pattern.
+ *
+ * interface Pattern <: Node { }
+ */
+ Pattern: function STV_Pattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onPattern) {
+ aCallbacks.onPattern(aNode);
+ }
+ },
+
+ /**
+ * An object-destructuring pattern. A literal property in an object pattern
+ * can have either a string or number as its value.
+ *
+ * interface ObjectPattern <: Pattern {
+ * type: "ObjectPattern";
+ * properties: [ { key: Literal | Identifier, value: Pattern } ];
+ * }
+ */
+ ObjectPattern: function STV_ObjectPattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onObjectPattern) {
+ aCallbacks.onObjectPattern(aNode);
+ }
+ for (let { key, value } of aNode.properties) {
+ this[key.type](key, aNode, aCallbacks);
+ this[value.type](value, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An array-destructuring pattern.
+ *
+ * interface ArrayPattern <: Pattern {
+ * type: "ArrayPattern";
+ * elements: [ Pattern | null ];
+ * }
+ */
+ ArrayPattern: function STV_ArrayPattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrayPattern) {
+ aCallbacks.onArrayPattern(aNode);
+ }
+ for (let element of aNode.elements) {
+ if (element) {
+ this[element.type](element, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A case (if test is an Expression) or default (if test is null) clause in
+ * the body of a switch statement.
+ *
+ * interface SwitchCase <: Node {
+ * type: "SwitchCase";
+ * test: Expression | null;
+ * consequent: [ Statement ];
+ * }
+ */
+ SwitchCase: function STV_SwitchCase(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSwitchCase) {
+ aCallbacks.onSwitchCase(aNode);
+ }
+ if (aNode.test) {
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ }
+ for (let consequent of aNode.consequent) {
+ this[consequent.type](consequent, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A catch clause following a try block. The optional guard property
+ * corresponds to the optional expression guard on the bound variable.
+ *
+ * interface CatchClause <: Node {
+ * type: "CatchClause";
+ * param: Pattern;
+ * guard: Expression | null;
+ * body: BlockStatement;
+ * }
+ */
+ CatchClause: function STV_CatchClause(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onCatchClause) {
+ aCallbacks.onCatchClause(aNode);
+ }
+ this[aNode.param.type](aNode.param, aNode, aCallbacks);
+ if (aNode.guard) {
+ this[aNode.guard.type](aNode.guard, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for or for each block in an array comprehension or generator expression.
+ *
+ * interface ComprehensionBlock <: Node {
+ * left: Pattern;
+ * right: Expression;
+ * each: boolean;
+ * }
+ */
+ ComprehensionBlock: function STV_ComprehensionBlock(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onComprehensionBlock) {
+ aCallbacks.onComprehensionBlock(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An identifier. Note that an identifier may be an expression or a
+ * destructuring pattern.
+ *
+ * interface Identifier <: Node, Expression, Pattern {
+ * type: "Identifier";
+ * name: string;
+ * }
+ */
+ Identifier: function STV_Identifier(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onIdentifier) {
+ aCallbacks.onIdentifier(aNode);
+ }
+ },
+
+ /**
+ * A literal token. Note that a literal can be an expression.
+ *
+ * interface Literal <: Node, Expression {
+ * type: "Literal";
+ * value: string | boolean | null | number | RegExp;
+ * }
+ */
+ Literal: function STV_Literal(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLiteral) {
+ aCallbacks.onLiteral(aNode);
+ }
+ }
+};
+
+/**
+ * Logs a warning.
+ *
+ * @param string aStr
+ * The message to be displayed.
+ * @param Exception aEx
+ * The thrown exception.
+ */
+function log(aStr, aEx) {
+ let msg = "Warning: " + aStr + ", " + aEx + "\n" + aEx.stack;
+ Cu.reportError(msg);
+ dump(msg + "\n");
+};
+
+XPCOMUtils.defineLazyGetter(Parser, "reflectionAPI", function() Reflect);
diff --git a/browser/devtools/shared/SplitView.jsm b/browser/devtools/shared/SplitView.jsm
new file mode 100644
index 000000000..ec8376f45
--- /dev/null
+++ b/browser/devtools/shared/SplitView.jsm
@@ -0,0 +1,302 @@
+/* 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 = ["SplitView"];
+
+/* this must be kept in sync with CSS (ie. splitview.css) */
+const LANDSCAPE_MEDIA_QUERY = "(min-width: 551px)";
+
+let 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", function onKeyCatchAll(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 == aEvent.DOM_VK_PAGE_UP ||
+ aEvent.keyCode == aEvent.DOM_VK_HOME) {
+ newFocusOrdinal = 0;
+ } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
+ aEvent.keyCode == aEvent.DOM_VK_END) {
+ newFocusOrdinal = this._nav.childNodes.length - 1;
+ } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal--;
+ } else if (aEvent.keyCode == aEvent.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;
+ }
+ }.bind(this), false);
+}
+
+SplitView.prototype = {
+ /**
+ * Retrieve whether the UI currently has a landscape orientation.
+ *
+ * @return boolean
+ */
+ get isLandscape() this._mql.matches,
+
+ /**
+ * Retrieve the root element.
+ *
+ * @return DOMElement
+ */
+ get rootElement() this._root,
+
+ /**
+ * Retrieve the active item's summary element or null if there is none.
+ *
+ * @return DOMElement
+ */
+ get activeSummary() 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", function onSummaryClick(aEvent) {
+ aEvent.stopPropagation();
+ this.activeSummary = aSummary;
+ }.bind(this), false);
+
+ this._side.appendChild(aDetails);
+
+ if (binding.onCreate) {
+ // queue onCreate handler
+ this._root.ownerDocument.defaultView.setTimeout(function () {
+ binding.onCreate(aSummary, aDetails, binding.data);
+ }, 0);
+ }
+ },
+
+ /**
+ * 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/browser/devtools/shared/event-emitter.js b/browser/devtools/shared/event-emitter.js
new file mode 100644
index 000000000..44f5fd5e3
--- /dev/null
+++ b/browser/devtools/shared/event-emitter.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/. */
+
+/**
+ * EventEmitter.
+ */
+
+this.EventEmitter = function EventEmitter() {};
+
+if (typeof(require) === "function") {
+ module.exports = EventEmitter;
+ var {Cu} = require("chrome");
+} else {
+ var EXPORTED_SYMBOLS = ["EventEmitter"];
+ var Cu = this["Components"].utils;
+}
+
+/**
+ * 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) {
+ let 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
+ * Called when the event is fired. Will be called at most one time.
+ */
+ once: function EventEmitter_once(aEvent, aListener) {
+ let handler = function() {
+ this.off(aEvent, handler);
+ aListener.apply(null, arguments);
+ }.bind(this);
+ this.on(aEvent, handler);
+ },
+
+ /**
+ * 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;
+ let listeners = this._eventEmitterListeners.get(aEvent);
+ if (listeners) {
+ this._eventEmitterListeners.set(aEvent, listeners.filter(function(l) aListener != l));
+ }
+ },
+
+ /**
+ * Emit an event. All arguments to this method will
+ * be sent to listner functions.
+ */
+ emit: function EventEmitter_emit(aEvent) {
+ if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent))
+ return;
+
+ let originalListeners = this._eventEmitterListeners.get(aEvent);
+ for (let listener of this._eventEmitterListeners.get(aEvent)) {
+ // If the object was destroyed during event emission, stop
+ // emitting.
+ if (!this._eventEmitterListeners) {
+ break;
+ }
+
+ // If listeners were removed during emission, make sure the
+ // event handler we're going to fire wasn't removed.
+ if (originalListeners === this._eventEmitterListeners.get(aEvent) ||
+ this._eventEmitterListeners.get(aEvent).some(function(l) l === listener)) {
+ try {
+ listener.apply(null, arguments);
+ }
+ catch (ex) {
+ // Prevent a bad listener from interfering with the others.
+ let msg = ex + ": " + ex.stack;
+ Cu.reportError(msg);
+ dump(msg + "\n");
+ }
+ }
+ }
+ }
+};
diff --git a/browser/devtools/shared/inplace-editor.js b/browser/devtools/shared/inplace-editor.js
new file mode 100644
index 000000000..cc5d28038
--- /dev/null
+++ b/browser/devtools/shared/inplace-editor.js
@@ -0,0 +1,851 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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) {
+ * if (commit) {
+ * spanToEdit.textContent = value;
+ * }
+ * },
+ * trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const {Ci, Cu} = require("chrome");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * 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} aOptions
+ * 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 and a boolean telling the caller whether to
+ * commit the change. This function is called before the editor
+ * has been torn down.
+ * {function} destroy:
+ * Called when the editor is destroyed and has been torn down.
+ * {string} advanceChars:
+ * If any characters in advanceChars are typed, focus will advance
+ * to the next element.
+ * {boolean} stopOnReturn:
+ * If true, the return key will not advance the editor to the next
+ * focusable element.
+ * {string} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ */
+function editableField(aOptions)
+{
+ return editableItem(aOptions, function(aElement, aEvent) {
+ new InplaceEditor(aOptions, aEvent);
+ });
+}
+
+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} aOptions
+ * 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} aCallback
+ * Called when the editor is activated.
+ */
+function editableItem(aOptions, aCallback)
+{
+ let trigger = aOptions.trigger || "click"
+ let element = aOptions.element;
+ element.addEventListener(trigger, function(evt) {
+ let win = this.ownerDocument.defaultView;
+ let selection = win.getSelection();
+ if (trigger != "click" || selection.isCollapsed) {
+ aCallback(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 (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
+ evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+ aCallback(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) {
+ 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;
+}
+
+exports.editableItem = this.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(aSpan)
+{
+ return aSpan.inplaceEditor;
+};
+exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
+
+function InplaceEditor(aOptions, aEvent)
+{
+ this.elt = aOptions.element;
+ let doc = this.elt.ownerDocument;
+ this.doc = doc;
+ this.elt.inplaceEditor = this;
+
+ this.change = aOptions.change;
+ this.done = aOptions.done;
+ this.destroy = aOptions.destroy;
+ this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+ this.multiline = aOptions.multiline || false;
+ this.stopOnReturn = !!aOptions.stopOnReturn;
+
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onKeyup = this._onKeyup.bind(this);
+
+ this._createInput();
+ this._autosize();
+
+ // Pull out character codes for advanceChars, listing the
+ // characters that should trigger a blur.
+ this._advanceCharCodes = {};
+ let advanceChars = aOptions.advanceChars || '';
+ for (let i = 0; i < advanceChars.length; i++) {
+ this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
+ }
+
+ // 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);
+
+ if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
+ this.input.select();
+ }
+ this.input.focus();
+
+ this.input.addEventListener("blur", this._onBlur, false);
+ this.input.addEventListener("keypress", this._onKeyPress, false);
+ this.input.addEventListener("input", this._onInput, false);
+ this.input.addEventListener("mousedown", function(aEvt) {
+ aEvt.stopPropagation();
+ }, false);
+
+ this.warning = aOptions.warning;
+ this.validate = aOptions.validate;
+
+ if (this.warning && this.validate) {
+ this.input.addEventListener("keyup", this._onKeyup, false);
+ }
+
+ if (aOptions.start) {
+ aOptions.start(this, aEvent);
+ }
+}
+
+exports.InplaceEditor = InplaceEditor;
+
+InplaceEditor.prototype = {
+ _createInput: function InplaceEditor_createEditor()
+ {
+ this.input =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+ this.input.inplaceEditor = this;
+ this.input.classList.add("styleinspector-propertyeditor");
+ this.input.value = this.initial;
+
+ copyTextStyles(this.elt, this.input);
+ },
+
+ /**
+ * Get rid of the editor.
+ */
+ _clear: function InplaceEditor_clear()
+ {
+ 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("oninput", this._onInput, false);
+ this._stopAutosize();
+
+ this.elt.style.display = this.originalDisplay;
+ this.elt.focus();
+
+ if (this.destroy) {
+ this.destroy();
+ }
+
+ this.elt.parentNode.removeChild(this.input);
+ this.input = null;
+
+ delete this.elt.inplaceEditor;
+ delete this.elt;
+ },
+
+ /**
+ * Keeps the editor close to the size of its input string. This is pretty
+ * crappy, suggestions for improvement welcome.
+ */
+ _autosize: function InplaceEditor_autosize()
+ {
+ // 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";
+ copyTextStyles(this.input, this._measurement);
+ this._updateSize();
+ },
+
+ /**
+ * Clean up the mess created by _autosize().
+ */
+ _stopAutosize: function InplaceEditor_stopAutosize()
+ {
+ if (!this._measurement) {
+ return;
+ }
+ this._measurement.parentNode.removeChild(this._measurement);
+ delete this._measurement;
+ },
+
+ /**
+ * Size the editor to fit its current contents.
+ */
+ _updateSize: function InplaceEditor_updateSize()
+ {
+ // Replace spaces with non-breaking spaces. Otherwise setting
+ // the span's textContent will collapse spaces and the measurement
+ // will be wrong.
+ this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
+
+ // We add a bit of padding to the end. Should be enough to fit
+ // any letter that could be typed, otherwise we'll scroll before
+ // we get a chance to resize. Yuck.
+ let width = this._measurement.offsetWidth + 10;
+
+ if (this.multiline) {
+ // Make sure there's some content in the current line. This is a hack to
+ // account for the fact that after adding a newline the <pre> doesn't grow
+ // unless there's text content on the line.
+ width += 15;
+ this._measurement.textContent += "M";
+ this.input.style.height = this._measurement.offsetHeight + "px";
+ }
+
+ this.input.style.width = width + "px";
+ },
+
+ /**
+ * Increment property values in rule view.
+ *
+ * @param {number} increment
+ * The amount to increase/decrease the property value.
+ * @return {bool} true if value has been incremented.
+ */
+ _incrementValue: function InplaceEditor_incrementValue(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);
+
+ 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 InplaceEditor_incrementCSSValue(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 incrementedValue = null, selection;
+
+ if (type === "num") {
+ let newValue = this._incrementRawValue(rawValue, increment);
+ 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 {
+ let info;
+ 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;
+ }
+
+ let preRawValue = value.substr(0, range.start);
+ let postRawValue = value.substr(range.end);
+
+ return {
+ value: preRawValue + incrementedValue + postRawValue,
+ start: range.start + selection[0],
+ end: range.start + selection[1]
+ };
+ },
+
+ /**
+ * 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 InplaceEditor_parseCSSValue(value, offset)
+ {
+ const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+ 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;
+ }
+
+ 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 InplaceEditor_incrementGenericValue(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
+ };
+ }
+ }
+ },
+
+ /**
+ * 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 InplaceEditor_incrementRawValue(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);
+
+ // 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 InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
+ {
+ // Return early if no part of the rawValue is selected.
+ if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+ return;
+ }
+ if (offset < 1 && offsetEnd <= 1) {
+ return;
+ }
+ // 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;
+ }
+
+ if (rawValue.length !== 6) {
+ return;
+ }
+
+ // 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 (-1 < increment && 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;
+ }
+
+ 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]
+ };
+ },
+
+ /**
+ * Call the client's done handler and clear out.
+ */
+ _apply: function InplaceEditor_apply(aEvent)
+ {
+ if (this._applied) {
+ return;
+ }
+
+ this._applied = true;
+
+ if (this.done) {
+ let val = this.input.value.trim();
+ return this.done(this.cancelled ? this.initial : val, !this.cancelled);
+ }
+
+ return null;
+ },
+
+ /**
+ * Handle loss of focus by calling done if it hasn't been called yet.
+ */
+ _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
+ {
+ this._apply();
+ if (!aDoNotClear) {
+ this._clear();
+ }
+ },
+
+ /**
+ * Handle the input field's keypress event.
+ */
+ _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
+ {
+ let prevent = false;
+
+ const largeIncrement = 100;
+ const mediumIncrement = 10;
+ const smallIncrement = 0.1;
+
+ let increment = 0;
+
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
+ increment = 1;
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+ increment = -1;
+ }
+
+ if (aEvent.shiftKey && !aEvent.altKey) {
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+ increment *= largeIncrement;
+ } else {
+ increment *= mediumIncrement;
+ }
+ } else if (aEvent.altKey && !aEvent.shiftKey) {
+ increment *= smallIncrement;
+ }
+
+ if (increment && this._incrementValue(increment) ) {
+ this._updateSize();
+ prevent = true;
+ }
+
+ if (this.multiline &&
+ aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
+ aEvent.shiftKey) {
+ prevent = false;
+ } else if (aEvent.charCode in this._advanceCharCodes
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
+ prevent = true;
+
+ let direction = FOCUS_FORWARD;
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
+ aEvent.shiftKey) {
+ this.cancelled = true;
+ direction = FOCUS_BACKWARD;
+ }
+ if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+ direction = null;
+ }
+
+ let input = this.input;
+
+ this._apply();
+
+ 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, send it a click event to trigger
+ if (next && next.ownerDocument === this.doc && next._editable) {
+ next.click();
+ }
+ }
+
+ this._clear();
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
+ // Cancel and blur ourselves.
+ prevent = true;
+ this.cancelled = true;
+ this._apply();
+ this._clear();
+ aEvent.stopPropagation();
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_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 = !this.input.value;
+ }
+
+ if (prevent) {
+ aEvent.preventDefault();
+ }
+ },
+
+ /**
+ * Handle the input field's keyup event.
+ */
+ _onKeyup: function(aEvent) {
+ // Validate the entered value.
+ this.warning.hidden = this.validate(this.input.value);
+ this._applied = false;
+ this._onBlur(null, true);
+ },
+
+ /**
+ * Handle changes to the input text.
+ */
+ _onInput: function InplaceEditor_onInput(aEvent)
+ {
+ // Validate the entered value.
+ if (this.warning && this.validate) {
+ this.warning.hidden = this.validate(this.input.value);
+ }
+
+ // 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.input.value.trim());
+ }
+ }
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(aFrom, aTo)
+{
+ let win = aFrom.ownerDocument.defaultView;
+ let style = win.getComputedStyle(aFrom);
+ aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+ aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+ aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+ aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(aWin, aDirection)
+{
+ return focusManager.moveFocus(aWin, null, aDirection, 0);
+}
+
+
+XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
+ return Services.focus;
+});
diff --git a/browser/devtools/shared/moz.build b/browser/devtools/shared/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/shared/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/shared/splitview.css b/browser/devtools/shared/splitview.css
new file mode 100644
index 000000000..72d4d61f9
--- /dev/null
+++ b/browser/devtools/shared/splitview.css
@@ -0,0 +1,98 @@
+/* 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;
+}
+
+.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;
+}
+
+.splitview-landscape-resizer {
+ cursor: ew-resize;
+}
+
+/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
+@media (min-width: 551px) {
+ .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;
+}
+
+.splitview-portrait-resizer {
+ display: none;
+}
+
+/* portrait mode */
+@media (max-width: 550px) {
+ .splitview-landscape-splitter {
+ display: none;
+ }
+
+ .splitview-portrait-resizer {
+ display: -moz-box;
+ }
+
+ .splitview-controller {
+ max-width: none;
+ }
+}
diff --git a/browser/devtools/shared/telemetry.js b/browser/devtools/shared/telemetry.js
new file mode 100644
index 000000000..ef0f957b0
--- /dev/null
+++ b/browser/devtools/shared/telemetry.js
@@ -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/. */
+
+/**
+ * Telemetry.
+ *
+ * To add metrics for a tool:
+ *
+ * 1. Create boolean, 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 browser/devtools/shared/telemetry.js
+ * (Telemetry.prototype._histograms):
+ * mytoolname: {
+ * histogram: "DEVTOOLS_MYTOOLNAME_OPENED_BOOLEAN",
+ * userHistogram: "DEVTOOLS_MYTOOLNAME_OPENED_PER_USER_FLAG",
+ * timerHistogram: "DEVTOOLS_MYTOOLNAME_TIME_ACTIVE_SECONDS"
+ * },
+ *
+ * 3. Include this module at the top of your tool. Use:
+ * let Telemetry = require("devtools/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
+ * metrics.mozilla.com.
+ */
+
+const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
+
+this.Telemetry = function() {
+ // 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;
+
+let {Cc, Ci, Cu} = require("chrome");
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+Telemetry.prototype = {
+ _histograms: {
+ toolbox: {
+ timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
+ },
+ options: {
+ histogram: "DEVTOOLS_OPTIONS_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_OPTIONS_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS"
+ },
+ webconsole: {
+ histogram: "DEVTOOLS_WEBCONSOLE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_WEBCONSOLE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ browserconsole: {
+ histogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ inspector: {
+ histogram: "DEVTOOLS_INSPECTOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_INSPECTOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ ruleview: {
+ histogram: "DEVTOOLS_RULEVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_RULEVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
+ },
+ computedview: {
+ histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
+ },
+ layoutview: {
+ histogram: "DEVTOOLS_LAYOUTVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_LAYOUTVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS"
+ },
+ fontinspector: {
+ histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ jsdebugger: {
+ histogram: "DEVTOOLS_JSDEBUGGER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSDEBUGGER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ jsbrowserdebugger: {
+ histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ styleeditor: {
+ histogram: "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ jsprofiler: {
+ histogram: "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
+ },
+ netmonitor: {
+ histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
+ },
+ tilt: {
+ histogram: "DEVTOOLS_TILT_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_TILT_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS"
+ },
+ paintflashing: {
+ histogram: "DEVTOOLS_PAINTFLASHING_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_PAINTFLASHING_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS"
+ },
+ scratchpad: {
+ histogram: "DEVTOOLS_SCRATCHPAD_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_SCRATCHPAD_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
+ },
+ responsive: {
+ histogram: "DEVTOOLS_RESPONSIVE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
+ },
+ developertoolbar: {
+ histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
+ }
+ },
+
+ /**
+ * 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];
+
+ if (!charts) {
+ dump('Warning: An attempt was made to open a tool with an id of "' + id +
+ '", which is not listed in Telemetry._histograms. ' +
+ "Location: telemetry.js/toolOpened()\n");
+ return;
+ }
+
+ if (charts.histogram) {
+ this.log(charts.histogram, true);
+ }
+ if (charts.userHistogram) {
+ this.logOncePerBrowserVersion(charts.userHistogram, true);
+ }
+ if (charts.timerHistogram) {
+ this._timers.set(charts.timerHistogram, new Date());
+ }
+ },
+
+ toolClosed: function(id) {
+ let charts = this._histograms[id];
+
+ if (!charts || !charts.timerHistogram) {
+ return;
+ }
+
+ let startTime = this._timers.get(charts.timerHistogram);
+
+ if (startTime) {
+ let time = (new Date() - startTime) / 1000;
+ this.log(charts.timerHistogram, time);
+ this._timers.delete(charts.timerHistogram);
+ }
+ },
+
+ /**
+ * 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) {
+ let histogram;
+
+ 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 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 = 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 [histogram, time] of this._timers) {
+ time = (new Date() - time) / 1000;
+
+ this.log(histogram, time);
+ this._timers.delete(histogram);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+});
diff --git a/browser/devtools/shared/test/Makefile.in b/browser/devtools/shared/test/Makefile.in
new file mode 100644
index 000000000..088cfa596
--- /dev/null
+++ b/browser/devtools/shared/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_eventemitter_basic.js \
+ browser_layoutHelpers.js \
+ browser_require_basic.js \
+ browser_telemetry_buttonsandsidebar.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_styleeditor.js \
+ browser_telemetry_toolboxtabs_webconsole.js \
+ browser_templater_basic.js \
+ browser_toolbar_basic.js \
+ browser_toolbar_tooltip.js \
+ browser_toolbar_webconsole_errors_count.js \
+ head.js \
+ leakhunt.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_FILES += \
+ browser_templater_basic.html \
+ browser_toolbar_basic.html \
+ browser_toolbar_webconsole_errors_count.html \
+ browser_layoutHelpers.html \
+ browser_layoutHelpers_iframe.html \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/shared/test/browser_eventemitter_basic.js b/browser/devtools/shared/test/browser_eventemitter_basic.js
new file mode 100644
index 000000000..7e9cccae3
--- /dev/null
+++ b/browser/devtools/shared/test/browser_eventemitter_basic.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ testEmitter();
+ testEmitter({});
+}
+
+
+function testEmitter(aObject) {
+ Cu.import("resource:///modules/devtools/shared/event-emitter.js", this);
+
+ let emitter;
+
+ if (aObject) {
+ emitter = aObject;
+ EventEmitter.decorate(emitter);
+ } else {
+ emitter = new EventEmitter();
+ }
+
+ ok(emitter, "We have an event emitter");
+
+ emitter.on("next", next);
+ emitter.emit("next", "abc", "def");
+
+ let beenHere1 = false;
+ function next(eventName, str1, str2) {
+ is(eventName, "next", "Got event");
+ is(str1, "abc", "Argument 1 is correct");
+ is(str2, "def", "Argument 2 is correct");
+
+ ok(!beenHere1, "first time in next callback");
+ beenHere1 = true;
+
+ emitter.off("next", next);
+
+ emitter.emit("next");
+
+ emitter.once("onlyonce", onlyOnce);
+
+ emitter.emit("onlyonce");
+ emitter.emit("onlyonce");
+ }
+
+ let beenHere2 = false;
+ function onlyOnce() {
+ ok(!beenHere2, "\"once\" listner has been called once");
+ beenHere2 = true;
+ emitter.emit("onlyonce");
+
+ killItWhileEmitting();
+ }
+
+ function killItWhileEmitting() {
+ function c1() {
+ ok(true, "c1 called");
+ }
+ function c2() {
+ ok(true, "c2 called");
+ emitter.off("tick", c3);
+ }
+ function c3() {
+ ok(false, "c3 should not be called");
+ }
+ function c4() {
+ ok(true, "c4 called");
+ }
+
+ emitter.on("tick", c1);
+ emitter.on("tick", c2);
+ emitter.on("tick", c3);
+ emitter.on("tick", c4);
+
+ emitter.emit("tick");
+
+ delete emitter;
+ finish();
+ }
+}
diff --git a/browser/devtools/shared/test/browser_layoutHelpers.html b/browser/devtools/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..3b9a285b4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,25 @@
+<!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>
+<iframe id=frame src='./browser_layoutHelpers_iframe.html'></iframe>
diff --git a/browser/devtools/shared/test/browser_layoutHelpers.js b/browser/devtools/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..9747399a4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that scrollIntoViewIfNeeded works properly.
+
+let imported = {};
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm",
+ imported);
+registerCleanupFunction(function () {
+ imported = {};
+});
+
+let LayoutHelpers = imported.LayoutHelpers;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_layoutHelpers.js");
+ let doc = browser.contentDocument;
+ runTest(doc.defaultView, doc.getElementById('some'));
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+function runTest(win, 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.
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden above should appear centered.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above.');
+
+ win.scroll(win.innerWidth / 2, 0); // Just below the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden below should appear centered.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below.');
+
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element completely hidden above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 0); // Below the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element completely hidden below should appear below ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below ' +
+ 'if parameter is false.');
+
+ // The case of iframes.
+ win.scroll(0, 0);
+
+ let frame = win.document.getElementById('frame');
+ let fwin = frame.contentWindow;
+
+ frame.style.top = win.innerHeight + 'px';
+ frame.style.left = win.innerWidth + 'px';
+
+ fwin.addEventListener('load', function frameLoad() {
+ let some = fwin.document.getElementById('some');
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollX, Math.floor(win.innerWidth / 2) + 20,
+ 'Scrolling from an iframe should center the iframe vertically.');
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 20,
+ 'Scrolling from an iframe should center the iframe horizontally.');
+ is(fwin.scrollX, Math.floor(fwin.innerWidth / 2) + 1,
+ 'Scrolling from an iframe should center the element vertically.');
+ is(fwin.scrollY, Math.floor(fwin.innerHeight / 2) + 1,
+ 'Scrolling from an iframe should center the element horizontally.');
+ }, false);
+}
diff --git a/browser/devtools/shared/test/browser_layoutHelpers_iframe.html b/browser/devtools/shared/test/browser_layoutHelpers_iframe.html
new file mode 100644
index 000000000..66ef5b293
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers_iframe.html
@@ -0,0 +1,19 @@
+<!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;
+ }
+</style>
+
+<div id=some></div>
+
diff --git a/browser/devtools/shared/test/browser_require_basic.js b/browser/devtools/shared/test/browser_require_basic.js
new file mode 100644
index 000000000..f86974df4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_require_basic.js
@@ -0,0 +1,140 @@
+/* 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.
+
+let [ define, require ] = (function() {
+ let tempScope = {};
+ Components.utils.import("resource://gre/modules/devtools/Require.jsm", tempScope);
+ return [ tempScope.define, tempScope.require ];
+})();
+
+function test() {
+ addTab("about:blank", function() {
+ info("Starting Require Tests");
+ setup();
+
+ testWorking();
+ testDomains();
+ testLeakage();
+ testMultiImport();
+ testRecursive();
+ testUncompilable();
+ testFirebug();
+
+ shutdown();
+ });
+}
+
+function setup() {
+ define('gclitest/requirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ exports.thing1 = 'thing1';
+ exports.thing2 = 2;
+
+ let status = 'initial';
+ exports.setStatus = function(aStatus) { status = aStatus; };
+ exports.getStatus = function() { return status; };
+ });
+
+ define('gclitest/unrequirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ null.throwNPE();
+ });
+
+ define('gclitest/recurse', [ 'require', 'exports', 'module', 'gclitest/recurse' ], function(require, exports, module) {
+ require('gclitest/recurse');
+ });
+
+ define('gclitest/firebug', [ 'gclitest/requirable' ], function(requirable) {
+ return { requirable: requirable, fb: true };
+ });
+}
+
+function shutdown() {
+ delete define.modules['gclitest/requirable'];
+ delete define.globalDomain.modules['gclitest/requirable'];
+ delete define.modules['gclitest/unrequirable'];
+ delete define.globalDomain.modules['gclitest/unrequirable'];
+ delete define.modules['gclitest/recurse'];
+ delete define.globalDomain.modules['gclitest/recurse'];
+ delete define.modules['gclitest/firebug'];
+ delete define.globalDomain.modules['gclitest/firebug'];
+
+ define = undefined;
+ require = undefined;
+
+ finish();
+}
+
+function testWorking() {
+ // There are lots of requirement tests that we could be doing here
+ // The fact that we can get anything at all working is a testament to
+ // require doing what it should - we don't need to test the
+ let requireable = require('gclitest/requirable');
+ is('thing1', requireable.thing1, 'thing1 was required');
+ is(2, requireable.thing2, 'thing2 was required');
+ is(requireable.thing3, undefined, 'thing3 was not required');
+}
+
+function testDomains() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.status, undefined, 'requirable has no status');
+ requireable.setStatus(null);
+ is(null, requireable.getStatus(), 'requirable.getStatus changed to null');
+ is(requireable.status, undefined, 'requirable still has no status');
+ requireable.setStatus('42');
+ is('42', requireable.getStatus(), 'requirable.getStatus changed to 42');
+ is(requireable.status, undefined, 'requirable *still* has no status');
+
+ let domain = new define.Domain();
+ let requireable2 = domain.require('gclitest/requirable');
+ is(requireable2.status, undefined, 'requirable2 has no status');
+ is('initial', requireable2.getStatus(), 'requirable2.getStatus is initial');
+ requireable2.setStatus(999);
+ is(999, requireable2.getStatus(), 'requirable2.getStatus changed to 999');
+ is(requireable2.status, undefined, 'requirable2 still has no status');
+
+ is('42', requireable.getStatus(), 'status 42');
+ ok(requireable.status === undefined, 'requirable has no status (as expected)');
+
+ delete domain.modules['gclitest/requirable'];
+}
+
+function testLeakage() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.setup, null, 'leakage of setup');
+ is(requireable.shutdown, null, 'leakage of shutdown');
+ is(requireable.testWorking, null, 'leakage of testWorking');
+}
+
+function testMultiImport() {
+ let r1 = require('gclitest/requirable');
+ let r2 = require('gclitest/requirable');
+ is(r1, r2, 'double require was strict equal');
+}
+
+function testUncompilable() {
+ // It's not totally clear how a module loader should perform with unusable
+ // modules, however at least it should go into a flat spin ...
+ // GCLI mini_require reports an error as it should
+ try {
+ let unrequireable = require('gclitest/unrequirable');
+ fail();
+ }
+ catch (ex) {
+ // an exception is expected
+ }
+}
+
+function testRecursive() {
+ // See Bug 658583
+ // require('gclitest/recurse');
+ // Also see the comments in the testRecursive() function
+}
+
+function testFirebug() {
+ let requirable = require('gclitest/requirable');
+ let firebug = require('gclitest/firebug');
+ ok(firebug.fb, 'firebug.fb is true');
+ is(requirable, firebug.requirable, 'requirable pass-through');
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js b/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js
new file mode 100644
index 000000000..8553ed799
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js
@@ -0,0 +1,179 @@
+/* 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_telemetry_buttonsandsidebar.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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ testButtons();
+}
+
+function testButtons() {
+ info("Testing buttons");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let container = toolbox.doc.getElementById("toolbox-buttons");
+ let buttons = container.getElementsByTagName("toolbarbutton");
+
+ // Copy HTMLCollection to array.
+ buttons = Array.prototype.slice.call(buttons);
+
+ (function testButton() {
+ let button = buttons.pop();
+
+ if (button) {
+ info("Clicking button " + button.id);
+ button.click();
+ delayedClicks(button, 3).then(function(button) {
+ if (buttons.length == 0) {
+ // Remove scratchpads
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ info("Closing scratchpad window");
+ win.close();
+ }
+
+ testSidebar();
+ } else {
+ setTimeout(testButton, TOOL_DELAY);
+ }
+ });
+ }
+ })();
+ }).then(null, reportError);
+}
+
+function delayedClicks(node, clicks) {
+ let deferred = Promise.defer();
+ let clicked = 0;
+
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ deferred.resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+
+ return deferred.promise;
+}
+
+function testSidebar() {
+ info("Testing sidebar");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector", "layoutview"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function() {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ checkResults();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(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.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..e53c1b829
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("inspector", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..1b1a6234b
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("jsdebugger", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..567219222
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("jsprofiler", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..bc139acfb
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("netmonitor", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..687a07c38
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("options", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..9e30c74fa
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("styleeditor", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..ba027e7fd
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,110 @@
+/* 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_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;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ 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);
+ }
+ }
+
+ openToolboxTabTwice("webconsole", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ 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");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_templater_basic.html b/browser/devtools/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/shared/test/browser_templater_basic.js b/browser/devtools/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..ce8cb130e
--- /dev/null
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// 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.
+ */
+
+var Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
+var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
+
+function test() {
+ addTab(TEST_URI, function() {
+ info("Starting DOM Templater Tests");
+ runTest(0);
+ });
+}
+
+function runTest(index) {
+ var options = tests[index] = tests[index]();
+ var holder = content.document.createElement('div');
+ holder.id = options.name;
+ var body = content.document.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);
+ }
+ else {
+ finished();
+ }
+ }
+
+ if (options.later) {
+ var ais = is.bind(this);
+
+ function createTester(holder, options) {
+ return function() {
+ ais(holder.innerHTML, options.later, options.name + ' later');
+ runNextTest();
+ }.bind(this);
+ }
+
+ executeSoon(createTester(holder, options));
+ }
+ else {
+ runNextTest();
+ }
+}
+
+function finished() {
+ 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 = [
+ function() { return {
+ name: 'simpleNesting',
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested:{ value:'pass 1' } },
+ result: '<div id="ex1">pass 1</div>'
+ };},
+
+ function() { return {
+ name: 'returnDom',
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ };},
+
+ function() { return {
+ name: 'srcChange',
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred:'green.png' },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ };},
+
+ function() { return {
+ name: 'ifTrue',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'fred' },
+ result: '<p>hello fred</p>'
+ };},
+
+ function() { return {
+ name: 'ifFalse',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'jim' },
+ result: ''
+ };},
+
+ function() { return {
+ 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>'
+ };},
+
+ function() { return {
+ 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
+ function() { return {
+ name: 'asyncLoopElement',
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: '<span></span>',
+ later: '123'
+ };},
+
+ function() { return {
+ 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;
+ }
+ };},
+
+ function() { return {
+ 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>'
+ };},
+
+ function() { return {
+ name: 'asyncInline',
+ template: '${delayed}',
+ data: { delayed: delayReply('inline') },
+ result: '<span></span>',
+ later: 'inline'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ function() { return {
+ 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>'
+ };},
+
+ function() { return {
+ 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
+ function() { return {
+ 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
+ function() { return {
+ 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 ${}
+ function() { return {
+ name: 'propertySimple',
+ template: '<p>${a.b.c}</p>',
+ data: { a: { b: { c: 'hello' } } },
+ result: '<p>hello</p>'
+ };},
+
+ function() { return {
+ name: 'propertyPass',
+ template: '<p>${Math.max(1, 2)}</p>',
+ options: { allowEval: true },
+ result: '<p>2</p>'
+ };},
+
+ function() { return {
+ 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
+ function() { return {
+ name: 'propertyUndefAttrFull',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<p>null|undefined|undefined</p>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrBlank',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<p>||</p>'
+ };},
+
+ function() { return {
+ 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>'
+ };},
+
+ function() { return {
+ 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>'
+ };}
+];
+
+function delayReply(data) {
+ var d = Promise.defer();
+ executeSoon(function() {
+ d.resolve(data);
+ });
+ return d.promise;
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_basic.html b/browser/devtools/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..7ec012b0e
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,35 @@
+<!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">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>var b=2;</script>
+
+</body>
+</html>
+
diff --git a/browser/devtools/shared/test/browser_toolbar_basic.js b/browser/devtools/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..8819effc1
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_toolbar_basic.js");
+ runTest();
+ });
+}
+
+function runTest() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
+
+function checkOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ addTab("about:blank", function(browser, tab) {
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkClosed));
+ document.getElementById("Tools:DevToolbar").doCommand();
+ });
+ });
+}
+
+function checkClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkClosed");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkReOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function checkReOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkReOpen");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkReClosed));
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+}
+
+function checkReClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkReClosed");
+
+ finish();
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_tooltip.js b/browser/devtools/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..fa86b2fcb
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_toolbar_tooltip.js");
+ openTest();
+ });
+}
+
+function openTest() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(runTest));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function runTest() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.display.focusManager.helpRequest();
+ DeveloperToolbar.display.inputter.setInput('help help');
+
+ DeveloperToolbar.display.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.display.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.display.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.display.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')
+
+ finish();
+}
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..216cc0d49
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,32 @@
+<!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">
+ 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/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..aaf027451
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar errors count works properly.
+
+function test() {
+ const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
+ "browser_toolbar_webconsole_errors_count.html";
+
+ let HUDService = Cu.import("resource:///modules/HUDService.jsm",
+ {}).HUDService;
+ let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
+ {}).gDevTools;
+
+ let webconsole = document.getElementById("developer-toolbar-toolbox-button");
+ let tab1, tab2;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI, openToolbar);
+
+ function openToolbar(browser, tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true, onOpenToolbar);
+ }
+ else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+
+ 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, onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(browser, 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) {
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function() {
+ return hud.outputNode.querySelectorAll(".hud-exception").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: 1, // warnings are not repeated by the js engine
+ 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() {
+ return hud.outputNode.querySelectorAll(".hud-exception").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);
+ 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/browser/devtools/shared/test/head.js b/browser/devtools/shared/test/head.js
new file mode 100644
index 000000000..d79c5c4c8
--- /dev/null
+++ b/browser/devtools/shared/test/head.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/. */
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback)
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = aURL;
+
+ let tab = gBrowser.selectedTab;
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ function onTabLoad() {
+ browser.removeEventListener("load", onTabLoad, true);
+ aCallback(browser, tab, browser.contentDocument);
+ }
+
+ browser.addEventListener("load", onTabLoad, true);
+}
+
+registerCleanupFunction(function tearDown() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+
+ console = undefined;
+});
+
+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 aOptions
+ * 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 |aOptions| object and the last value returned by |validator|.
+ */
+function waitForValue(aOptions)
+{
+ let start = Date.now();
+ let timeout = aOptions.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn)
+ {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + aOptions.name);
+ let expected = "value" in aOptions ?
+ "'" + aOptions.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(aOptions, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(aOptions, lastValue);
+ let successful = "value" in aOptions ?
+ lastValue == aOptions.value :
+ lastValue;
+ if (successful) {
+ ok(true, aOptions.name);
+ successFn(aOptions, lastValue);
+ }
+ else {
+ setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+ }
+ }
+
+ wait(aOptions.validator, aOptions.success, aOptions.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ var func = function() {
+ Services.obs.removeObserver(func, name);
+ callback();
+ };
+ Services.obs.addObserver(func, name, false);
+}
diff --git a/browser/devtools/shared/test/leakhunt.js b/browser/devtools/shared/test/leakhunt.js
new file mode 100644
index 000000000..66d067a3c
--- /dev/null
+++ b/browser/devtools/shared/test/leakhunt.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/. */
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ var path = [];
+ var seen = [];
+
+ try {
+ var output = leakHunt.inner(root, path, seen);
+ output.forEach(function(line) {
+ dump(line + '\n');
+ });
+ }
+ catch (ex) {
+ dump(ex + '\n');
+ }
+}
+
+leakHunt.inner = function LH_inner(root, path, seen) {
+ var prefix = new Array(path.length).join(' ');
+
+ var reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ var direct
+ try {
+ direct = Object.keys(root);
+ }
+ catch (ex) {
+ log(prefix + ' Error enumerating: ' + ex);
+ return reply;
+ }
+
+ try {
+ var index = 0;
+ for (var data of root) {
+ var prop = '' + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ }
+ catch (ex) { /* Ignore things that are not enumerable */ }
+
+ for (var prop in root) {
+ var 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 LH_digProperty(prop, data, path, seen, direct, log) {
+ var newPath = path.slice();
+ newPath.push(prop);
+ var prefix = new Array(newPath.length).join(' ');
+
+ var recurse = true;
+ var message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === 'function' && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === 'string') {
+ var 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);
+ var 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 LH_matchesAnyPattern(str, patterns) {
+ var match = false;
+ patterns.forEach(function(pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function LH_getType(data) {
+ if (data === null) {
+ return 'null';
+ }
+ if (data === undefined) {
+ return 'undefined';
+ }
+
+ var type = typeof data;
+ if (type === 'object' || type === 'Object') {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function LH_getCtorName(aObj) {
+ try {
+ if (aObj.constructor && aObj.constructor.name) {
+ return aObj.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(aObj).slice(8, -1);
+};
diff --git a/browser/devtools/shared/test/moz.build b/browser/devtools/shared/test/moz.build
new file mode 100644
index 000000000..191c90f0b
--- /dev/null
+++ b/browser/devtools/shared/test/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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 += ['unit/xpcshell.ini']
diff --git a/browser/devtools/shared/test/unit/test_undoStack.js b/browser/devtools/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..f1a230693
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; 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/ */
+
+const Cu = Components.utils;
+let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+let loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource:///modules/devtools",
+ },
+ globals: {},
+});
+let require = Loader.Require(loader, { id: "undo-test" })
+
+let {UndoStack} = require("devtools/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/browser/devtools/shared/test/unit/xpcshell.ini b/browser/devtools/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..342274bed
--- /dev/null
+++ b/browser/devtools/shared/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+
+[test_undoStack.js]
diff --git a/browser/devtools/shared/theme-switching.js b/browser/devtools/shared/theme-switching.js
new file mode 100644
index 000000000..d36e51c87
--- /dev/null
+++ b/browser/devtools/shared/theme-switching.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/. */
+
+(function() {
+ const DEVTOOLS_SKIN_URL = "chrome://browser/skin/devtools/";
+
+ function forceStyle() {
+ let computedStyle = window.getComputedStyle(document.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;
+ }
+ let display = computedStyle.display; // Save display value
+ document.documentElement.style.display = "none";
+ window.getComputedStyle(document.documentElement).display; // Flush
+ document.documentElement.style.display = display; // Restore
+ }
+
+ function switchTheme(newTheme, oldTheme) {
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (oldTheme && newTheme != oldTheme) {
+ let oldThemeUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + oldTheme + "-theme.css", null, null);
+ try {
+ winUtils.removeSheet(oldThemeUrl, window.AUTHOR_SHEET);
+ } catch(ex) {}
+ }
+
+ let newThemeUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + newTheme + "-theme.css", null, null);
+ winUtils.loadSheet(newThemeUrl, window.AUTHOR_SHEET);
+
+ // Floating scrollbars à la osx
+ if (Services.appinfo.OS != "Darwin") {
+ let scrollbarsUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", null, null);
+
+ if (newTheme == "dark") {
+ winUtils.loadSheet(scrollbarsUrl, window.AGENT_SHEET);
+ } else if (oldTheme == "dark") {
+ try {
+ winUtils.removeSheet(scrollbarsUrl, window.AGENT_SHEET);
+ } catch(ex) {}
+ }
+ forceStyle();
+ }
+
+ document.documentElement.classList.remove("theme-" + oldTheme);
+ document.documentElement.classList.add("theme-" + newTheme);
+ }
+
+ function handlePrefChange(event, data) {
+ if (data.pref == "devtools.theme") {
+ switchTheme(data.newValue, data.oldValue);
+ }
+ }
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ switchTheme(theme);
+
+ gDevTools.on("pref-changed", handlePrefChange);
+ window.addEventListener("unload", function() {
+ gDevTools.off("pref-changed", handlePrefChange);
+ });
+})();
diff --git a/browser/devtools/shared/undo.js b/browser/devtools/shared/undo.js
new file mode 100644
index 000000000..ca825329a
--- /dev/null
+++ b/browser/devtools/shared/undo.js
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+/**
+ * A simple undo stack manager.
+ *
+ * Actions are added along with the necessary code to
+ * reverse the action.
+ *
+ * @param function aChange Called whenever the size or position
+ * of the undo stack changes, to use for updating undo-related
+ * UI.
+ * @param integer aMaxUndo Maximum number of undo steps.
+ * defaults to 50.
+ */
+function UndoStack(aMaxUndo)
+{
+ this.maxUndo = aMaxUndo || 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 Undo_destroy()
+ {
+ 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 Undo_startBatch()
+ {
+ if (this._batchDepth++ === 0) {
+ this._batch = [];
+ }
+ },
+
+ /**
+ * End a batch of related changes, performing its action and adding
+ * it to the undo stack.
+ */
+ endBatch: function Undo_endBatch()
+ {
+ 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 aDo Called to perform the action.
+ * @param function aUndo Called to reverse the action.
+ */
+ do: function Undo_do(aDo, aUndo) {
+ this.startBatch();
+ this._batch.push({ do: aDo, undo: aUndo });
+ this.endBatch();
+ },
+
+ /*
+ * Returns true if undo() will do anything.
+ */
+ canUndo: function Undo_canUndo()
+ {
+ return this._index > 0;
+ },
+
+ /**
+ * Undo the top of the undo stack.
+ *
+ * @return true if an action was undone.
+ */
+ undo: function Undo_canUndo()
+ {
+ if (!this.canUndo()) {
+ return false;
+ }
+ this._stack[--this._index].undo();
+ this._change();
+ return true;
+ },
+
+ /**
+ * Returns true if redo() will do anything.
+ */
+ canRedo: function Undo_canRedo()
+ {
+ return this._stack.length > this._index;
+ },
+
+ /**
+ * Redo the most recently undone action.
+ *
+ * @return true if an action was redone.
+ */
+ redo: function Undo_canRedo()
+ {
+ if (!this.canRedo()) {
+ return false;
+ }
+ this._stack[this._index++].do();
+ this._change();
+ return true;
+ },
+
+ _change: function Undo__change()
+ {
+ 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 Undo_installController(aControllerWindow)
+ {
+ this._controllerWindow = aControllerWindow;
+ aControllerWindow.controllers.appendController(this);
+ },
+
+ /**
+ * Uninstall this object from the command controller.
+ */
+ uninstallController: function Undo_uninstallController()
+ {
+ if (!this._controllerWindow) {
+ return;
+ }
+ this._controllerWindow.controllers.removeController(this);
+ },
+
+ supportsCommand: function Undo_supportsCommand(aCommand)
+ {
+ return (aCommand == "cmd_undo" ||
+ aCommand == "cmd_redo");
+ },
+
+ isCommandEnabled: function Undo_isCommandEnabled(aCommand)
+ {
+ switch(aCommand) {
+ case "cmd_undo": return this.canUndo();
+ case "cmd_redo": return this.canRedo();
+ };
+ return false;
+ },
+
+ doCommand: function Undo_doCommand(aCommand)
+ {
+ switch(aCommand) {
+ case "cmd_undo": return this.undo();
+ case "cmd_redo": return this.redo();
+ }
+ },
+
+ onEvent: function Undo_onEvent(aEvent) {},
+}
diff --git a/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
new file mode 100644
index 000000000..a6c50a01c
--- /dev/null
+++ b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
@@ -0,0 +1,227 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+this.EXPORTED_SYMBOLS = ["BreadcrumbsWidget"];
+
+/**
+ * A breadcrumb-like list of items.
+ * This widget should be used in tandem with the WidgetMethods in ViewHelpers.jsm
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode) {
+ 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.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 string | nsIDOMNode aContents
+ * The string or 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() 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");
+ }
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ this.window.clearTimeout(this._ensureVisibleTimeout);
+ this._ensureVisibleTimeout = this.window.setTimeout(() => {
+ if (this._selectedItem) {
+ this._list.ensureElementIsVisible(this._selectedItem);
+ }
+ }, ENSURE_SELECTION_VISIBLE_DELAY);
+ },
+
+ /**
+ * 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,
+ _ensureVisibleTimeout: null
+};
+
+/**
+ * A Breadcrumb constructor for the BreadcrumbsWidget.
+ *
+ * @param BreadcrumbsWidget aWidget
+ * The widget to contain this breadcrumb.
+ * @param string | nsIDOMNode aContents
+ * The string or 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 this item's view contents are a string, then create a label to hold
+ // the text displayed in this breadcrumb.
+ if (typeof aContents == "string") {
+ let label = this.document.createElement("label");
+ label.setAttribute("value", aContents);
+ this.contents = label;
+ return;
+ }
+ // 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/browser/devtools/shared/widgets/SideMenuWidget.jsm b/browser/devtools/shared/widgets/SideMenuWidget.jsm
new file mode 100644
index 000000000..d4ca8d9f0
--- /dev/null
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -0,0 +1,621 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
+
+/**
+ * A simple side menu, with the ability of grouping menu items.
+ * This widget should be used in tandem with the WidgetMethods in ViewHelpers.jsm
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param boolean aShowArrows
+ * Specifies if items in this container should display horizontal arrows.
+ */
+this.SideMenuWidget = function SideMenuWidget(aNode, aShowArrows = true) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+ this._showArrows = aShowArrows;
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "side-menu-widget-container";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("with-arrow", aShowArrows);
+ 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._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ // Menu items can optionally be grouped.
+ this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
+ this._orderedGroupElementsArray = [];
+ this._orderedMenuElementsArray = [];
+
+ // 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.delegateWidgetEventMethods(this, aNode);
+};
+
+SideMenuWidget.prototype = {
+ /**
+ * Specifies if groups in this container should be sorted alphabetically.
+ */
+ sortedGroups: true,
+
+ /**
+ * 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 that the container viewport should be "stuck" to the
+ * bottom. That is, the container is automatically scrolled down to
+ * keep appended items visible, but only when the scroll position is
+ * already at the bottom.
+ */
+ autoscrollWithAppendedItems: false,
+
+ /**
+ * 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 string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ * @param string aGroup [optional]
+ * The group to place the displayed item into.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function(aIndex, aContents, aTooltip = "", aGroup = "") {
+ aTooltip = NetworkHelper.convertToUnicode(unescape(aTooltip));
+ aGroup = NetworkHelper.convertToUnicode(unescape(aGroup));
+
+ // Invalidate any notices set on this widget.
+ this.removeAttribute("notice");
+
+ // Maintaining scroll position at the bottom when a new item is inserted
+ // depends on several factors (the order of testing is important to avoid
+ // needlessly expensive operations that may cause reflows):
+ let maintainScrollAtBottom =
+ // 1. The behavior should be enabled,
+ this.autoscrollWithAppendedItems &&
+ // 2. There shouldn't currently be any selected item in the list.
+ !this._selectedItem &&
+ // 3. The new item should be appended at the end of the list.
+ (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) &&
+ // 4. The list should already be scrolled at the bottom.
+ (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight);
+
+ let group = this._getMenuGroupForName(aGroup);
+ let item = this._getMenuItemForGroup(group, aContents, aTooltip);
+ let element = item.insertSelfAt(aIndex);
+
+ if (this.maintainSelectionVisible) {
+ this.ensureSelectionIsVisible({ withGroup: true, delayed: true });
+ }
+ if (maintainScrollAtBottom) {
+ this._list.scrollTop = this._list.scrollHeight;
+ }
+
+ return element;
+ },
+
+ /**
+ * 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) {
+ if (aChild.className == "side-menu-widget-item-contents") {
+ // Remove the item itself, not the contents.
+ aChild.parentNode.remove();
+ } else {
+ // Groups with no title don't have any special internal structure.
+ aChild.remove();
+ }
+
+ this._orderedMenuElementsArray.splice(
+ this._orderedMenuElementsArray.indexOf(aChild), 1);
+
+ 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;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() 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) {
+ node.classList.add("selected");
+ node.parentNode.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ node.parentNode.classList.remove("selected");
+ }
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ this.ensureSelectionIsVisible({ delayed: true });
+ },
+
+ /**
+ * Ensures the selected element is visible.
+ * @see SideMenuWidget.prototype.ensureElementIsVisible.
+ */
+ ensureSelectionIsVisible: function(aFlags) {
+ this.ensureElementIsVisible(this.selectedItem, aFlags);
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ * @param object aFlags [optional]
+ * An object containing some of the following flags:
+ * - withGroup: true if the group header should also be made visible, if possible
+ * - delayed: wait a few cycles before ensuring the selection is visible
+ */
+ ensureElementIsVisible: function(aElement, aFlags = {}) {
+ if (!aElement) {
+ return;
+ }
+
+ if (aFlags.delayed) {
+ delete aFlags.delayed;
+ this.window.clearTimeout(this._ensureVisibleTimeout);
+ this._ensureVisibleTimeout = this.window.setTimeout(() => {
+ this.ensureElementIsVisible(aElement, aFlags);
+ }, ENSURE_SELECTION_VISIBLE_DELAY);
+ return;
+ }
+
+ if (aFlags.withGroup) {
+ let groupList = aElement.parentNode;
+ let groupContainer = groupList.parentNode;
+ groupContainer.scrollIntoView(true); // Align with the top.
+ }
+
+ this._boxObject.ensureElementIsVisible(aElement);
+ },
+
+ /**
+ * 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;
+ }
+ },
+
+ /**
+ * 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) {
+ return this._parent.getAttribute(aName);
+ },
+
+ /**
+ * 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 == "notice") {
+ this.notice = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function(aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "notice") {
+ this._removeNotice();
+ }
+ },
+
+ /**
+ * Sets the text displayed in this container as a notice.
+ * @param string aValue
+ */
+ set notice(aValue) {
+ if (this._noticeTextNode) {
+ this._noticeTextNode.setAttribute("value", aValue);
+ }
+ this._noticeTextValue = aValue;
+ this._appendNotice();
+ },
+
+ /**
+ * Creates and appends a label representing a notice in this container.
+ */
+ _appendNotice: function() {
+ if (this._noticeTextNode || !this._noticeTextValue) {
+ return;
+ }
+
+ let container = this.document.createElement("vbox");
+ container.className = "side-menu-widget-empty-notice-container";
+ container.setAttribute("align", "center");
+
+ let label = this.document.createElement("label");
+ label.className = "plain side-menu-widget-empty-notice";
+ label.setAttribute("value", this._noticeTextValue);
+ container.appendChild(label);
+
+ this._parent.insertBefore(container, this._list);
+ this._noticeTextContainer = container;
+ this._noticeTextNode = label;
+ },
+
+ /**
+ * Removes the label representing a notice in this container.
+ */
+ _removeNotice: function() {
+ if (!this._noticeTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._noticeTextContainer);
+ this._noticeTextContainer = null;
+ this._noticeTextNode = 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);
+ this._groupsByName.set(aName, group);
+ group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf() : -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 string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ */
+ _getMenuItemForGroup: function(aGroup, aContents, aTooltip) {
+ return new SideMenuItem(aGroup, aContents, aTooltip, this._showArrows);
+ },
+
+ window: null,
+ document: null,
+ _showArrows: false,
+ _parent: null,
+ _list: null,
+ _boxObject: null,
+ _selectedItem: null,
+ _groupsByName: null,
+ _orderedGroupElementsArray: null,
+ _orderedMenuElementsArray: null,
+ _ensureVisibleTimeout: null,
+ _noticeTextContainer: null,
+ _noticeTextNode: null,
+ _noticeTextValue: ""
+};
+
+/**
+ * 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.
+ */
+function SideMenuGroup(aWidget, aName) {
+ 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);
+ target.setAttribute("tooltiptext", 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");
+
+ 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";
+ }
+}
+
+SideMenuGroup.prototype = {
+ get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+ get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+
+ /**
+ * 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() {
+ let identifier = this.identifier;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ for (let group of groupsArray) {
+ let name = group.getAttribute("name");
+ if (name > identifier && // Insertion sort at its best :)
+ !name.contains(identifier)) { // Least significat group should be last.
+ return groupsArray.indexOf(group);
+ }
+ }
+ return -1;
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ identifier: "",
+ _target: null,
+ _title: null,
+ _name: null,
+ _list: null
+};
+
+/**
+ * A SideMenuItem constructor for the BreadcrumbsWidget.
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain this menu item.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param boolean aArrowFlag
+ * True if a horizontal arrow should be shown.
+ */
+function SideMenuItem(aGroup, aContents, aTooltip, aArrowFlag) {
+ this.document = aGroup.document;
+ this.window = aGroup.window;
+ this.ownerView = aGroup;
+
+ // Show a horizontal arrow towards the content.
+ if (aArrowFlag) {
+ let container = this._container = this.document.createElement("hbox");
+ container.className = "side-menu-widget-item";
+ container.setAttribute("tooltiptext", aTooltip);
+
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-item-contents";
+
+ let arrow = this._arrow = this.document.createElement("hbox");
+ arrow.className = "side-menu-widget-item-arrow";
+
+ container.appendChild(target);
+ container.appendChild(arrow);
+ }
+ // Skip a few redundant nodes when no horizontal arrow is shown.
+ else {
+ let target = this._target = this._container = this.document.createElement("hbox");
+ target.className = "side-menu-widget-item side-menu-widget-item-contents";
+ }
+
+ this._target.setAttribute("flex", "1");
+ this.contents = aContents;
+}
+
+SideMenuItem.prototype = {
+ get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+ get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+
+ /**
+ * 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);
+ }
+
+ return this._target;
+ },
+
+ /**
+ * 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 this item's view contents are a string, then create a label to hold
+ // the text displayed in this breadcrumb.
+ if (typeof aContents == "string") {
+ let label = this.document.createElement("label");
+ label.className = "side-menu-widget-item-label";
+ label.setAttribute("value", aContents);
+ label.setAttribute("crop", "start");
+ label.setAttribute("flex", "1");
+ this.contents = label;
+ return;
+ }
+ // 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,
+ _arrow: null
+};
diff --git a/browser/devtools/shared/widgets/VariablesView.jsm b/browser/devtools/shared/widgets/VariablesView.jsm
new file mode 100644
index 000000000..7a4d5531b
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -0,0 +1,3166 @@
+/* -*- 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
+const LAZY_EXPAND_DELAY = 50; // ms
+const LAZY_APPEND_DELAY = 100; // ms
+const LAZY_APPEND_BATCH = 100; // nodes
+const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
+const PAGE_SIZE_MAX_JUMPS = 30;
+const SEARCH_ACTION_MAX_DELAY = 300; // ms
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["VariablesView"];
+
+/**
+ * Debugger localization strings.
+ */
+const STR = Services.strings.createBundle(DBG_STRINGS_URI);
+
+/**
+ * A tree view for inspecting scopes, objects and properties.
+ * Iterable via "for (let [id, scope] in 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._appendEmptyNotice();
+
+ this._onSearchboxInput = this._onSearchboxInput.bind(this);
+ this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
+ this._onViewKeyPress = this._onViewKeyPress.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._parent.appendChild(this._list);
+ this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ 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().populate(aObject, { sorted: true });
+ },
+
+ /**
+ * Adds a scope to contain any inspected variables.
+ *
+ * @param string aName
+ * The scope's name (e.g. "Local", "Global" etc.).
+ * @return Scope
+ * The newly created Scope instance.
+ */
+ addScope: function(aName = "") {
+ this._removeEmptyNotice();
+ this._toggleSearchVisibility(true);
+
+ let scope = new Scope(this, aName);
+ 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;
+ }
+ // Check if this empty operation may be executed lazily.
+ if (this.lazyEmpty && aTimeout > 0) {
+ this._emptySoon(aTimeout);
+ return;
+ }
+
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._store.length = 0;
+ this._itemsByElement.clear();
+
+ 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._store.length = 0;
+ this._itemsByElement.clear();
+
+ this._emptyTimeout = this.window.setTimeout(() => {
+ this._emptyTimeout = null;
+
+ prevList.removeEventListener("keypress", this._onViewKeyPress, false);
+ currList.addEventListener("keypress", this._onViewKeyPress, false);
+ currList.setAttribute("orient", "vertical");
+
+ this._parent.removeChild(prevList);
+ this._parent.appendChild(currList);
+ this._boxObject = currList.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ if (!this._store.length) {
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ }
+ }, aTimeout);
+ },
+
+ /**
+ * 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 added lazily.
+ * @see Scope.prototype._lazyAppend
+ */
+ lazyAppend: true,
+
+ /**
+ * Specifies if nodes in this view may be expanded lazily.
+ * @see Scope.prototype.expand
+ */
+ lazyExpand: true,
+
+ /**
+ * 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,
+
+ /**
+ * Specifies if after an eval or switch operation, the variable or property
+ * which has been edited should be disabled.
+ */
+ preventDisableOnChage: 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: STR.GetStringFromName("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: STR.GetStringFromName("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: STR.GetStringFromName("variablesEditButtonTooltip"),
+
+ /**
+ * 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: STR.GetStringFromName("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: STR.GetStringFromName("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() !!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() 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 ownerView = 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-searchinput";
+ searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
+ searchbox.setAttribute("type", "search");
+ searchbox.setAttribute("flex", "1");
+ searchbox.addEventListener("input", this._onSearchboxInput, false);
+ searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ container.appendChild(searchbox);
+ ownerView.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("input", 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.performSearch(this._searchboxNode.value);
+ },
+
+ /**
+ * Listener handling the searchbox key press event.
+ */
+ _onSearchboxKeyPress: function(e) {
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._onSearchboxInput();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._searchboxNode.value = "";
+ this._onSearchboxInput();
+ return;
+ }
+ },
+
+ /**
+ * Allows searches to be scheduled and delayed to avoid redundant calls.
+ */
+ delayedSearch: true,
+
+ /**
+ * Schedules searching for variables or properties matching the query.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ scheduleSearch: function(aQuery) {
+ if (!this.delayedSearch) {
+ this.performSearch(aQuery);
+ return;
+ }
+ let delay = Math.max(SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
+
+ this.window.clearTimeout(this._searchTimeout);
+ this._searchFunction = this._startSearch.bind(this, aQuery);
+ this._searchTimeout = this.window.setTimeout(this._searchFunction, delay);
+ },
+
+ /**
+ * Immediately searches for variables or properties matching the query.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ performSearch: function(aQuery) {
+ this.window.clearTimeout(this._searchTimeout);
+ this._searchFunction = null;
+ this._startSearch(aQuery);
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * If aQuery is empty string, then all the scopes are unhidden and expanded,
+ * while the available variables and properties inside those scopes are
+ * just unhidden.
+ *
+ * If aQuery is null or undefined, then all the scopes are just unhidden,
+ * and the available variables and properties inside those scopes are also
+ * just unhidden.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ _startSearch: function(aQuery) {
+ for (let scope of this._store) {
+ switch (aQuery) {
+ case "":
+ scope.expand();
+ // fall through
+ case null:
+ case undefined:
+ scope._performSearch("");
+ break;
+ default:
+ scope._performSearch(aQuery.toLowerCase());
+ break;
+ }
+ }
+ },
+
+ /**
+ * Expands the first search results in this container.
+ */
+ expandFirstSearchResults: function() {
+ for (let scope of this._store) {
+ let match = scope._firstMatch;
+ if (match) {
+ match.expand();
+ }
+ }
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * Searches for the scope in this container displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Scope
+ * The matched scope, or null if nothing is found.
+ */
+ getScopeForNode: function(aNode) {
+ let item = this._itemsByElement.get(aNode);
+ // Match only Scopes, not Variables or Properties.
+ if (item && !(item instanceof Variable)) {
+ return item;
+ }
+ return null;
+ },
+
+ /**
+ * 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 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 e.DOM_VK_UP:
+ // Always rewind focus.
+ this.focusPrevItem(true);
+ return;
+
+ case e.DOM_VK_DOWN:
+ // Always advance focus.
+ this.focusNextItem(true);
+ return;
+
+ case e.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 e.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 e.DOM_VK_PAGE_UP:
+ // Rewind a certain number of elements based on the container height.
+ this.focusItemAtDelta(-(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case e.DOM_VK_PAGE_DOWN:
+ // Advance a certain number of elements based on the container height.
+ this.focusItemAtDelta(+(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case e.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+
+ case e.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ // 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 e.DOM_VK_DELETE:
+ case e.DOM_VK_BACK_SPACE:
+ // Delete the Variable or Property if allowed.
+ if (item instanceof Variable) {
+ item._onDelete(e);
+ }
+ return;
+ }
+ },
+
+ /**
+ * 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.
+ */
+ pageSize: 0,
+
+ /**
+ * 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 the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get parentNode() this._parent,
+
+ /**
+ * Gets the owner document holding this view.
+ * @return nsIHTMLDocument
+ */
+ get document() this._document || (this._document = this._parent.ownerDocument),
+
+ /**
+ * Gets the default window holding this view.
+ * @return nsIDOMWindow
+ */
+ get window() this._window || (this._window = this.document.defaultView),
+
+ _document: null,
+ _window: null,
+
+ _store: null,
+ _prevHierarchy: null,
+ _currHierarchy: null,
+ _enumVisible: true,
+ _nonEnumVisible: true,
+ _emptyTimeout: null,
+ _searchTimeout: null,
+ _searchFunction: null,
+ _parent: null,
+ _list: null,
+ _boxObject: null,
+ _searchboxNode: null,
+ _searchboxContainer: null,
+ _searchboxPlaceholder: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+VariablesView.NON_SORTABLE_CLASSES = [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array"
+];
+
+/**
+ * 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 = "\"" + 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 = "\"" + 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.contains("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.evaluationMacro(aItem, ""));
+
+ return true; // Don't hide the element.
+};
+
+
+/**
+ * A Scope is an object holding Variable instances.
+ * Iterable via "for (let [name, variable] in 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);
+ this._batchAppend = this._batchAppend.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.eval = aView.eval;
+ this.switch = aView.switch;
+ this.delete = aView.delete;
+ this.editableValueTooltip = aView.editableValueTooltip;
+ this.editableNameTooltip = aView.editableNameTooltip;
+ this.editButtonTooltip = aView.editButtonTooltip;
+ this.deleteButtonTooltip = aView.deleteButtonTooltip;
+ this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
+ this.contextMenuId = aView.contextMenuId;
+ this.separatorStr = aView.separatorStr;
+
+ // 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.
+ XPCOMUtils.defineLazyGetter(this, "_store", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_enumItems", () => []);
+ XPCOMUtils.defineLazyGetter(this, "_nonEnumItems", () => []);
+ XPCOMUtils.defineLazyGetter(this, "_batchItems", () => []);
+
+ this._init(aName.trim(), aFlags);
+}
+
+Scope.prototype = {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ shouldPrefetch: true,
+
+ /**
+ * 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.
+ * @return Variable
+ * The newly created child Variable.
+ */
+ _createChild: function(aName, aDescriptor) {
+ return new Variable(this, aName, aDescriptor);
+ },
+
+ /**
+ * 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.
+ * 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 boolean aRelaxed
+ * True if name duplicates should be allowed.
+ * @return Variable
+ * The newly created Variable instance, null if it already exists.
+ */
+ addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
+ if (this._store.has(aName) && !aRelaxed) {
+ return null;
+ }
+
+ let child = this._createChild(aName, aDescriptor);
+ this._store.set(aName, child);
+ this._variablesView._itemsByElement.set(child._target, child);
+ this._variablesView._currHierarchy.set(child._absoluteName, child);
+ child.header = !!aName;
+ 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();
+ }
+ // 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.value);
+ }
+ }
+ },
+
+ /**
+ * 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._locked) {
+ return;
+ }
+ // If there's a large number of enumerable or non-enumerable items
+ // contained in this scope, painting them may take several seconds,
+ // even if they were already displayed before. In this case, show a throbber
+ // to suggest that this scope is expanding.
+ if (!this._isExpanding &&
+ this._variablesView.lazyExpand &&
+ this._store.size > LAZY_APPEND_BATCH) {
+ this._isExpanding = true;
+
+ // Start spinning a throbber in this scope's title and allow a few
+ // milliseconds for it to be painted.
+ this._startThrobber();
+ this.window.setTimeout(this.expand.bind(this), LAZY_EXPAND_DELAY);
+ return;
+ }
+
+ if (this._variablesView._enumVisible) {
+ this._openEnum();
+ }
+ if (this._variablesView._nonEnumVisible) {
+ Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
+ }
+ this._isExpanding = false;
+ this._isExpanded = true;
+
+ if (this.onexpand) {
+ this.onexpand(this);
+ }
+ },
+
+ /**
+ * Collapses the scope, hiding all the added details.
+ */
+ collapse: function() {
+ if (!this._isExpanded || this._locked) {
+ 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._wasToggled = true;
+ 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("non-header");
+ 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("non-header", "");
+ this._isHeaderVisible = false;
+ },
+
+ /**
+ * 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() this._isContentVisible,
+
+ /**
+ * Gets the expanded state.
+ * @return boolean
+ */
+ get expanded() this._isExpanded,
+
+ /**
+ * Gets the header visibility state.
+ * @return boolean
+ */
+ get header() this._isHeaderVisible,
+
+ /**
+ * Gets the twisty visibility state.
+ * @return boolean
+ */
+ get twisty() this._isArrowVisible,
+
+ /**
+ * Gets the expand lock state.
+ * @return boolean
+ */
+ get locked() this._locked,
+
+ /**
+ * 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._locked = 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() this._idString,
+
+ /**
+ * Gets the name associated with this item.
+ * @return string
+ */
+ get name() this._nameString,
+
+ /**
+ * Gets the displayed value for this item.
+ * @return string
+ */
+ get displayValue() this._valueString,
+
+ /**
+ * Gets the class names used for the displayed value.
+ * @return string
+ */
+ get displayValueClassName() this._valueClassName,
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() 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, "variables-view-scope", "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 aClassName
+ * A custom class name for this scope.
+ * @param string aTitleClassName [optional]
+ * A custom class name for this scope's title.
+ */
+ _displayScope: function(aName, aClassName, aTitleClassName) {
+ let document = this.document;
+
+ let element = this._target = document.createElement("vbox");
+ element.id = this._idString;
+ element.className = aClassName;
+
+ let arrow = this._arrow = document.createElement("hbox");
+ arrow.className = "arrow";
+
+ let name = this._name = document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName);
+
+ 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 (e.target == this._inputNode ||
+ e.target == this._editNode ||
+ e.target == this._deleteNode) {
+ return;
+ }
+ this.toggle();
+ this.focus();
+ },
+
+ /**
+ * Lazily appends a node to this scope's enumerable or non-enumerable
+ * container. Once a certain number of nodes have been batched, they
+ * will be appended.
+ *
+ * @param boolean aImmediateFlag
+ * Set to false if append calls should be dispatched synchronously
+ * on the current thread, to allow for a paint flush.
+ * @param boolean aEnumerableFlag
+ * Specifies if the node to append is enumerable or non-enumerable.
+ * @param nsIDOMNode aChild
+ * The child node to append.
+ */
+ _lazyAppend: function(aImmediateFlag, aEnumerableFlag, aChild) {
+ // Append immediately, don't stage items and don't allow for a paint flush.
+ if (aImmediateFlag || !this._variablesView.lazyAppend) {
+ if (aEnumerableFlag) {
+ this._enum.appendChild(aChild);
+ } else {
+ this._nonenum.appendChild(aChild);
+ }
+ return;
+ }
+
+ let window = this.window;
+ let batchItems = this._batchItems;
+
+ window.clearTimeout(this._batchTimeout);
+ batchItems.push({ enumerableFlag: aEnumerableFlag, child: aChild });
+
+ // If a certain number of nodes have been batched, append all the
+ // staged items now.
+ if (batchItems.length > LAZY_APPEND_BATCH) {
+ // Allow for a paint flush.
+ Services.tm.currentThread.dispatch({ run: this._batchAppend }, 1);
+ return;
+ }
+ // Postpone appending the staged items for later, to allow batching
+ // more nodes.
+ this._batchTimeout = window.setTimeout(this._batchAppend, LAZY_APPEND_DELAY);
+ },
+
+ /**
+ * Appends all the batched nodes to this scope's enumerable and non-enumerable
+ * containers.
+ */
+ _batchAppend: function() {
+ let document = this.document;
+ let batchItems = this._batchItems;
+
+ // Create two document fragments, one for enumerable nodes, and one
+ // for non-enumerable nodes.
+ let frags = [document.createDocumentFragment(), document.createDocumentFragment()];
+
+ for (let item of batchItems) {
+ frags[~~item.enumerableFlag].appendChild(item.child);
+ }
+ batchItems.length = 0;
+ this._enum.appendChild(frags[1]);
+ this._nonenum.appendChild(frags[0]);
+ },
+
+ /**
+ * Starts spinning a throbber in this scope's title.
+ */
+ _startThrobber: function() {
+ if (this._throbber) {
+ this._throbber.hidden = false;
+ return;
+ }
+ let throbber = this._throbber = this.document.createElement("hbox");
+ throbber.className = "variables-view-throbber";
+ this._title.appendChild(throbber);
+ },
+
+ /**
+ * Stops spinning the throbber in this scope's title.
+ */
+ _stopThrobber: function() {
+ if (!this._throbber) {
+ return;
+ }
+ this._throbber.hidden = true;
+ },
+
+ /**
+ * Opens the enumerable items container.
+ */
+ _openEnum: function() {
+ this._arrow.setAttribute("open", "");
+ this._enum.setAttribute("open", "");
+ this._stopThrobber();
+ },
+
+ /**
+ * Opens the non-enumerable items container.
+ */
+ _openNonEnum: function() {
+ this._nonenum.setAttribute("open", "");
+ this._stopThrobber();
+ },
+
+ /**
+ * 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.contains(aLowerCaseQuery) &&
+ !lowerCaseValue.contains(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._wasToggled && aLowerCaseQuery) {
+ variable.expand();
+ }
+ if (variable._isExpanded && !aLowerCaseQuery) {
+ variable._wasToggled = true;
+ }
+
+ // 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) && /* Parent object exists. */
+ variable instanceof Scope) {
+
+ // Show and expand the parent, as it is certainly accessible.
+ variable._matched = true;
+ aLowerCaseQuery && variable.expand();
+ }
+ }
+
+ // Proceed with the search recursively inside this variable or property.
+ if (currentObject._wasToggled ||
+ 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("non-match");
+ } else {
+ this._isMatch = false;
+ this.target.setAttribute("non-match", "");
+ }
+ },
+
+ /**
+ * Gets the first search results match in this scope.
+ * @return Variable | Property
+ */
+ get _firstMatch() {
+ for (let [, variable] of this._store) {
+ let match;
+ if (variable._isMatch) {
+ match = variable;
+ } else {
+ match = variable._firstMatch;
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * 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() this._topView || (this._topView = (function(self) {
+ let parentView = self.ownerView;
+ let topView;
+
+ while (topView = parentView.ownerView) {
+ parentView = topView;
+ }
+ return parentView;
+ })(this)),
+
+ /**
+ * Gets the parent node holding this scope.
+ * @return nsIDOMNode
+ */
+ get parentNode() this.ownerView._list,
+
+ /**
+ * Gets the owner document holding this scope.
+ * @return nsIHTMLDocument
+ */
+ get document() this._document || (this._document = this.ownerView.document),
+
+ /**
+ * Gets the default window holding this scope.
+ * @return nsIDOMWindow
+ */
+ get window() this._window || (this._window = this.ownerView.window),
+
+ _topView: null,
+ _document: null,
+ _window: null,
+
+ ownerView: null,
+ eval: null,
+ switch: null,
+ delete: null,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ editButtonTooltip: "",
+ deleteButtonTooltip: "",
+ preventDescriptorModifiers: false,
+ contextMenuId: "",
+ separatorStr: "",
+
+ _store: null,
+ _enumItems: null,
+ _nonEnumItems: null,
+ _fetched: false,
+ _retrieved: false,
+ _committed: false,
+ _batchItems: null,
+ _batchTimeout: null,
+ _locked: false,
+ _isExpanding: false,
+ _isExpanded: false,
+ _wasToggled: false,
+ _isContentVisible: true,
+ _isHeaderVisible: true,
+ _isArrowVisible: true,
+ _isMatch: true,
+ _idString: "",
+ _nameString: "",
+ _target: null,
+ _arrow: null,
+ _name: null,
+ _title: null,
+ _enum: null,
+ _nonenum: null,
+ _throbber: null
+};
+
+/**
+ * A Variable is a Scope holding Property instances.
+ * Iterable via "for (let [name, property] in instance) { }".
+ *
+ * @param Scope aScope
+ * The scope to contain this variable.
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+function Variable(aScope, aName, aDescriptor) {
+ this._setTooltips = this._setTooltips.bind(this);
+ this._activateNameInput = this._activateNameInput.bind(this);
+ this._activateValueInput = this._activateValueInput.bind(this);
+
+ // 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);
+ this._symbolicName = aName;
+ this._absoluteName = aScope.name + "[\"" + aName + "\"]";
+}
+
+Variable.prototype = Heritage.extend(Scope.prototype, {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ get shouldPrefetch(){
+ return this.name == "window" || this.name == "this";
+ },
+
+ /**
+ * 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.
+ * @return Property
+ * The newly created child Property.
+ */
+ _createChild: function(aName, aDescriptor) {
+ return new Property(this, aName, aDescriptor);
+ },
+
+ /**
+ * 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();
+ }
+ // 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.
+ * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
+ * @return string
+ */
+ get symbolicName() this._symbolicName,
+
+ /**
+ * Returns this variable's value from the descriptor if available.
+ * @return any
+ */
+ get value() this._initialDescriptor.value,
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get getter() this._initialDescriptor.get,
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get setter() 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.
+ if (!this._nameString || aGrip === undefined || aGrip === null) {
+ return;
+ }
+ // Getters and setters should display grip information in sub-properties.
+ if (!this._isUndefined && (this.getter || this.setter)) {
+ this._valueLabel.setAttribute("value", "");
+ return;
+ }
+
+ // Make sure the value is escaped unicode if it's a string.
+ if (typeof aGrip == "string") {
+ aGrip = NetworkHelper.convertToUnicode(unescape(aGrip));
+ }
+
+ let prevGrip = this._valueGrip;
+ if (prevGrip) {
+ this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
+ }
+ this._valueGrip = aGrip;
+ this._valueString = VariablesView.getString(aGrip);
+ this._valueClassName = VariablesView.getClass(aGrip);
+
+ this._valueLabel.classList.add(this._valueClassName);
+ this._valueLabel.setAttribute("value", this._valueString);
+ },
+
+ /**
+ * 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, "variables-view-variable variable-or-property");
+
+ // Don't allow displaying variable information there's no name available.
+ if (this._nameString) {
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+ }
+
+ this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
+ },
+
+ /**
+ * Called when this variable has finished initializing, and is ready to
+ * be attached to the owner view.
+ *
+ * @param boolean aImmediateFlag
+ * @see Scope.prototype._lazyAppend
+ */
+ _onInit: function(aImmediateFlag) {
+ if (this._initialDescriptor.enumerable ||
+ this._nameString == "this" ||
+ this._nameString == "<return>" ||
+ this._nameString == "<exception>") {
+ this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._lazyAppend(aImmediateFlag, false, 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.ownerView.separatorStr);
+
+ let valueLabel = this._valueLabel = document.createElement("label");
+ valueLabel.className = "plain value";
+ valueLabel.setAttribute("crop", "center");
+ valueLabel.setAttribute('flex', "1");
+
+ this._title.appendChild(separatorLabel);
+ this._title.appendChild(valueLabel);
+
+ let isPrimitive = this._isPrimitive = VariablesView.isPrimitive(descriptor);
+ let isUndefined = this._isUndefined = VariablesView.isUndefined(descriptor);
+
+ if (isPrimitive || isUndefined) {
+ this.hideArrow();
+ }
+ if (!isUndefined && (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) {
+ if (!this._isUndefined && (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.appendChild(editNode);
+ }
+ }
+ if (ownerView.delete) {
+ if (!this._isUndefined || !(ownerView.getter && ownerView.setter)) {
+ let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
+ deleteNode.className = "plain variables-view-delete";
+ deleteNode.setAttribute("ordinal", 2);
+ deleteNode.addEventListener("click", this._onDelete.bind(this), false);
+ this._title.appendChild(deleteNode);
+ }
+ }
+ 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 = "variable-or-property-non-writable-icon";
+ 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("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("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("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", "WebIDL"];
+
+ for (let label of labels) {
+ let labelElement = this.document.createElement("label");
+ labelElement.setAttribute("value", label);
+ 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._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);
+ }
+ },
+
+ /**
+ * Sets a variable's configurable, enumerable and writable attributes,
+ * and specifies if it's a 'this', '<exception>' 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 (name == "<exception>") {
+ target.setAttribute("exception", "");
+ }
+ else if (name == "<return>") {
+ target.setAttribute("return", "");
+ }
+ else if (name == "__proto__") {
+ target.setAttribute("proto", "");
+ }
+ },
+
+ /**
+ * 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);
+ },
+
+ /**
+ * Creates a textbox node in place of a label.
+ *
+ * @param nsIDOMNode aLabel
+ * The label to be replaced with a textbox.
+ * @param string aClassName
+ * The class to be applied to the textbox.
+ * @param object aCallbacks
+ * An object containing the onKeypress and onBlur callbacks.
+ */
+ _activateInput: function(aLabel, aClassName, aCallbacks) {
+ let initialString = aLabel.getAttribute("value");
+
+ // Create a texbox input element which will be shown in the current
+ // element's specified label location.
+ let input = this.document.createElement("textbox");
+ input.className = "plain " + aClassName;
+ input.setAttribute("value", initialString);
+ input.setAttribute("flex", "1");
+
+ // Replace the specified label with a textbox input element.
+ aLabel.parentNode.replaceChild(input, aLabel);
+ this._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 (aLabel.getAttribute("value").match(/^".+"$/)) {
+ input.selectionEnd--;
+ input.selectionStart++;
+ }
+
+ input.addEventListener("keypress", aCallbacks.onKeypress, false);
+ input.addEventListener("blur", aCallbacks.onBlur, false);
+
+ this._prevExpandable = this.twisty;
+ this._prevExpanded = this.expanded;
+ this.collapse();
+ this.hideArrow();
+ this._locked = true;
+
+ this._inputNode = input;
+ this._stopThrobber();
+ },
+
+ /**
+ * Removes the textbox node in place of a label.
+ *
+ * @param nsIDOMNode aLabel
+ * The label which was replaced with a textbox.
+ * @param object aCallbacks
+ * An object containing the onKeypress and onBlur callbacks.
+ */
+ _deactivateInput: function(aLabel, aInput, aCallbacks) {
+ aInput.parentNode.replaceChild(aLabel, aInput);
+ this._variablesView._boxObject.scrollBy(-this._target.clientWidth, 0);
+
+ aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
+ aInput.removeEventListener("blur", aCallbacks.onBlur, false);
+
+ this._locked = false;
+ this.twisty = this._prevExpandable;
+ this.expanded = this._prevExpanded;
+
+ this._inputNode = null;
+ this._stopThrobber();
+ },
+
+ /**
+ * Makes this variable's name editable.
+ */
+ _activateNameInput: function(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ if (!this.ownerView.switch) {
+ return;
+ }
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
+ this._deactivateNameInput = this._deactivateNameInput.bind(this);
+
+ this._activateInput(this._name, "element-name-input", {
+ onKeypress: this._onNameInputKeyPress,
+ onBlur: this._deactivateNameInput
+ });
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ },
+
+ /**
+ * Deactivates this variable's editable name mode.
+ */
+ _deactivateNameInput: function(e) {
+ this._deactivateInput(this._name, e.target, {
+ onKeypress: this._onNameInputKeyPress,
+ onBlur: this._deactivateNameInput
+ });
+ this._separatorLabel.hidden = false;
+ this._valueLabel.hidden = false;
+ },
+
+ /**
+ * Makes this variable's value editable.
+ */
+ _activateValueInput: function(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ if (!this.ownerView.eval) {
+ return;
+ }
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
+ this._deactivateValueInput = this._deactivateValueInput.bind(this);
+
+ this._activateInput(this._valueLabel, "element-value-input", {
+ onKeypress: this._onValueInputKeyPress,
+ onBlur: this._deactivateValueInput
+ });
+ },
+
+ /**
+ * Deactivates this variable's editable value mode.
+ */
+ _deactivateValueInput: function(e) {
+ this._deactivateInput(this._valueLabel, e.target, {
+ onKeypress: this._onValueInputKeyPress,
+ onBlur: this._deactivateValueInput
+ });
+ },
+
+ /**
+ * Disables this variable prior to a new name switch or value evaluation.
+ */
+ _disable: function() {
+ this.hideArrow();
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ this._enum.hidden = true;
+ this._nonenum.hidden = true;
+
+ if (this._editNode) {
+ this._editNode.hidden = true;
+ }
+ if (this._deleteNode) {
+ this._deleteNode.hidden = true;
+ }
+ },
+
+ /**
+ * Deactivates this variable's editable mode and callbacks the new name.
+ */
+ _saveNameInput: function(e) {
+ let input = e.target;
+ let initialString = this._name.getAttribute("value");
+ let currentString = input.value.trim();
+ this._deactivateNameInput(e);
+
+ if (initialString != currentString) {
+ if (!this._variablesView.preventDisableOnChage) {
+ this._disable();
+ this._name.value = currentString;
+ }
+ this.ownerView.switch(this, currentString);
+ }
+ },
+
+ /**
+ * Deactivates this variable's editable mode and evaluates the new value.
+ */
+ _saveValueInput: function(e) {
+ let input = e.target;
+ let initialString = this._valueLabel.getAttribute("value");
+ let currentString = input.value.trim();
+ this._deactivateValueInput(e);
+
+ if (initialString != currentString) {
+ if (!this._variablesView.preventDisableOnChage) {
+ this._disable();
+ }
+ this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
+ }
+ },
+
+ /**
+ * The current macro used to generate the string evaluated when performing
+ * a variable or property value change.
+ */
+ evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * The key press listener for this variable's editable name textbox.
+ */
+ _onNameInputKeyPress: function(e) {
+ e.stopPropagation();
+
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._saveNameInput(e);
+ this.focus();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._deactivateNameInput(e);
+ this.focus();
+ return;
+ }
+ },
+
+ /**
+ * The key press listener for this variable's editable value textbox.
+ */
+ _onValueInputKeyPress: function(e) {
+ e.stopPropagation();
+
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._saveValueInput(e);
+ this.focus();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._deactivateValueInput(e);
+ this.focus();
+ return;
+ }
+ },
+
+ /**
+ * The click listener for the edit button.
+ */
+ _onEdit: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._activateValueInput();
+ },
+
+ /**
+ * The click listener for the delete button.
+ */
+ _onDelete: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.ownerView.delete) {
+ if (!this.ownerView.delete(this)) {
+ this.hide();
+ }
+ }
+ },
+
+ _symbolicName: "",
+ _absoluteName: "",
+ _initialDescriptor: null,
+ _isPrimitive: false,
+ _isUndefined: false,
+ _separatorLabel: null,
+ _valueLabel: null,
+ _inputNode: null,
+ _editNode: null,
+ _deleteNode: 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] in instance) { }".
+ *
+ * @param Variable aVar
+ * The variable to contain this property.
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ */
+function Property(aVar, aName, aDescriptor) {
+ Variable.call(this, aVar, aName, aDescriptor);
+ this._symbolicName = aVar._symbolicName + "[\"" + aName + "\"]";
+ this._absoluteName = aVar._absoluteName + "[\"" + aName + "\"]";
+}
+
+Property.prototype = Heritage.extend(Variable.prototype, {
+ /**
+ * Initializes this property's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ */
+ _init: function(aName, aDescriptor) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, "variables-view-property variable-or-property");
+
+ // Don't allow displaying property information there's no name available.
+ if (this._nameString) {
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+ }
+
+ this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
+ },
+
+ /**
+ * Called when this property has finished initializing, and is ready to
+ * be attached to the owner view.
+ *
+ * @param boolean aImmediateFlag
+ * @see Scope.prototype._lazyAppend
+ */
+ _onInit: function(aImmediateFlag) {
+ if (this._initialDescriptor.enumerable) {
+ this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._lazyAppend(aImmediateFlag, false, this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ }
+});
+
+/**
+ * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
+ */
+VariablesView.prototype.__iterator__ =
+Scope.prototype.__iterator__ =
+Variable.prototype.__iterator__ =
+Property.prototype.__iterator__ = function() {
+ for (let item of this._store) {
+ yield item;
+ }
+};
+
+/**
+ * Forget everything recorded about added scopes, variables or properties.
+ * @see VariablesView.createHierarchy
+ */
+VariablesView.prototype.clearHierarchy = function() {
+ this._prevHierarchy.clear();
+ this._currHierarchy.clear();
+};
+
+/**
+ * Start recording a hierarchy of any added scopes, variables or properties.
+ * @see VariablesView.commitHierarchy
+ */
+VariablesView.prototype.createHierarchy = function() {
+ this._prevHierarchy = this._currHierarchy;
+ this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
+};
+
+/**
+ * Briefly flash the variables that changed between the previous and current
+ * scope/variable/property hierarchies and reopen previously expanded nodes.
+ */
+VariablesView.prototype.commitHierarchy = function() {
+ let prevHierarchy = this._prevHierarchy;
+ let currHierarchy = this._currHierarchy;
+
+ for (let [absoluteName, currVariable] of currHierarchy) {
+ // Ignore variables which were already commmitted.
+ if (currVariable._committed) {
+ continue;
+ }
+ // Avoid performing expensive operations.
+ if (this.commitHierarchyIgnoredItems[currVariable._nameString]) {
+ continue;
+ }
+
+ // Try to get the previous instance of the inspected variable to
+ // determine the difference in state.
+ let prevVariable = prevHierarchy.get(absoluteName);
+ let expanded = false;
+ let changed = false;
+
+ // If the inspected variable existed in a previous hierarchy, check if
+ // the displayed value (a representation of the grip) has changed and if
+ // it was previously expanded.
+ if (prevVariable) {
+ expanded = prevVariable._isExpanded;
+
+ // Only analyze Variables and Properties for displayed value changes.
+ if (currVariable instanceof Variable) {
+ changed = prevVariable._valueString != currVariable._valueString;
+ }
+ }
+
+ // Make sure this variable is not handled in ulteror commits for the
+ // same hierarchy.
+ currVariable._committed = true;
+
+ // Re-expand the variable if not previously collapsed.
+ if (expanded) {
+ currVariable._wasToggled = prevVariable._wasToggled;
+ currVariable.expand();
+ }
+ // This variable was either not changed or removed, no need to continue.
+ if (!changed) {
+ continue;
+ }
+
+ // Apply an attribute determining the flash type and duration.
+ // Dispatch this action after all the nodes have been drawn, so that
+ // the transition efects can take place.
+ this.window.setTimeout(function(aTarget) {
+ aTarget.addEventListener("transitionend", function onEvent() {
+ aTarget.removeEventListener("transitionend", onEvent, false);
+ aTarget.removeAttribute("changed");
+ }, false);
+ aTarget.setAttribute("changed", "");
+ }.bind(this, currVariable.target), this.lazyEmptyDelay + 1);
+ }
+};
+
+// 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 = Object.create(null, {
+ "window": { value: true }
+});
+
+/**
+ * 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 and long strings are considered types.
+ let type = grip.type;
+ if (type == "undefined" || type == "null" || 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 and null are both considered types.
+ let type = grip.type;
+ if (type == "undefined" || type == "null") {
+ 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) {
+ if (aValue === undefined) {
+ return { type: "undefined" };
+ }
+ if (aValue === null) {
+ return { type: "null" };
+ }
+ if (typeof aValue == "object" || typeof aValue == "function") {
+ return { type: "object", class: WebConsoleUtils.getObjectClassName(aValue) };
+ }
+ return aValue;
+};
+
+/**
+ * Returns a custom formatted property string for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @param boolean aConciseFlag
+ * Return a concisely formatted property string.
+ * @return string
+ * The formatted property string.
+ */
+VariablesView.getString = function(aGrip, aConciseFlag) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ return "undefined";
+ case "null":
+ return "null";
+ case "longString":
+ return "\"" + aGrip.initial + "\"";
+ default:
+ if (!aConciseFlag) {
+ return "[" + aGrip.type + " " + aGrip.class + "]";
+ } else {
+ return aGrip.class;
+ }
+ }
+ } else {
+ switch (typeof aGrip) {
+ case "string":
+ return "\"" + aGrip + "\"";
+ case "boolean":
+ return aGrip ? "true" : "false";
+ }
+ }
+ return aGrip + "";
+};
+
+/**
+ * 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") {
+ switch (aGrip.type) {
+ case "undefined":
+ return "token-undefined";
+ case "null":
+ return "token-null";
+ case "longString":
+ return "token-string";
+ }
+ } else {
+ switch (typeof aGrip) {
+ case "string":
+ return "token-string";
+ case "boolean":
+ return "token-boolean";
+ case "number":
+ return "token-number";
+ }
+ }
+ 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.
+ */
+let generateId = (function() {
+ let count = 0;
+ return function(aName = "") {
+ return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
+ };
+})();
diff --git a/browser/devtools/shared/widgets/VariablesView.xul b/browser/devtools/shared/widgets/VariablesView.xul
new file mode 100644
index 000000000..2269e65c9
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesView.xul
@@ -0,0 +1,16 @@
+<?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://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % viewDTD SYSTEM "chrome://browser/locale/devtools/VariablesView.dtd">
+ %viewDTD;
+]>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&PropertiesViewWindowTitle;">
+ <vbox id="variables" flex="1"/>
+</window>
diff --git a/browser/devtools/shared/widgets/VariablesViewController.jsm b/browser/devtools/shared/widgets/VariablesViewController.jsm
new file mode 100644
index 000000000..56704b2d2
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,350 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+ Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+const MAX_LONG_STRING_LENGTH = 200000;
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController"];
+
+
+/**
+ * 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
+ * Options for configuring the controller. Supported options:
+ * - getGripClient: callback for creating an object grip client
+ * - getLongStringClient: callback for creating a long string grip client
+ * - releaseActor: callback for releasing an actor when it's no longer needed
+ * - 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
+ */
+function VariablesViewController(aView, aOptions) {
+ this.addExpander = this.addExpander.bind(this);
+
+ this._getGripClient = aOptions.getGripClient;
+ this._getLongStringClient = aOptions.getLongStringClient;
+ this._releaseActor = aOptions.releaseActor;
+
+ if (aOptions.overrideValueEvalMacro) {
+ this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+ }
+ if (aOptions.getterOrSetterEvalMacro) {
+ this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+ }
+ if (aOptions.simpleValueEvalMacro) {
+ this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+ }
+
+ this._actors = new Set();
+ this.view = aView;
+ this.view.controller = this;
+}
+
+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,
+
+ /**
+ * 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 = Promise.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();
+
+ // Mark the string as having retrieved.
+ aTarget._retrieved = true;
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * 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) {
+ let deferred = Promise.defer();
+
+ this._getGripClient(aGrip).getPrototypeAndProperties(aResponse => {
+ let { ownProperties, prototype, safeGetterValues } = aResponse;
+ 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) {
+ ownProperties[name].getterValue = safeGetterValues[name].getterValue;
+ ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
+ .getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+
+ // Add all the variable properties.
+ if (ownProperties) {
+ 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__.
+ if (prototype && prototype.type != "null") {
+ let proto = aTarget.addItem("__proto__", { value: prototype });
+ // Expansion handlers must be set after the properties are added.
+ this.addExpander(proto, prototype);
+ }
+
+ // Mark the variable as having retrieved all its properties.
+ aTarget._retrieved = true;
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds an 'onexpand' callback for a variable, lazily handling
+ * the addition of new properties.
+ *
+ * @param Variable aVar
+ * 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.expand(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.
+ *
+ * @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.
+ */
+ expand: function(aTarget, aSource) {
+ // Fetch the variables only once.
+ if (aTarget._fetched) {
+ return aTarget._fetched;
+ }
+
+ let deferred = Promise.defer();
+ aTarget._fetched = deferred.promise;
+
+ if (!aSource) {
+ throw new Error("No actor grip was given for the variable.");
+ }
+
+ // If the target a Variable or Property then we're fetching properties
+ if (VariablesView.isVariable(aTarget)) {
+ this._populateFromObject(aTarget, aSource).then(() => {
+ deferred.resolve();
+ // Signal that properties have been fetched.
+ this.view.emit("fetched", "properties", aTarget);
+ });
+ return deferred.promise;
+ }
+
+ switch (aSource.type) {
+ case "longString":
+ this._populateFromLongString(aTarget, aSource).then(() => {
+ deferred.resolve();
+ // Signal that a long string has been fetched.
+ this.view.emit("fetched", "longString", aTarget);
+ });
+ break;
+ case "with":
+ case "object":
+ this._populateFromObject(aTarget, aSource.object).then(() => {
+ deferred.resolve();
+ // Signal that variables have been fetched.
+ this.view.emit("fetched", "variables", aTarget);
+ });
+ break;
+ case "block":
+ case "function":
+ // Add nodes for every argument and every other variable in scope.
+ let args = aSource.bindings.arguments;
+ if (args) {
+ for (let arg of args) {
+ let name = Object.getOwnPropertyNames(arg)[0];
+ let ref = aTarget.addItem(name, arg[name]);
+ let val = arg[name].value;
+ this.addExpander(ref, val);
+ }
+ }
+
+ aTarget.addItems(aSource.bindings.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
+ });
+
+ // 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.
+
+ deferred.resolve();
+ break;
+ default:
+ let error = "Unknown Debugger.Environment type: " + aSource.type;
+ Cu.reportError(error);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * 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);
+ }
+ }
+ },
+};
+
+
+/**
+ * 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);
+};
diff --git a/browser/devtools/shared/widgets/ViewHelpers.jsm b/browser/devtools/shared/widgets/ViewHelpers.jsm
new file mode 100644
index 000000000..848fabd89
--- /dev/null
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -0,0 +1,1606 @@
+/* -*- 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const PANE_APPEARANCE_DELAY = 50;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["Heritage", "ViewHelpers", "WidgetMethods"];
+
+/**
+ * Inheritance helpers from the addon SDK's core/heritage.
+ * Remove these when all devtools are loadered.
+ */
+this.Heritage = {
+ /**
+ * @see extend in sdk/core/heritage.
+ */
+ extend: function(aPrototype, aProperties = {}) {
+ return Object.create(aPrototype, this.getOwnPropertyDescriptors(aProperties));
+ },
+
+ /**
+ * @see getOwnPropertyDescriptors in sdk/core/heritage.
+ */
+ getOwnPropertyDescriptors: function(aObject) {
+ return Object.getOwnPropertyNames(aObject).reduce((aDescriptor, aName) => {
+ aDescriptor[aName] = Object.getOwnPropertyDescriptor(aObject, aName);
+ return aDescriptor;
+ }, {});
+ }
+};
+
+/**
+ * Helpers for creating and messaging between UI components.
+ */
+this.ViewHelpers = {
+ /**
+ * Convenience method, dispatching a custom event.
+ *
+ * @param nsIDOMNode aTarget
+ * A custom target element to dispatch the event from.
+ * @param string aType
+ * The name of the event.
+ * @param any aDetail
+ * The data passed when initializing the event.
+ * @return boolean
+ * True if the event was cancelled or a registered handler
+ * called preventDefault.
+ */
+ dispatchEvent: function(aTarget, aType, aDetail) {
+ if (!(aTarget instanceof Ci.nsIDOMNode)) {
+ return true; // Event cancelled.
+ }
+ let document = aTarget.ownerDocument || aTarget;
+ let dispatcher = aTarget.ownerDocument ? aTarget : document.documentElement;
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent(aType, true, true, aDetail);
+ return dispatcher.dispatchEvent(event);
+ },
+
+ /**
+ * Helper delegating some of the DOM attribute methods of a node to a widget.
+ *
+ * @param object aWidget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode aNode
+ * A node to delegate the methods to.
+ */
+ delegateWidgetAttributeMethods: function(aWidget, aNode) {
+ aWidget.getAttribute = aNode.getAttribute.bind(aNode);
+ aWidget.setAttribute = aNode.setAttribute.bind(aNode);
+ aWidget.removeAttribute = aNode.removeAttribute.bind(aNode);
+ },
+
+ /**
+ * Helper delegating some of the DOM event methods of a node to a widget.
+ *
+ * @param object aWidget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode aNode
+ * A node to delegate the methods to.
+ */
+ delegateWidgetEventMethods: function(aWidget, aNode) {
+ aWidget.addEventListener = aNode.addEventListener.bind(aNode);
+ aWidget.removeEventListener = aNode.removeEventListener.bind(aNode);
+ },
+
+ /**
+ * 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(aObject) {
+ return aObject && aObject.on && aObject.off && aObject.once && aObject.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(aObject) {
+ return aObject instanceof Ci.nsIDOMNode ||
+ aObject instanceof Ci.nsIDOMElement ||
+ aObject instanceof Ci.nsIDOMDocumentFragment;
+ },
+
+ /**
+ * Prevents event propagation when navigation keys are pressed.
+ *
+ * @param Event e
+ * The event to be prevented.
+ */
+ preventScrolling: function(e) {
+ switch (e.keyCode) {
+ case e.DOM_VK_UP:
+ case e.DOM_VK_DOWN:
+ case e.DOM_VK_LEFT:
+ case e.DOM_VK_RIGHT:
+ case e.DOM_VK_PAGE_UP:
+ case e.DOM_VK_PAGE_DOWN:
+ case e.DOM_VK_HOME:
+ case e.DOM_VK_END:
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Sets a side 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 nsIDOMNode aPane
+ * The element representing the pane to toggle.
+ */
+ togglePane: function(aFlags, aPane) {
+ // Hiding is always handled via margins, not the hidden attribute.
+ aPane.removeAttribute("hidden");
+
+ // Add a class to the pane to handle min-widths, margins and animations.
+ if (!aPane.classList.contains("generic-toggled-side-pane")) {
+ aPane.classList.add("generic-toggled-side-pane");
+ }
+
+ // Avoid useless toggles.
+ if (aFlags.visible == !aPane.hasAttribute("pane-collapsed")) {
+ if (aFlags.callback) aFlags.callback();
+ return;
+ }
+
+ // Computes and sets the pane margins in order to hide or show it.
+ function set() {
+ if (aFlags.visible) {
+ aPane.style.marginLeft = "0";
+ aPane.style.marginRight = "0";
+ aPane.removeAttribute("pane-collapsed");
+ } else {
+ let margin = ~~(aPane.getAttribute("width")) + 1;
+ aPane.style.marginLeft = -margin + "px";
+ aPane.style.marginRight = -margin + "px";
+ aPane.setAttribute("pane-collapsed", "");
+ }
+
+ // Invoke the callback when the transition ended.
+ if (aFlags.animated) {
+ aPane.addEventListener("transitionend", function onEvent() {
+ aPane.removeEventListener("transitionend", onEvent, false);
+ if (aFlags.callback) aFlags.callback();
+ }, false);
+ }
+ // Invoke the callback immediately since there's no transition.
+ else {
+ if (aFlags.callback) aFlags.callback();
+ }
+ }
+
+ // The "animated" attributes enables animated toggles (slide in-out).
+ if (aFlags.animated) {
+ aPane.setAttribute("animated", "");
+ } else {
+ aPane.removeAttribute("animated");
+ }
+
+ // Sometimes it's useful delaying the toggle a few ticks to ensure
+ // a smoother slide in-out animation.
+ if (aFlags.delayed) {
+ aPane.ownerDocument.defaultView.setTimeout(set.bind(this), PANE_APPEARANCE_DELAY);
+ } else {
+ set.call(this);
+ }
+ }
+};
+
+/**
+ * Localization convenience methods.
+ *
+ * @param string aStringBundleName
+ * The desired string bundle's name.
+ */
+ViewHelpers.L10N = function(aStringBundleName) {
+ XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
+ Services.strings.createBundle(aStringBundleName));
+
+ XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
+ Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
+};
+
+ViewHelpers.L10N.prototype = {
+ stringBundle: null,
+
+ /**
+ * L10N shortcut function.
+ *
+ * @param string aName
+ * @return string
+ */
+ getStr: function(aName) {
+ return this.stringBundle.GetStringFromName(aName);
+ },
+
+ /**
+ * L10N shortcut function.
+ *
+ * @param string aName
+ * @param array aArgs
+ * @return string
+ */
+ getFormatStr: function(aName, ...aArgs) {
+ return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
+ },
+
+ /**
+ * L10N shortcut function for numeric arguments that need to be formatted.
+ * All numeric arguments will be fixed to 2 decimals and given a localized
+ * decimal separator. Other arguments will be left alone.
+ *
+ * @param string aName
+ * @param array aArgs
+ * @return string
+ */
+ getFormatStrWithNumbers: function(aName, ...aArgs) {
+ let newArgs = aArgs.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
+ return this.stringBundle.formatStringFromName(aName, newArgs, newArgs.length);
+ },
+
+ /**
+ * Converts a number to a locale-aware string format and keeps a certain
+ * number of decimals.
+ *
+ * @param number aNumber
+ * The number to convert.
+ * @param number aDecimals [optional]
+ * Total decimals to keep.
+ * @return string
+ * The localized number as a string.
+ */
+ numberWithDecimals: function(aNumber, aDecimals = 0) {
+ // If this is an integer, don't do anything special.
+ if (aNumber == (aNumber | 0)) {
+ return aNumber;
+ }
+ // Remove {n} trailing decimals. Can't use toFixed(n) because
+ // toLocaleString converts the number to a string. Also can't use
+ // toLocaleString(, { maximumFractionDigits: n }) because it's not
+ // implemented on OS X (bug 368838). Gross.
+ let localized = aNumber.toLocaleString(); // localize
+ let padded = localized + new Array(aDecimals).join("0"); // pad with zeros
+ let match = padded.match("([^]*?\\d{" + aDecimals + "})\\d*$");
+ return match.pop();
+ }
+};
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ * let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
+ * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ * ...
+ * });
+ *
+ * prefs.myCharPref = "foo";
+ * let aux = prefs.myCharPref;
+ *
+ * @param string aPrefsRoot
+ * The root path to the required preferences branch.
+ * @param object aPrefsObject
+ * An object containing { accessorName: [prefType, prefName] } keys.
+ */
+ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
+ this.root = aPrefsRoot;
+
+ for (let accessorName in aPrefsObject) {
+ let [prefType, prefName] = aPrefsObject[accessorName];
+ this.map(accessorName, prefType, prefName);
+ }
+};
+
+ViewHelpers.Prefs.prototype = {
+ /**
+ * Helper method for getting a pref value.
+ *
+ * @param string aType
+ * @param string aPrefName
+ * @return any
+ */
+ _get: function(aType, aPrefName) {
+ if (this[aPrefName] === undefined) {
+ this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
+ }
+ return this[aPrefName];
+ },
+
+ /**
+ * Helper method for setting a pref value.
+ *
+ * @param string aType
+ * @param string aPrefName
+ * @param any aValue
+ */
+ _set: function(aType, aPrefName, aValue) {
+ Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
+ this[aPrefName] = aValue;
+ },
+
+ /**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ *
+ * @param string aAccessorName
+ * @param string aType
+ * @param string aPrefName
+ */
+ map: function(aAccessorName, aType, aPrefName) {
+ Object.defineProperty(this, aAccessorName, {
+ get: () => this._get(aType, [this.root, aPrefName].join(".")),
+ set: (aValue) => this._set(aType, [this.root, aPrefName].join("."), aValue)
+ });
+ }
+};
+
+/**
+ * A generic Item is used to describe children present in a Widget.
+ * The label, value and description properties are necessarily strings.
+ * Iterable via "for (let childItem in parentItem) { }".
+ *
+ * @param object aOwnerView
+ * The owner view creating this item.
+ * @param any aAttachment
+ * Some attached primitive/object.
+ * @param nsIDOMNode | nsIDOMDocumentFragment | array aContents [optional]
+ * A prebuilt node, or an array containing the following properties:
+ * - aLabel: the label displayed in the widget
+ * - aValue: the actual internal value of the item
+ * - aDescription: an optional description of the item
+ */
+function Item(aOwnerView, aAttachment, aContents = []) {
+ this.ownerView = aOwnerView;
+ this.attachment = aAttachment;
+
+ let [aLabel, aValue, aDescription] = aContents;
+ this._label = aLabel + "";
+ this._value = aValue + "";
+ this._description = (aDescription || "") + "";
+
+ // Allow the insertion of prebuilt nodes, otherwise delegate the item view
+ // creation to a widget.
+ if (ViewHelpers.isNode(aLabel)) {
+ this._prebuiltTarget = aLabel;
+ }
+
+ XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
+};
+
+Item.prototype = {
+ get label() this._label,
+ get value() this._value,
+ get description() this._description,
+ get target() this._target,
+
+ /**
+ * Immediately appends a child item to this item.
+ *
+ * @param nsIDOMNode aElement
+ * An nsIDOMNode representing the child element to append.
+ * @param object aOptions [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(aElement, aOptions = {}) {
+ let item = new Item(this, aOptions.attachment);
+
+ // Entangle the item with the newly inserted child node.
+ this._entangleItem(item, this._target.appendChild(aElement));
+
+ // Handle any additional options after entangling the item.
+ if (aOptions.attributes) {
+ aOptions.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (aOptions.finalize) {
+ item.finalize = aOptions.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Immediately removes the specified child item from this item.
+ *
+ * @param Item aItem
+ * The item associated with the element to remove.
+ */
+ remove: function(aItem) {
+ if (!aItem) {
+ return;
+ }
+ this._target.removeChild(aItem._target);
+ this._untangleItem(aItem);
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ * @param nsIDOMNode aElement
+ * The element displaying the item.
+ */
+ _entangleItem: function(aItem, aElement) {
+ this._itemsByElement.set(aElement, aItem);
+ aItem._target = aElement;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _untangleItem: function(aItem) {
+ if (aItem.finalize) {
+ aItem.finalize(aItem);
+ }
+ for (let childItem in aItem) {
+ aItem.remove(childItem);
+ }
+
+ this._unlinkItem(aItem);
+ aItem._prebuiltTarget = null;
+ aItem._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _unlinkItem: function(aItem) {
+ this._itemsByElement.delete(aItem._target);
+ },
+
+ /**
+ * Returns a string representing the object.
+ * @return string
+ */
+ toString: function() {
+ if (this._label && this._value) {
+ return this._label + " -> " + this._value;
+ }
+ if (this.attachment) {
+ return this.attachment.toString();
+ }
+ return "(null)";
+ },
+
+ _label: "",
+ _value: "",
+ _description: "",
+ _prebuiltTarget: null,
+ _target: null,
+ finalize: null,
+ attachment: null
+};
+
+/**
+ * Some generic Widget methods handling Item instances.
+ * Iterable via "for (let childItem in 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.
+ *
+ * Language:
+ * - An "item" is an instance of an Item.
+ * - An "element" or "node" is a nsIDOMNode.
+ *
+ * The supplied element node or widget can either be a <xul:menulist>, or any
+ * other object interfacing the following methods:
+ * - function:nsIDOMNode insertItemAt(aIndex:number, aLabel:string, 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)
+ *
+ * For automagical keyboard and mouse accessibility, the element node or widget
+ * should be an event emitter with the following events:
+ * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
+ * - "mousePress" -> (aName:string, aEvent:MouseEvent)
+ */
+this.WidgetMethods = {
+ /**
+ * Sets the element node or widget associated with this container.
+ * @param nsIDOMNode | object aWidget
+ */
+ set widget(aWidget) {
+ this._widget = aWidget;
+
+ // Can't use WeakMaps for itemsByLabel or itemsByValue because
+ // keys are strings, and itemsByElement needs to be iterable.
+ XPCOMUtils.defineLazyGetter(this, "_itemsByLabel", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_itemsByValue", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_stagedItems", () => []);
+
+ // Handle internal events emitted by the widget if necessary.
+ if (ViewHelpers.isEventEmitter(aWidget)) {
+ aWidget.on("keyPress", this._onWidgetKeyPress.bind(this));
+ aWidget.on("mousePress", this._onWidgetMousePress.bind(this));
+ }
+ },
+
+ /**
+ * Gets the element node or widget associated with this container.
+ * @return nsIDOMNode | object
+ */
+ get widget() 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 label. 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.
+ *
+ * Furthermore, this container makes sure that all the items are unique
+ * (two items with the same label or value are not allowed) and non-degenerate
+ * (items with "undefined" or "null" labels/values). This can, as well, be
+ * overridden via the "relaxed" flag.
+ *
+ * @param nsIDOMNode | nsIDOMDocumentFragment array aContents
+ * A prebuilt node, or an array containing the following properties:
+ * - label: the label displayed in the container
+ * - value: the actual internal value of the item
+ * - description: an optional description of the item
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - staged: true to stage the item to be appended later
+ * - index: specifies on which position should the item be appended
+ * - relaxed: true if this container should allow dupes & degenerates
+ * - attachment: some attached primitive/object for the item
+ * - 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(aContents, aOptions = {}) {
+ let item = new Item(this, aOptions.attachment, aContents);
+
+ // Batch the item to be added later.
+ if (aOptions.staged) {
+ // An ulterior commit operation will ignore any specified index.
+ delete aOptions.index;
+ return void this._stagedItems.push({ item: item, options: aOptions });
+ }
+ // Find the target position in this container and insert the item there.
+ if (!("index" in aOptions)) {
+ return this._insertItemAt(this._findExpectedIndexFor(item), item, aOptions);
+ }
+ // Insert the item at the specified index. If negative or out of bounds,
+ // the item will be simply appended.
+ return this._insertItemAt(aOptions.index, item, aOptions);
+ },
+
+ /**
+ * Flushes all the prepared items into this container.
+ * Any specified index on the items will be ignored. Everything is appended.
+ *
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - sorted: true to sort all the items before adding them
+ */
+ commit: function(aOptions = {}) {
+ let stagedItems = this._stagedItems;
+
+ // Sort the items before adding them to this container, if preferred.
+ if (aOptions.sorted) {
+ stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
+ }
+ // Append the prepared items to this container.
+ for (let { item, options } of stagedItems) {
+ this._insertItemAt(-1, item, options);
+ }
+ // Recreate the temporary items list for ulterior pushes.
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Updates this container to reflect the information provided by the
+ * currently selected item.
+ *
+ * @return boolean
+ * True if a selected item was available, false otherwise.
+ */
+ refresh: function() {
+ let selectedItem = this.selectedItem;
+ if (!selectedItem) {
+ return false;
+ }
+ this._widget.removeAttribute("notice");
+ this._widget.setAttribute("label", selectedItem._label);
+ this._widget.setAttribute("tooltiptext", selectedItem._value);
+ return true;
+ },
+
+ /**
+ * Immediately removes the specified item from this container.
+ *
+ * @param Item aItem
+ * The item associated with the element to remove.
+ */
+ remove: function(aItem) {
+ if (!aItem) {
+ return;
+ }
+ this._widget.removeChild(aItem._target);
+ this._untangleItem(aItem);
+ },
+
+ /**
+ * Removes the item at the specified index from this container.
+ *
+ * @param number aIndex
+ * The index of the item to remove.
+ */
+ removeAt: function(aIndex) {
+ this.remove(this.getItemAtIndex(aIndex));
+ },
+
+ /**
+ * Removes all items from this container.
+ */
+ empty: function() {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.removeAllItems();
+ this._widget.setAttribute("notice", this.emptyText);
+ this._widget.setAttribute("label", this.emptyText);
+ this._widget.removeAttribute("tooltiptext");
+
+ for (let [, item] of this._itemsByElement) {
+ this._untangleItem(item);
+ }
+
+ this._itemsByLabel.clear();
+ this._itemsByValue.clear();
+ this._itemsByElement.clear();
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Does not remove any item in this container. Instead, it overrides the
+ * current label to signal that it is unavailable and removes the tooltip.
+ */
+ setUnavailable: function() {
+ this._widget.setAttribute("notice", this.unavailableText);
+ this._widget.setAttribute("label", this.unavailableText);
+ this._widget.removeAttribute("tooltiptext");
+ },
+
+ /**
+ * The label string automatically added to this container when there are
+ * no child nodes present.
+ */
+ emptyText: "",
+
+ /**
+ * The label string added to this container when it is marked as unavailable.
+ */
+ unavailableText: "",
+
+ /**
+ * 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 aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ toggleContents: function(aVisibleFlag) {
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !aVisibleFlag;
+ }
+ },
+
+ /**
+ * Toggles all items in this container hidden or visible based on a predicate.
+ *
+ * @param function aPredicate [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(aPredicate = this._currentFilterPredicate) {
+ this._currentFilterPredicate = aPredicate;
+
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !aPredicate(item);
+ }
+ },
+
+ /**
+ * Sorts all the items in this container based on a predicate.
+ *
+ * @param function aPredicate [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 label.
+ */
+ sortContents: function(aPredicate = this._currentSortPredicate) {
+ let sortedItems = this.orderedItems.sort(this._currentSortPredicate = aPredicate);
+
+ 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 aFirst
+ * The first item to be swapped.
+ * @param Item aSecond
+ * The second item to be swapped.
+ */
+ swapItems: function(aFirst, aSecond) {
+ if (aFirst == aSecond) { // We're just dandy, thank you.
+ return;
+ }
+ let { _prebuiltTarget: firstPrebuiltTarget, target: firstTarget } = aFirst;
+ let { _prebuiltTarget: secondPrebuiltTarget, target: secondTarget } = aSecond;
+
+ // 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 Ci.nsIDOMDocumentFragment) {
+ for (let node of firstTarget.childNodes) {
+ firstPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+ if (secondPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
+ 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(aFirst);
+ this._unlinkItem(aSecond);
+
+ // 4. Add the items again, but reversing their indices.
+ this._insertItemAt.apply(this, i < j ? [i, aSecond] : [j, aFirst]);
+ this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
+
+ // 5. Restore the previous selection, if necessary.
+ if (selectedIndex == i) {
+ this._widget.selectedItem = aFirst._target;
+ } else if (selectedIndex == j) {
+ this._widget.selectedItem = aSecond._target;
+ }
+ },
+
+ /**
+ * Visually swaps two items in this container at specific indices.
+ *
+ * @param number aFirst
+ * The index of the first item to be swapped.
+ * @param number aSecond
+ * The index of the second item to be swapped.
+ */
+ swapItemsAtIndices: function(aFirst, aSecond) {
+ this.swapItems(this.getItemAtIndex(aFirst), this.getItemAtIndex(aSecond));
+ },
+
+ /**
+ * Checks whether an item with the specified label is among the elements
+ * shown in this container.
+ *
+ * @param string aLabel
+ * The item's label.
+ * @return boolean
+ * True if the label is known, false otherwise.
+ */
+ containsLabel: function(aLabel) {
+ return this._itemsByLabel.has(aLabel) ||
+ this._stagedItems.some(({ item }) => item._label == aLabel);
+ },
+
+ /**
+ * Checks whether an item with the specified value is among the elements
+ * shown in this container.
+ *
+ * @param string aValue
+ * The item's value.
+ * @return boolean
+ * True if the value is known, false otherwise.
+ */
+ containsValue: function(aValue) {
+ return this._itemsByValue.has(aValue) ||
+ this._stagedItems.some(({ item }) => item._value == aValue);
+ },
+
+ /**
+ * Gets the "preferred value". This is the latest selected item's value,
+ * remembered just before emptying this container.
+ * @return string
+ */
+ get preferredValue() this._preferredValue,
+
+ /**
+ * Retrieves the item associated with the selected element.
+ * @return Item
+ */
+ 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 label of the selected element.
+ * @return string
+ */
+ get selectedLabel() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement)._label;
+ }
+ return "";
+ },
+
+ /**
+ * 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 "";
+ },
+
+ /**
+ * Selects the element with the entangled item in this container.
+ * @param Item | function aItem
+ */
+ set selectedItem(aItem) {
+ // A predicate is allowed to select a specific item.
+ // If no item is matched, then the current selection is removed.
+ if (typeof aItem == "function") {
+ aItem = this.getItemForPredicate(aItem);
+ }
+
+ // A falsy item is allowed to invalidate the current selection.
+ let targetElement = aItem ? aItem._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ // Make sure the currently selected item's target element is also focused.
+ if (this.autoFocusOnSelection && targetElement) {
+ targetElement.focus();
+ }
+
+ // Prevent selecting the same item again and avoid dispatching
+ // a redundant selection event, so return early.
+ if (targetElement == prevElement) {
+ return;
+ }
+ this._widget.selectedItem = targetElement;
+ ViewHelpers.dispatchEvent(targetElement || prevElement, "select", aItem);
+
+ // Updates this container to reflect the information provided by the
+ // currently selected item.
+ this.refresh();
+ },
+
+ /**
+ * Selects the element at the specified index in this container.
+ * @param number aIndex
+ */
+ set selectedIndex(aIndex) {
+ let targetElement = this._widget.getItemAtIndex(aIndex);
+ if (targetElement) {
+ this.selectedItem = this._itemsByElement.get(targetElement);
+ return;
+ }
+ this.selectedItem = null;
+ },
+
+ /**
+ * Selects the element with the specified label in this container.
+ * @param string aLabel
+ */
+ set selectedLabel(aLabel)
+ this.selectedItem = this._itemsByLabel.get(aLabel),
+
+ /**
+ * Selects the element with the specified value in this container.
+ * @param string aValue
+ */
+ set selectedValue(aValue)
+ this.selectedItem = this._itemsByValue.get(aValue),
+
+ /**
+ * 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,
+
+ /**
+ * 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 aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function(aDelta) {
+ // 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 aDelta <= 1).
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ selectedElement.focus();
+ } else {
+ this.selectedIndex = Math.max(0, aDelta - 1);
+ return;
+ }
+
+ 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.
+ }
+ }
+
+ // 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 aDirection
+ * 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(aDirection) {
+ let commandDispatcher = this._commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[aDirection]();
+
+ // 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(commandDispatcher.focusedElement)) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ // 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;
+ return this._cachedCommandDispatcher = 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 aIndex
+ * The index used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemAtIndex: function(aIndex) {
+ return this.getItemForElement(this._widget.getItemAtIndex(aIndex));
+ },
+
+ /**
+ * Gets the item in the container having the specified label.
+ *
+ * @param string aLabel
+ * The label used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByLabel: function(aLabel) {
+ return this._itemsByLabel.get(aLabel);
+ },
+
+ /**
+ * Gets the item in the container having the specified value.
+ *
+ * @param string aValue
+ * The value used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByValue: function(aValue) {
+ return this._itemsByValue.get(aValue);
+ },
+
+ /**
+ * Gets the item in the container associated with the specified element.
+ *
+ * @param nsIDOMNode aElement
+ * The element used to identify the item.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForElement: function(aElement) {
+ while (aElement) {
+ let item = this._itemsByElement.get(aElement);
+ if (item) {
+ return item;
+ }
+ aElement = aElement.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Gets a visible item in this container validating a specified predicate.
+ *
+ * @param function aPredicate
+ * The first item which validates this predicate is returned
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForPredicate: function(aPredicate, aOwner = this) {
+ for (let [element, item] of aOwner._itemsByElement) {
+ let match;
+ if (aPredicate(item) && !element.hidden) {
+ match = item;
+ } else {
+ match = this.getItemForPredicate(aPredicate, item);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Finds the index of an item in the container.
+ *
+ * @param Item aItem
+ * The item get the index for.
+ * @return number
+ * The index of the matched item, or -1 if nothing is found.
+ */
+ indexOfItem: function(aItem) {
+ return this._indexOfElement(aItem._target);
+ },
+
+ /**
+ * Finds the index of an element in the container.
+ *
+ * @param nsIDOMNode aElement
+ * The element get the index for.
+ * @return number
+ * The index of the matched element, or -1 if nothing is found.
+ */
+ _indexOfElement: function(aElement) {
+ for (let i = 0; i < this._itemsByElement.size; i++) {
+ if (this._widget.getItemAtIndex(i) == aElement) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Gets the total number of items in this container.
+ * @return number
+ */
+ get itemCount() this._itemsByElement.size,
+
+ /**
+ * Returns a list of items in this container, in no particular order.
+ * @return array
+ */
+ get items() {
+ let items = [];
+ for (let [, item] of this._itemsByElement) {
+ items.push(item);
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of labels in this container, in no particular order.
+ * @return array
+ */
+ get labels() {
+ let labels = [];
+ for (let [label] of this._itemsByLabel) {
+ labels.push(label);
+ }
+ return labels;
+ },
+
+ /**
+ * Returns a list of values in this container, in no particular order.
+ * @return array
+ */
+ get values() {
+ let values = [];
+ for (let [value] of this._itemsByValue) {
+ values.push(value);
+ }
+ return values;
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in no particular order.
+ * @return array
+ */
+ get visibleItems() {
+ let items = [];
+ for (let [element, item] of this._itemsByElement) {
+ if (!element.hidden) {
+ items.push(item);
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of all items in this container, in the displayed order.
+ * @return array
+ */
+ get orderedItems() {
+ let items = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ items.push(this.getItemAtIndex(i));
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in the displayed order
+ * @return array
+ */
+ get orderedVisibleItems() {
+ let items = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ let item = this.getItemAtIndex(i);
+ if (!item._target.hidden) {
+ items.push(item);
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Specifies the required conditions for an item to be considered unique.
+ * Possible values:
+ * - 1: label AND value are different from all other items
+ * - 2: label OR value are different from all other items
+ * - 3: only label is required to be different
+ * - 4: only value is required to be different
+ */
+ uniquenessQualifier: 1,
+
+ /**
+ * Checks if an item is unique in this container.
+ *
+ * @param Item aItem
+ * The item for which to verify uniqueness.
+ * @return boolean
+ * True if the item is unique, false otherwise.
+ */
+ isUnique: function(aItem) {
+ switch (this.uniquenessQualifier) {
+ case 1:
+ return !this._itemsByLabel.has(aItem._label) &&
+ !this._itemsByValue.has(aItem._value);
+ case 2:
+ return !this._itemsByLabel.has(aItem._label) ||
+ !this._itemsByValue.has(aItem._value);
+ case 3:
+ return !this._itemsByLabel.has(aItem._label);
+ case 4:
+ return !this._itemsByValue.has(aItem._value);
+ }
+ return false;
+ },
+
+ /**
+ * Checks if an item is eligible for this container.
+ *
+ * @param Item aItem
+ * The item for which to verify eligibility.
+ * @return boolean
+ * True if the item is eligible, false otherwise.
+ */
+ isEligible: function(aItem) {
+ let isUnique = this.isUnique(aItem);
+ let isPrebuilt = !!aItem._prebuiltTarget;
+ let isDegenerate = aItem._label == "undefined" || aItem._label == "null" ||
+ aItem._value == "undefined" || aItem._value == "null";
+
+ return isPrebuilt || (isUnique && !isDegenerate);
+ },
+
+ /**
+ * Finds the expected item index in this container based on the default
+ * sort predicate.
+ *
+ * @param Item aItem
+ * The item for which to get the expected index.
+ * @return number
+ * The expected item index.
+ */
+ _findExpectedIndexFor: function(aItem) {
+ let itemCount = this.itemCount;
+
+ for (let i = 0; i < itemCount; i++) {
+ if (this._currentSortPredicate(this.getItemAtIndex(i), aItem) > 0) {
+ return i;
+ }
+ }
+ return itemCount;
+ },
+
+ /**
+ * Immediately inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param Item aItem
+ * An object containing a label and a value property (at least).
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - node: allows the insertion of prebuilt nodes instead of labels
+ * - relaxed: true if this container should allow dupes & degenerates
+ * - 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(aIndex, aItem, aOptions = {}) {
+ // Relaxed nodes may be appended without verifying their eligibility.
+ if (!aOptions.relaxed && !this.isEligible(aItem)) {
+ return null;
+ }
+
+ // Entangle the item with the newly inserted node.
+ this._entangleItem(aItem, this._widget.insertItemAt(aIndex,
+ aItem._prebuiltTarget || aItem._label, // Allow the insertion of prebuilt nodes.
+ aItem._value,
+ aItem._description,
+ aItem.attachment));
+
+ // Handle any additional options after entangling the item.
+ if (!this._currentFilterPredicate(aItem)) {
+ aItem._target.hidden = true;
+ }
+ if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
+ aItem._target.focus();
+ }
+ if (aOptions.attributes) {
+ aOptions.attributes.forEach(e => aItem._target.setAttribute(e[0], e[1]));
+ }
+ if (aOptions.finalize) {
+ aItem.finalize = aOptions.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return aItem;
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ * @param nsIDOMNode aElement
+ * The element displaying the item.
+ */
+ _entangleItem: function(aItem, aElement) {
+ this._itemsByLabel.set(aItem._label, aItem);
+ this._itemsByValue.set(aItem._value, aItem);
+ this._itemsByElement.set(aElement, aItem);
+ aItem._target = aElement;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _untangleItem: function(aItem) {
+ if (aItem.finalize) {
+ aItem.finalize(aItem);
+ }
+ for (let childItem in aItem) {
+ aItem.remove(childItem);
+ }
+
+ this._unlinkItem(aItem);
+ aItem._prebuiltTarget = null;
+ aItem._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _unlinkItem: function(aItem) {
+ this._itemsByLabel.delete(aItem._label);
+ this._itemsByValue.delete(aItem._value);
+ this._itemsByElement.delete(aItem._target);
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string aName
+ * @param KeyboardEvent aEvent
+ */
+ _onWidgetKeyPress: function(aName, aEvent) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(aEvent);
+
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_UP:
+ case aEvent.DOM_VK_LEFT:
+ this.focusPrevItem();
+ return;
+ case aEvent.DOM_VK_DOWN:
+ case aEvent.DOM_VK_RIGHT:
+ this.focusNextItem();
+ return;
+ case aEvent.DOM_VK_PAGE_UP:
+ this.focusItemAtDelta(-(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case aEvent.DOM_VK_PAGE_DOWN:
+ this.focusItemAtDelta(+(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case aEvent.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+ case aEvent.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+ }
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string aName
+ * @param MouseEvent aEvent
+ */
+ _onWidgetMousePress: function(aName, aEvent) {
+ if (aEvent.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+
+ let item = this.getItemForElement(aEvent.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 aItem
+ * The item passing through the filter.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+ _currentFilterPredicate: function(aItem) {
+ return true;
+ },
+
+ /**
+ * The predicate used when sorting items. By default, items in this view
+ * are sorted by their label.
+ *
+ * @param Item aFirst
+ * The first item used in the comparison.
+ * @param Item aSecond
+ * The second item used in the comparison.
+ * @return number
+ * -1 to sort aFirst to a lower index than aSecond
+ * 0 to leave aFirst and aSecond unchanged with respect to each other
+ * 1 to sort aSecond to a lower index than aFirst
+ */
+ _currentSortPredicate: function(aFirst, aSecond) {
+ return +(aFirst._label.toLowerCase() > aSecond._label.toLowerCase());
+ },
+
+ _widget: null,
+ _preferredValue: null,
+ _cachedCommandDispatcher: null
+};
+
+/**
+ * A generator-iterator over all the items in this container.
+ */
+Item.prototype.__iterator__ =
+WidgetMethods.__iterator__ = function() {
+ for (let [, item] of this._itemsByElement) {
+ yield item;
+ }
+};
diff --git a/browser/devtools/shared/widgets/widgets.css b/browser/devtools/shared/widgets/widgets.css
new file mode 100644
index 000000000..5ee65ea91
--- /dev/null
+++ b/browser/devtools/shared/widgets/widgets.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/. */
+
+/* BreacrumbsWidget */
+
+.breadcrumbs-widget-item {
+ direction: ltr;
+}
+
+.breadcrumbs-widget-item {
+ -moz-user-focus: normal;
+}
+
+/* SideMenuWidget */
+
+.side-menu-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.side-menu-widget-item-contents {
+ -moz-user-focus: normal;
+}
+
+/* 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[non-header] > .title,
+.variable-or-property[non-header] > .title,
+.variable-or-property[non-match] > .title {
+ display: none;
+}
+
+.variable-or-property:not([safe-getter]) > tooltip > label[value=WebIDL],
+.variable-or-property:not([non-extensible]) > tooltip > label[value=extensible],
+.variable-or-property:not([frozen]) > tooltip > label[value=frozen],
+.variable-or-property:not([sealed]) > tooltip > label[value=sealed] {
+ display: none;
+}
diff --git a/browser/devtools/sourceeditor/Makefile.in b/browser/devtools/sourceeditor/Makefile.in
new file mode 100644
index 000000000..a0acbc8aa
--- /dev/null
+++ b/browser/devtools/sourceeditor/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_JS_MODULES = \
+ source-editor.jsm \
+ source-editor-orion.jsm \
+ source-editor-ui.jsm \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/sourceeditor/moz.build b/browser/devtools/sourceeditor/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/sourceeditor/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/sourceeditor/orion/LICENSE b/browser/devtools/sourceeditor/orion/LICENSE
new file mode 100644
index 000000000..2d907d73a
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/LICENSE
@@ -0,0 +1,29 @@
+Eclipse Distribution License - v 1.0
+
+Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
+
+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 the Eclipse Foundation, 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.
+
diff --git a/browser/devtools/sourceeditor/orion/Makefile.dryice.js b/browser/devtools/sourceeditor/orion/Makefile.dryice.js
new file mode 100644
index 000000000..6866ed937
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/Makefile.dryice.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/* 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/. */
+
+var copy = require('dryice').copy;
+
+const ORION_EDITOR = "org.eclipse.orion.client.editor/web";
+
+var js_src = copy.createDataObject();
+
+copy({
+ source: [
+ ORION_EDITOR + "/orion/textview/global.js",
+ ORION_EDITOR + "/orion/textview/eventTarget.js",
+ ORION_EDITOR + "/orion/editor/regex.js",
+ ORION_EDITOR + "/orion/textview/keyBinding.js",
+ ORION_EDITOR + "/orion/textview/annotations.js",
+ ORION_EDITOR + "/orion/textview/rulers.js",
+ ORION_EDITOR + "/orion/textview/undoStack.js",
+ ORION_EDITOR + "/orion/textview/textModel.js",
+ ORION_EDITOR + "/orion/textview/projectionTextModel.js",
+ ORION_EDITOR + "/orion/textview/tooltip.js",
+ ORION_EDITOR + "/orion/textview/textView.js",
+ ORION_EDITOR + "/orion/textview/textDND.js",
+ ORION_EDITOR + "/orion/editor/htmlGrammar.js",
+ ORION_EDITOR + "/orion/editor/textMateStyler.js",
+ ORION_EDITOR + "/examples/textview/textStyler.js",
+ ],
+ dest: js_src,
+});
+
+copy({
+ source: js_src,
+ dest: "orion.js",
+});
+
+var css_src = copy.createDataObject();
+
+copy({
+ source: [
+ ORION_EDITOR + "/orion/textview/textview.css",
+ ORION_EDITOR + "/orion/textview/rulers.css",
+ ORION_EDITOR + "/orion/textview/annotations.css",
+ ORION_EDITOR + "/examples/textview/textstyler.css",
+ ORION_EDITOR + "/examples/editor/htmlStyles.css",
+ ],
+ dest: css_src,
+});
+
+copy({
+ source: css_src,
+ dest: "orion.css",
+});
+
diff --git a/browser/devtools/sourceeditor/orion/README b/browser/devtools/sourceeditor/orion/README
new file mode 100644
index 000000000..c7669099c
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/README
@@ -0,0 +1,43 @@
+# Introduction
+
+This is the Orion editor packaged for Mozilla.
+
+The Orion editor web site: http://www.eclipse.org/orion
+
+# Upgrade
+
+To upgrade Orion to a newer version see the UPGRADE file.
+
+Orion version: git clone from 2012-01-26
+ commit hash 1d1150131dacecc9f4d9eb3cdda9103ea1819045
+
+ + patch for Eclipse Bug 370584 - [Firefox] Edit menu items in context menus
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=137d5a8e9bbc0fa204caae74ebd25a7d9d4729bd
+ see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370584
+
+ + patches for Eclipse Bug 370606 - Problems with UndoStack and deletions at
+ the beginning of the document
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=cec71bddaf32251c34d3728df5da13c130d14f33
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=3ce24b94f1d8103b16b9cf16f2f50a6302d43b18
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=27177e9a3dc70c20b4877e3eab3adfff1d56e342
+ see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370606
+
+ + patch for Mozilla Bug 730532 - remove CSS2Properties aliases for MozOpacity
+ and MozOutline*
+ see https://bugzilla.mozilla.org/show_bug.cgi?id=730532#c3
+
+# License
+
+The following files are licensed according to the contents in the LICENSE
+file:
+ orion.js
+ orion.css
+
+# Theming
+
+The syntax highlighting and the editor UI are themed using a style sheet. The
+default theme file is browser/themes/*/devtools/orion.css - this is based on the
+orion.css found in this folder.
+
+Please note that the orion.css file from this folder is not used. It is kept
+here only as reference.
diff --git a/browser/devtools/sourceeditor/orion/UPGRADE b/browser/devtools/sourceeditor/orion/UPGRADE
new file mode 100644
index 000000000..a2c006efe
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/UPGRADE
@@ -0,0 +1,20 @@
+Upgrade notes:
+
+1. Get the Orion client source code from:
+http://www.eclipse.org/orion
+
+2. Install Dryice from:
+https://github.com/mozilla/dryice
+
+You also need nodejs for Dryice to run:
+http://nodejs.org
+
+3. Copy Makefile.dryice.js to:
+org.eclipse.orion.client/bundles/
+
+4. Execute Makefile.dryice.js. You should get orion.js and orion.css.
+
+5. Copy the two files back here.
+
+6. Make a new build of Firefox.
+
diff --git a/browser/devtools/sourceeditor/orion/orion.css b/browser/devtools/sourceeditor/orion/orion.css
new file mode 100644
index 000000000..1e3b003ca
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/orion.css
@@ -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/. */
+
+.view {
+ background-color: white;
+}
+
+.viewContainer {
+ background-color: #eeeeee;
+ font-family: monospace;
+ font-size: 10pt;
+}
+::-webkit-scrollbar-corner {
+ background-color: #eeeeee;
+}
+
+.viewContent {
+}/* Styles for rulers */
+.ruler {
+ background-color: white;
+}
+.ruler.annotations {
+ border-right: 1px solid lightgray;
+ width: 16px;
+}
+.ruler.folding {
+ border-right: 1px solid lightgray;
+ width: 14px;
+}
+.ruler.lines {
+ border-right: 1px solid lightgray;
+ text-align: right;
+}
+.ruler.overview {
+ border-left: 1px solid lightgray;
+ width: 14px;
+}
+
+/* Styles for the line number ruler */
+.rulerLines {
+}
+.rulerLines.even
+.rulerLines.odd {
+}/* Styles for the annotation ruler (all lines) */
+.annotation {
+}
+.annotation.error,
+.annotation.warning
+.annotation.task,
+.annotation.bookmark,
+.annotation.breakpoint,
+.annotation.collapsed
+.annotation.expanded {
+}
+
+/* Styles for the annotation ruler (first line) */
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.error {
+ /* images/error.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAPVvcvWHiPVucvRuc+ttcfV6f91KVN5LU99PV/FZY/JhaM4oN84pONE4Rd1ATfJLWutVYPRgbdxpcsgWKMgZKs4lNfE/UvE/U+artcpdSc5uXveimslHPuBhW/eJhfV5efaCgO2CgP+/v+PExP///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACUALAAAAAAQABAAAAZ+wJJwSCwaScgkySgkjTQZTkYzWhadnE5oE+pwqkSshwQqkzxfa4kkQXxEpA9J9EFI1KQGQQBAigYCBA14ExEWF0gXihETeA0QD3AkD5QQg0NsDnAJmwkOd5gYFSQKpXAFDBhqaxgLBwQBBAapq00YEg0UDRKqTGtKSL7Cw8JBADs=");
+}
+.annotationHTML.warning {
+ /* images/warning.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAP7bc//egf/ij/7ijv/jl/7kl//mnv7lnv/uwf7CTP7DTf7DT/7IW//Na/7Na//NbP7QdP/dmbltAIJNAF03AMSAJMSCLKqASa2DS6uBSquCSrGHTq6ETbCHT7WKUrKIUcCVXL+UXMOYX8GWXsSZYMiib6+ETbOIUcOXX86uhd3Muf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACsALAAAAAAQABAAAAZowJVwSCwaj0ihikRSJYcoBEL0XKlGkcjImQQhJBREKFnyICoThKeE/AAW6AXgdPyUAgrLJBEo0YsbAQyDhAEdRRwDDw8OaA4NDQImRBgFEJdglxAEGEQZKQcHBqOkKRpFF6mqq1WtrUEAOw==");
+}
+.annotationHTML.task {
+ /* images/task.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAAN7s4uTy6ICvY423c2WdP2ugR3mqWYeza2ejOl6VNVqPM1aJMURsJ2GaOnKlT8PbsbPDqGmmO1OCLk98LEhxKGWfOWKaN0t2KkJoJf///////wAAAAAAAAAAAAAAAAAAACH5BAEAABoALAAAAAAQABAAAAVmoCaOZDk+UaquDxkNcCxHJHLceI6QleD/vkCmQrIYjkiDMGAhJRzQ6NKRICkKgYJ2qVWQFktCmEBYkCSNZSbQaDckpAl5TCZMSBdtAaDXX0gUUYJRFCQMSYgGDCQQGI6PkBAmkyUhADs=");
+}
+.annotationHTML.bookmark {
+ /* images/bookmark.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQALMAAP7//+/VNPzZS/vifeumAPrBOOSlHOSuRP///wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAgALAAAAAAQABAAAARLEMlJq5Xn3EvIrkenfRIhCB5pmKhRdbAJAGhssuc8n6eJoAKdkOaTAIdEQeWoA1oGsiZhYAnIcqiApVPjElyUbkFSgCkn5XElLYkAADs=");
+}
+.annotationHTML.breakpoint {
+ /* images/breakpoint.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAFheoFxkoFxnpmt0pmZxpnF7rYyWwmJwpnaFs3aDrWt8rXGBrYycwmZ3mXuNs42cu77F03GIs3aJrYGVu2J5oKCuxeDj6LK/03GLrYieu3aIoIygu6m4zcLN3MTM1m6Rs2aLriRgkSZilXGXtoGcs7LD0QBLhSZikihol3ScubrO2Yaqu5q4xpO0wpm7yabF0ZO9yaXI0r3X3tHj6P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADQALAAAAAAQABAAAAafQJpwSCwWLYZBIDAwWIw0A+FFpW6aRUPCxe1yE4ahhdCCxWSzmSwGgxGeUceKpUqhUCkVa7UK0wgkJCUjJoUmIyWBBEIEGhoeJ4YmJx6OAUIADQ0QIZIhEJoAQgEUFBUgkiAVpZdRCxIPFx8iIh8XDw4FfhYHDhgZHB0dHBkYEwdwUQoTEc3OEwp+QwYHCBMMDBMIB9JESAJLAk5Q5EVBADs=");
+}
+.annotationHTML.collapsed {
+ /* images/collapsed.png */
+ width: 14px;
+ height: 14px;
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNpi/P//PwMlgImBQkCxASzoAp++fo+6de+Z+fXbD/Jev/nAICoiwKCpqrBBTUlqNR835zJ09YzIYfDxy7eo/cevLmXlYGNQUJAEahZieP3mHcODB08Zfv/4w+BoqR3Nz8O1DKcXzt94HPqXmZlBU1+LgZNfkMHazIOBA0hr6uswgMTP33gYijcMLlx/EMAnLs7w7sc/hg9AG0HgPZB+B8S84hJA+UcBeMPg+at3DJIMnAxZzt5wsUhnXzDdsmIVWB6vAcLCfAys3z4wzN64huEfkJ/uH8IwexOQDQymD2/fgeXxekFLRWHD51evGDhZGRi4WSFSnCwgNjB2Xr1m0AbK4zXAQkdhNdPf3wx3r91g+PruLcOqnasYvn54x3Dv2k0G5r+/GMyB8nijEQTefvoadeH6w9Cbtx8GvH//kUFQkJ9BQ1V+g76m/GphPu5lBA0YenmBYgMAAgwA34GIKjmLxOUAAAAASUVORK5CYII=");
+}
+.annotationHTML.expanded {
+ /* images/expanded.png */
+ width: 14px;
+ height: 14px;
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT5JREFUeNrUksFKw0AURW+mTWw67SSEiG209U90r4jddFO34l+5U0HdZCHiFwiCOz9AlMSmGEpMOqk1TWJSFGyFbATR2dyZd+Dw3mOENE3xkyP8PYHrBT3OX7uW43ZefA6FUaw1dJPSyrmu1k8KBYOh37Od4XFZLEPXFdRrFMGIw3U9TKMYqw1tb0VjcxLy9eEF425CCIxWE5JcxSQGxCyNloG87gXhwWIHc4J767lTZQw8ShFGSZbxRyaQmZJxd3NRUJ6ffwQNEi6PzG/L2tjdmvFCgcKqKL2F2Olu43MzggDka+IjPuOFI7Sbujn2fUglYKkkzFIi+R0I/QDrGS8UqDX5QkhiOHYfE84hkhSTkGNgOyDJFCzjhYLTq+vDtrG8r1LZtB6fcHtzB+uhD5VWzLx+lvF/8JV/XfAuwADsrJbMGG4l4AAAAABJRU5ErkJggg==");
+}
+.annotationHTML.multiple {
+ /* images/multiple.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAOdpa+yJiuFYXOFYXeBYXONwded8f+NwdmhwkHB4iPr7/ezx+fP2+2h4kOzy+Wh4iPr8/gCBwTaczjaXyjaYyjaXyTaYyfr8/QCMzQCMzACHxzao2jal2Dak1zag03iAgI/Ckn64fZrHmX+4fZLCianPopPCiarOoqbLlafLlbnXq7nWq6fLlMTcsoCIeJCQcIiIeKCYaJiQcO16ee16evGVlfGWlfahn/ahoPWhn/WhoPe1tP///////wAAAAAAACH5BAEAAD0ALAAAAAAQABAAAAaRwJ5wSCwaj8WYcslcDmObaDTGq1Zjzw4mk+FQIRcFTzaUeTRoj4zHaI+HL0lkLnnxFgsH7zWEWSoTFBMwVlUwQy6JMDCJjYwuQx8tk5MfOzk4OjcfkSssKCkqHzY0MzQ1nEIJJSYkJCcJAQCzAQlDDyIjISMiCQYEAgMGD0MNIMfHDQUHBc3EQgjR0tPSSNY9QQA7");
+}
+.annotationHTML.overlay {
+ /* images/plus.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJEAQvB2JVdrAAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAD1JREFUCNdtjkESADAEAzemf69f66HMqGlOIhYiFRFRtSQBWAY7mzx+EDTL6sSgb1jTk7Q87rxyqe37fXsAa78gLyZnRgEAAAAASUVORK5CYII=");
+ background-position: right bottom;
+ position: relative;
+ top: -16px;
+}
+.annotationHTML.currentBracket {
+ /* images/currentBracket.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBULCGQmEKAAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAnklEQVQ4y7VTsRHDIBATJg1HCUzAHEzFBExAzwZsRMkE9gifKhc72ODYibr/+xcnoQdugq0LAujEwmbn0UxQh4OxpjX1XgshwFqLnPM5PQTQGlprWpbl3RhJ/CSQUm7qPYLp7i8cEpRSoJT6ju0lIaVEQgiKMQ4lHHpQayVjzHWCn5jIOcc8z9dMBADvPZxz3SC1tzCI8vgWdvL+VzwB8JSj2GFTyxIAAAAASUVORK5CYII=");
+}
+.annotationHTML.matchingBracket {
+ /* images/matchingBracket.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBUMAsuyb3kAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAoklEQVQ4y61TsQ3EIAw80DcI0USKGIApWIsB2IGGKbJPugxBR3VfvfRRCOSTvw7LPuPzGXgI8f0gwAsFu5rXIYMdDiEOIdnKW5YFzjnEGH+bhwA/KKVwmibu0BhRnpEZY1BrHTaVT7fQJZjnGeu63tOAJFNKVEox53yqQZfAWstt27oidgm01ve3UEqBaBjnspG89wgh3LiFgZXHt3Dh23/FGxKViehm0X85AAAAAElFTkSuQmCC");
+}
+.annotationHTML.currentLine {
+ /* images/currentLine.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAALxe0bNWzbdZzrlb0KpPx61RybBTy6VLxadNxZGctIeUroyYsG92hHyMqIKRq2l9nmyAoHGDonaIpStXj6q80k1aXf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABYALAAAAAAQABAAAAVCoCWOZGmeKDql5ppOMGXBk/zOoltSNO6XrlXwxIPNYiMGq8SoLC2MaNPygEQkDYdikUg6LQcEoWAICAaA5HPNLoUAADs=");
+}
+
+/* Styles for the overview ruler */
+.annotationOverview {
+ cursor: pointer;
+ border-radius: 2px;
+ left: 2px;
+ width: 8px;
+}
+.annotationOverview.task {
+ background-color: lightgreen;
+ border: 1px solid green;
+}
+.annotationOverview.breakpoint {
+ background-color: lightblue;
+ border: 1px solid blue;
+}
+.annotationOverview.bookmark {
+ background-color: yellow;
+ border: 1px solid orange;
+}
+.annotationOverview.error {
+ background-color: lightcoral;
+ border: 1px solid darkred;
+}
+.annotationOverview.warning {
+ background-color: Gold;
+ border: 1px solid black;
+}
+.annotationOverview.currentBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.matchingBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.currentLine {
+ background-color: #EAF2FE;
+ border: 1px solid black;
+}
+
+/* Styles for text range */
+.annotationRange {
+ background-repeat: repeat-x;
+ background-position: left bottom;
+}
+.annotationRange.task {
+ /* images/squiggly_task.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEoIrb7JmcAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAGUlEQVQI12NggIH/DGdhDCM45z/DfyiBAADgdQjGhI/4DAAAAABJRU5ErkJggg==");
+}
+.annotationRange.breakpoint {
+ /* images/squiggly_breakpoint.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEqHTKradgAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAIklEQVQI11XJMQ0AMAzAMGMafwrFlD19+sUKIJTFo9k+B/kQ+Qr2bIVKOgAAAABJRU5ErkJggg==");
+}
+.annotationRange.bookmark {
+ /* images/squiggly_bookmark.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
+}
+.annotationRange.error {
+ /* images/squiggly_error.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
+}
+.annotationRange.warning {
+ /* images/squiggly_warning.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
+}
+.annotationRange.currentBracket {
+}
+.annotationRange.matchingBracket {
+ outline: 1px solid red;
+}
+
+/* Styles for lines of text */
+.annotationLine {
+}
+.annotationLine.currentLine {
+ background-color: #EAF2FE;
+}
+
+.token_singleline_comment {
+ color: green;
+}
+
+.token_multiline_comment {
+ color: green;
+}
+
+.token_doc_comment {
+ color: #00008F;
+}
+
+.token_doc_html_markup {
+ color: #7F7F9F;
+}
+
+.token_doc_tag {
+ color: #7F9FBF;
+}
+
+.token_task_tag {
+ color: #7F9FBF;
+}
+
+.token_string {
+ color: blue;
+}
+
+.token_keyword {
+ color: darkred;
+ font-weight: bold;
+}
+
+.token_space {
+ /* images/white_space.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAAABnRSTlMA/wAAAACkwsAdAAAAIUlEQVR4nGP4z8CAC+GUIEXuABhgkTuABEiRw2cmae4EAH05X7xDolNRAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+.token_tab {
+ /* images/white_tab.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAIAAACJ2loDAAAABnRSTlMA/wD/AP83WBt9AAAAMklEQVR4nGP4TwRgoK6i52c3bz5w6zMSA6tJn28d2Lx589nnCAYu63AaSLxJRLoJPwAAeNk0aG4opfMAAAAASUVORK5CYII=");
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+.line_caret {
+ background-color: #EAF2FE;
+}
+
+/* Styling for html syntax highlighting */
+.entity-name-tag {
+ color: #3f7f7f;
+}
+
+.entity-other-attribute-name {
+ color: #7f007f;
+}
+
+.punctuation-definition-comment {
+ color: #3f5fbf;
+}
+
+.comment {
+ color: #3f5fbf
+}
+
+.string-quoted {
+ color: #2a00ff;
+ font-style: italic;
+}
+
+.invalid {
+ color: red;
+ font-weight: bold;
+} \ No newline at end of file
diff --git a/browser/devtools/sourceeditor/orion/orion.js b/browser/devtools/sourceeditor/orion/orion.js
new file mode 100644
index 000000000..06ad42195
--- /dev/null
+++ b/browser/devtools/sourceeditor/orion/orion.js
@@ -0,0 +1,12303 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ * Mihai Sucan (Mozilla Foundation) - fix for Bug#364214
+ */
+
+/*global window */
+
+/**
+ * Evaluates the definition function and mixes in the returned module with
+ * the module specified by <code>moduleName</code>.
+ * <p>
+ * This function is intented to by used when RequireJS is not available.
+ * </p>
+ *
+ * @param {String} name The mixin module name.
+ * @param {String[]} deps The array of dependency names.
+ * @param {Function} callback The definition function.
+ */
+if (!window.define) {
+ window.define = function(name, deps, callback) {
+ var module = this;
+ var split = (name || "").split("/"), i, j;
+ for (i = 0; i < split.length - 1; i++) {
+ module = module[split[i]] = (module[split[i]] || {});
+ }
+ var depModules = [], depModule;
+ for (j = 0; j < deps.length; j++) {
+ depModule = this;
+ split = deps[j].split("/");
+ for (i = 0; i < split.length - 1; i++) {
+ depModule = depModule[split[i]] = (depModule[split[i]] || {});
+ }
+ depModules.push(depModule);
+ }
+ var newModule = callback.apply(this, depModules);
+ for (var p in newModule) {
+ if (newModule.hasOwnProperty(p)) {
+ module[p] = newModule[p];
+ }
+ }
+ };
+}
+
+/**
+ * Require/get the defined modules.
+ * <p>
+ * This function is intented to by used when RequireJS is not available.
+ * </p>
+ *
+ * @param {String[]|String} deps The array of dependency names. This can also be
+ * a string, a single dependency name.
+ * @param {Function} [callback] Optional, the callback function to execute when
+ * multiple dependencies are required. The callback arguments will have
+ * references to each module in the same order as the deps array.
+ * @returns {Object|undefined} If the deps parameter is a string, then this
+ * function returns the required module definition, otherwise undefined is
+ * returned.
+ */
+if (!window.require) {
+ window.require = function(deps, callback) {
+ var depsArr = typeof deps === "string" ? [deps] : deps;
+ var depModules = [], depModule, split, i, j;
+ for (j = 0; j < depsArr.length; j++) {
+ depModule = this;
+ split = depsArr[j].split("/");
+ for (i = 0; i < split.length - 1; i++) {
+ depModule = depModule[split[i]] = (depModule[split[i]] || {});
+ }
+ depModules.push(depModule);
+ }
+ if (callback) {
+ callback.apply(this, depModules);
+ }
+ return typeof deps === "string" ? depModules[0] : undefined;
+ };
+}/*******************************************************************************
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+define("orion/textview/eventTarget", [], function() {
+ /**
+ * Constructs a new EventTarget object.
+ *
+ * @class
+ * @name orion.textview.EventTarget
+ */
+ function EventTarget() {
+ }
+ /**
+ * Adds in the event target interface into the specified object.
+ *
+ * @param {Object} object The object to add in the event target interface.
+ */
+ EventTarget.addMixin = function(object) {
+ var proto = EventTarget.prototype;
+ for (var p in proto) {
+ if (proto.hasOwnProperty(p)) {
+ object[p] = proto[p];
+ }
+ }
+ };
+ EventTarget.prototype = /** @lends orion.textview.EventTarget.prototype */ {
+ /**
+ * Adds an event listener to this event target.
+ *
+ * @param {String} type The event type.
+ * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
+ * @param {Boolean} [useCapture=false] <code>true</code> if the listener should be trigged in the capture phase.
+ *
+ * @see #removeEventListener
+ */
+ addEventListener: function(type, listener, useCapture) {
+ if (!this._eventTypes) { this._eventTypes = {}; }
+ var state = this._eventTypes[type];
+ if (!state) {
+ state = this._eventTypes[type] = {level: 0, listeners: []};
+ }
+ var listeners = state.listeners;
+ listeners.push({listener: listener, useCapture: useCapture});
+ },
+ /**
+ * Dispatches the given event to the listeners added to this event target.
+ * @param {Event} evt The event to dispatch.
+ */
+ dispatchEvent: function(evt) {
+ if (!this._eventTypes) { return; }
+ var type = evt.type;
+ var state = this._eventTypes[type];
+ if (state) {
+ var listeners = state.listeners;
+ try {
+ state.level++;
+ if (listeners) {
+ for (var i=0, len=listeners.length; i < len; i++) {
+ if (listeners[i]) {
+ var l = listeners[i].listener;
+ if (typeof l === "function") {
+ l.call(this, evt);
+ } else if (l.handleEvent && typeof l.handleEvent === "function") {
+ l.handleEvent(evt);
+ }
+ }
+ }
+ }
+ } finally {
+ state.level--;
+ if (state.compact && state.level === 0) {
+ for (var j=listeners.length - 1; j >= 0; j--) {
+ if (!listeners[j]) {
+ listeners.splice(j, 1);
+ }
+ }
+ if (listeners.length === 0) {
+ delete this._eventTypes[type];
+ }
+ state.compact = false;
+ }
+ }
+ }
+ },
+ /**
+ * Returns whether there is a listener for the specified event type.
+ *
+ * @param {String} type The event type
+ *
+ * @see #addEventListener
+ * @see #removeEventListener
+ */
+ isListening: function(type) {
+ if (!this._eventTypes) { return false; }
+ return this._eventTypes[type] !== undefined;
+ },
+ /**
+ * Removes an event listener from the event target.
+ * <p>
+ * All the parameters must be the same ones used to add the listener.
+ * </p>
+ *
+ * @param {String} type The event type
+ * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
+ * @param {Boolean} [useCapture=false] <code>true</code> if the listener should be trigged in the capture phase.
+ *
+ * @see #addEventListener
+ */
+ removeEventListener: function(type, listener, useCapture){
+ if (!this._eventTypes) { return; }
+ var state = this._eventTypes[type];
+ if (state) {
+ var listeners = state.listeners;
+ for (var i=0, len=listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l && l.listener === listener && l.useCapture === useCapture) {
+ if (state.level !== 0) {
+ listeners[i] = null;
+ state.compact = true;
+ } else {
+ listeners.splice(i, 1);
+ }
+ break;
+ }
+ }
+ if (listeners.length === 0) {
+ delete this._eventTypes[type];
+ }
+ }
+ }
+ };
+ return {EventTarget: EventTarget};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+/*global define */
+/*jslint browser:true regexp:false*/
+/**
+ * @name orion.editor.regex
+ * @class Utilities for dealing with regular expressions.
+ * @description Utilities for dealing with regular expressions.
+ */
+define("orion/editor/regex", [], function() {
+ /**
+ * @methodOf orion.editor.regex
+ * @static
+ * @description Escapes regex special characters in the input string.
+ * @param {String} str The string to escape.
+ * @returns {String} A copy of <code>str</code> with regex special characters escaped.
+ */
+ function escape(str) {
+ return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&");
+ }
+
+ /**
+ * @methodOf orion.editor.regex
+ * @static
+ * @description Parses a pattern and flags out of a regex literal string.
+ * @param {String} str The string to parse. Should look something like <code>"/ab+c/"</code> or <code>"/ab+c/i"</code>.
+ * @returns {Object} If <code>str</code> looks like a regex literal, returns an object with properties
+ * <code><dl>
+ * <dt>pattern</dt><dd>{String}</dd>
+ * <dt>flags</dt><dd>{String}</dd>
+ * </dl></code> otherwise returns <code>null</code>.
+ */
+ function parse(str) {
+ var regexp = /^\s*\/(.+)\/([gim]{0,3})\s*$/.exec(str);
+ if (regexp) {
+ return {
+ pattern : regexp[1],
+ flags : regexp[2]
+ };
+ }
+ return null;
+ }
+
+ return {
+ escape: escape,
+ parse: parse
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global window define */
+
+define("orion/textview/keyBinding", [], function() {
+ var isMac = window.navigator.platform.indexOf("Mac") !== -1;
+
+ /**
+ * Constructs a new key binding with the given key code and modifiers.
+ *
+ * @param {String|Number} keyCode the key code.
+ * @param {Boolean} mod1 the primary modifier (usually Command on Mac and Control on other platforms).
+ * @param {Boolean} mod2 the secondary modifier (usually Shift).
+ * @param {Boolean} mod3 the third modifier (usually Alt).
+ * @param {Boolean} mod4 the fourth modifier (usually Control on the Mac).
+ *
+ * @class A KeyBinding represents of a key code and a modifier state that can be triggered by the user using the keyboard.
+ * @name orion.textview.KeyBinding
+ *
+ * @property {String|Number} keyCode The key code.
+ * @property {Boolean} mod1 The primary modifier (usually Command on Mac and Control on other platforms).
+ * @property {Boolean} mod2 The secondary modifier (usually Shift).
+ * @property {Boolean} mod3 The third modifier (usually Alt).
+ * @property {Boolean} mod4 The fourth modifier (usually Control on the Mac).
+ *
+ * @see orion.textview.TextView#setKeyBinding
+ */
+ function KeyBinding (keyCode, mod1, mod2, mod3, mod4) {
+ if (typeof(keyCode) === "string") {
+ this.keyCode = keyCode.toUpperCase().charCodeAt(0);
+ } else {
+ this.keyCode = keyCode;
+ }
+ this.mod1 = mod1 !== undefined && mod1 !== null ? mod1 : false;
+ this.mod2 = mod2 !== undefined && mod2 !== null ? mod2 : false;
+ this.mod3 = mod3 !== undefined && mod3 !== null ? mod3 : false;
+ this.mod4 = mod4 !== undefined && mod4 !== null ? mod4 : false;
+ }
+ KeyBinding.prototype = /** @lends orion.textview.KeyBinding.prototype */ {
+ /**
+ * Returns whether this key binding matches the given key event.
+ *
+ * @param e the key event.
+ * @returns {Boolean} <code>true</code> whether the key binding matches the key event.
+ */
+ match: function (e) {
+ if (this.keyCode === e.keyCode) {
+ var mod1 = isMac ? e.metaKey : e.ctrlKey;
+ if (this.mod1 !== mod1) { return false; }
+ if (this.mod2 !== e.shiftKey) { return false; }
+ if (this.mod3 !== e.altKey) { return false; }
+ if (isMac && this.mod4 !== e.ctrlKey) { return false; }
+ return true;
+ }
+ return false;
+ },
+ /**
+ * Returns whether this key binding is the same as the given parameter.
+ *
+ * @param {orion.textview.KeyBinding} kb the key binding to compare with.
+ * @returns {Boolean} whether or not the parameter and the receiver describe the same key binding.
+ */
+ equals: function(kb) {
+ if (!kb) { return false; }
+ if (this.keyCode !== kb.keyCode) { return false; }
+ if (this.mod1 !== kb.mod1) { return false; }
+ if (this.mod2 !== kb.mod2) { return false; }
+ if (this.mod3 !== kb.mod3) { return false; }
+ if (this.mod4 !== kb.mod4) { return false; }
+ return true;
+ }
+ };
+ return {KeyBinding: KeyBinding};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/annotations", ['orion/textview/eventTarget'], function(mEventTarget) {
+ /**
+ * @class This object represents a decoration attached to a range of text. Annotations are added to a
+ * <code>AnnotationModel</code> which is attached to a <code>TextModel</code>.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.AnnotationModel}<br/>
+ * {@link orion.textview.Ruler}<br/>
+ * </p>
+ * @name orion.textview.Annotation
+ *
+ * @property {String} type The annotation type (for example, orion.annotation.error).
+ * @property {Number} start The start offset of the annotation in the text model.
+ * @property {Number} end The end offset of the annotation in the text model.
+ * @property {String} html The HTML displayed for the annotation.
+ * @property {String} title The text description for the annotation.
+ * @property {orion.textview.Style} style The style information for the annotation used in the annotations ruler and tooltips.
+ * @property {orion.textview.Style} overviewStyle The style information for the annotation used in the overview ruler.
+ * @property {orion.textview.Style} rangeStyle The style information for the annotation used in the text view to decorate a range of text.
+ * @property {orion.textview.Style} lineStyle The style information for the annotation used in the text view to decorate a line of text.
+ */
+ /**
+ * Constructs a new folding annotation.
+ *
+ * @param {orion.textview.ProjectionTextModel} projectionModel The projection text model.
+ * @param {String} type The annotation type.
+ * @param {Number} start The start offset of the annotation in the text model.
+ * @param {Number} end The end offset of the annotation in the text model.
+ * @param {String} expandedHTML The HTML displayed for this annotation when it is expanded.
+ * @param {orion.textview.Style} expandedStyle The style information for the annotation when it is expanded.
+ * @param {String} collapsedHTML The HTML displayed for this annotation when it is collapsed.
+ * @param {orion.textview.Style} collapsedStyle The style information for the annotation when it is collapsed.
+ *
+ * @class This object represents a folding annotation.
+ * @name orion.textview.FoldingAnnotation
+ */
+ function FoldingAnnotation (projectionModel, type, start, end, expandedHTML, expandedStyle, collapsedHTML, collapsedStyle) {
+ this.type = type;
+ this.start = start;
+ this.end = end;
+ this._projectionModel = projectionModel;
+ this._expandedHTML = this.html = expandedHTML;
+ this._expandedStyle = this.style = expandedStyle;
+ this._collapsedHTML = collapsedHTML;
+ this._collapsedStyle = collapsedStyle;
+ this.expanded = true;
+ }
+
+ FoldingAnnotation.prototype = /** @lends orion.textview.FoldingAnnotation.prototype */ {
+ /**
+ * Collapses the annotation.
+ */
+ collapse: function () {
+ if (!this.expanded) { return; }
+ this.expanded = false;
+ this.html = this._collapsedHTML;
+ this.style = this._collapsedStyle;
+ var projectionModel = this._projectionModel;
+ var baseModel = projectionModel.getBaseModel();
+ this._projection = {
+ start: baseModel.getLineStart(baseModel.getLineAtOffset(this.start) + 1),
+ end: baseModel.getLineEnd(baseModel.getLineAtOffset(this.end), true)
+ };
+ projectionModel.addProjection(this._projection);
+ },
+ /**
+ * Expands the annotation.
+ */
+ expand: function () {
+ if (this.expanded) { return; }
+ this.expanded = true;
+ this.html = this._expandedHTML;
+ this.style = this._expandedStyle;
+ this._projectionModel.removeProjection(this._projection);
+ }
+ };
+
+ /**
+ * Constructs a new AnnotationTypeList object.
+ *
+ * @class
+ * @name orion.textview.AnnotationTypeList
+ */
+ function AnnotationTypeList () {
+ }
+ /**
+ * Adds in the annotation type interface into the specified object.
+ *
+ * @param {Object} object The object to add in the annotation type interface.
+ */
+ AnnotationTypeList.addMixin = function(object) {
+ var proto = AnnotationTypeList.prototype;
+ for (var p in proto) {
+ if (proto.hasOwnProperty(p)) {
+ object[p] = proto[p];
+ }
+ }
+ };
+ AnnotationTypeList.prototype = /** @lends orion.textview.AnnotationTypeList.prototype */ {
+ /**
+ * Adds an annotation type to the receiver.
+ * <p>
+ * Only annotations of the specified types will be shown by
+ * the receiver.
+ * </p>
+ *
+ * @param {Object} type the annotation type to be shown
+ *
+ * @see #removeAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ addAnnotationType: function(type) {
+ if (!this._annotationTypes) { this._annotationTypes = []; }
+ this._annotationTypes.push(type);
+ },
+ /**
+ * Gets the annotation type priority. The priority is determined by the
+ * order the annotation type is added to the receiver. Annotation types
+ * added first have higher priority.
+ * <p>
+ * Returns <code>0</code> if the annotation type is not added.
+ * </p>
+ *
+ * @param {Object} type the annotation type
+ *
+ * @see #addAnnotationType
+ * @see #removeAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ getAnnotationTypePriority: function(type) {
+ if (this._annotationTypes) {
+ for (var i = 0; i < this._annotationTypes.length; i++) {
+ if (this._annotationTypes[i] === type) {
+ return i + 1;
+ }
+ }
+ }
+ return 0;
+ },
+ /**
+ * Returns an array of annotations in the specified annotation model for the given range of text sorted by type.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
+ * @param {Number} start the start offset of the range.
+ * @param {Number} end the end offset of the range.
+ * @return {orion.textview.Annotation[]} an annotation array.
+ */
+ getAnnotationsByType: function(annotationModel, start, end) {
+ var iter = annotationModel.getAnnotations(start, end);
+ var annotation, annotations = [];
+ while (iter.hasNext()) {
+ annotation = iter.next();
+ var priority = this.getAnnotationTypePriority(annotation.type);
+ if (priority === 0) { continue; }
+ annotations.push(annotation);
+ }
+ var self = this;
+ annotations.sort(function(a, b) {
+ return self.getAnnotationTypePriority(a.type) - self.getAnnotationTypePriority(b.type);
+ });
+ return annotations;
+ },
+ /**
+ * Returns whether the receiver shows annotations of the specified type.
+ *
+ * @param {Object} type the annotation type
+ * @returns {Boolean} whether the specified annotation type is shown
+ *
+ * @see #addAnnotationType
+ * @see #removeAnnotationType
+ */
+ isAnnotationTypeVisible: function(type) {
+ return this.getAnnotationTypePriority(type) !== 0;
+ },
+ /**
+ * Removes an annotation type from the receiver.
+ *
+ * @param {Object} type the annotation type to be removed
+ *
+ * @see #addAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ removeAnnotationType: function(type) {
+ if (!this._annotationTypes) { return; }
+ for (var i = 0; i < this._annotationTypes.length; i++) {
+ if (this._annotationTypes[i] === type) {
+ this._annotationTypes.splice(i, 1);
+ break;
+ }
+ }
+ }
+ };
+
+ /**
+ * Constructs an annotation model.
+ *
+ * @param {textModel} textModel The text model.
+ *
+ * @class This object manages annotations for a <code>TextModel</code>.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.Annotation}<br/>
+ * {@link orion.textview.TextModel}<br/>
+ * </p>
+ * @name orion.textview.AnnotationModel
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function AnnotationModel(textModel) {
+ this._annotations = [];
+ var self = this;
+ this._listener = {
+ onChanged: function(modelChangedEvent) {
+ self._onChanged(modelChangedEvent);
+ }
+ };
+ this.setTextModel(textModel);
+ }
+
+ AnnotationModel.prototype = /** @lends orion.textview.AnnotationModel.prototype */ {
+ /**
+ * Adds an annotation to the annotation model.
+ * <p>The annotation model listeners are notified of this change.</p>
+ *
+ * @param {orion.textview.Annotation} annotation the annotation to be added.
+ *
+ * @see #removeAnnotation
+ */
+ addAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var annotations = this._annotations;
+ var index = this._binarySearch(annotations, annotation.start);
+ annotations.splice(index, 0, annotation);
+ var e = {
+ type: "Changed",
+ added: [annotation],
+ removed: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Returns the text model.
+ *
+ * @return {orion.textview.TextModel} The text model.
+ *
+ * @see #setTextModel
+ */
+ getTextModel: function() {
+ return this._model;
+ },
+ /**
+ * @class This object represents an annotation iterator.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.AnnotationModel#getAnnotations}<br/>
+ * </p>
+ * @name orion.textview.AnnotationIterator
+ *
+ * @property {Function} hasNext Determines whether there are more annotations in the iterator.
+ * @property {Function} next Returns the next annotation in the iterator.
+ */
+ /**
+ * Returns an iterator of annotations for the given range of text.
+ *
+ * @param {Number} start the start offset of the range.
+ * @param {Number} end the end offset of the range.
+ * @return {orion.textview.AnnotationIterator} an annotation iterartor.
+ */
+ getAnnotations: function(start, end) {
+ var annotations = this._annotations, current;
+ //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
+ var i = 0;
+ var skip = function() {
+ while (i < annotations.length) {
+ var a = annotations[i++];
+ if ((start === a.start) || (start > a.start ? start < a.end : a.start < end)) {
+ return a;
+ }
+ if (a.start >= end) {
+ break;
+ }
+ }
+ return null;
+ };
+ current = skip();
+ return {
+ next: function() {
+ var result = current;
+ if (result) { current = skip(); }
+ return result;
+ },
+ hasNext: function() {
+ return current !== null;
+ }
+ };
+ },
+ /**
+ * Notifies the annotation model that the given annotation has been modified.
+ * <p>The annotation model listeners are notified of this change.</p>
+ *
+ * @param {orion.textview.Annotation} annotation the modified annotation.
+ *
+ * @see #addAnnotation
+ */
+ modifyAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var index = this._getAnnotationIndex(annotation);
+ if (index < 0) { return; }
+ var e = {
+ type: "Changed",
+ added: [],
+ removed: [],
+ changed: [annotation]
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Notifies all listeners that the annotation model has changed.
+ *
+ * @param {orion.textview.Annotation[]} added The list of annotation being added to the model.
+ * @param {orion.textview.Annotation[]} changed The list of annotation modified in the model.
+ * @param {orion.textview.Annotation[]} removed The list of annotation being removed from the model.
+ * @param {ModelChangedEvent} textModelChangedEvent the text model changed event that trigger this change, can be null if the change was trigger by a method call (for example, {@link #addAnnotation}).
+ */
+ onChanged: function(e) {
+ return this.dispatchEvent(e);
+ },
+ /**
+ * Removes all annotations of the given <code>type</code>. All annotations
+ * are removed if the type is not specified.
+ * <p>The annotation model listeners are notified of this change. Only one changed event is generated.</p>
+ *
+ * @param {Object} type the type of annotations to be removed.
+ *
+ * @see #removeAnnotation
+ */
+ removeAnnotations: function(type) {
+ var annotations = this._annotations;
+ var removed, i;
+ if (type) {
+ removed = [];
+ for (i = annotations.length - 1; i >= 0; i--) {
+ var annotation = annotations[i];
+ if (annotation.type === type) {
+ annotations.splice(i, 1);
+ }
+ removed.splice(0, 0, annotation);
+ }
+ } else {
+ removed = annotations;
+ annotations = [];
+ }
+ var e = {
+ type: "Changed",
+ removed: removed,
+ added: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Removes an annotation from the annotation model.
+ * <p>The annotation model listeners are notified of this change.</p>
+ *
+ * @param {orion.textview.Annotation} annotation the annotation to be removed.
+ *
+ * @see #addAnnotation
+ */
+ removeAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var index = this._getAnnotationIndex(annotation);
+ if (index < 0) { return; }
+ var e = {
+ type: "Changed",
+ removed: this._annotations.splice(index, 1),
+ added: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Removes and adds the specifed annotations to the annotation model.
+ * <p>The annotation model listeners are notified of this change. Only one changed event is generated.</p>
+ *
+ * @param {orion.textview.Annotation} remove the annotations to be removed.
+ * @param {orion.textview.Annotation} add the annotations to be added.
+ *
+ * @see #addAnnotation
+ * @see #removeAnnotation
+ */
+ replaceAnnotations: function(remove, add) {
+ var annotations = this._annotations, i, index, annotation, removed = [];
+ if (remove) {
+ for (i = remove.length - 1; i >= 0; i--) {
+ annotation = remove[i];
+ index = this._getAnnotationIndex(annotation);
+ if (index < 0) { continue; }
+ annotations.splice(index, 1);
+ removed.splice(0, 0, annotation);
+ }
+ }
+ if (!add) { add = []; }
+ for (i = 0; i < add.length; i++) {
+ annotation = add[i];
+ index = this._binarySearch(annotations, annotation.start);
+ annotations.splice(index, 0, annotation);
+ }
+ var e = {
+ type: "Changed",
+ removed: removed,
+ added: add,
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Sets the text model of the annotation model. The annotation
+ * model listens for changes in the text model to update and remove
+ * annotations that are affected by the change.
+ *
+ * @param {orion.textview.TextModel} textModel the text model.
+ *
+ * @see #getTextModel
+ */
+ setTextModel: function(textModel) {
+ if (this._model) {
+ this._model.removeEventListener("Changed", this._listener.onChanged);
+ }
+ this._model = textModel;
+ if (this._model) {
+ this._model.addEventListener("Changed", this._listener.onChanged);
+ }
+ },
+ /** @ignore */
+ _binarySearch: function (array, offset) {
+ var high = array.length, low = -1, index;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ /** @ignore */
+ _getAnnotationIndex: function(annotation) {
+ var annotations = this._annotations;
+ var index = this._binarySearch(annotations, annotation.start);
+ while (index < annotations.length && annotations[index].start === annotation.start) {
+ if (annotations[index] === annotation) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ },
+ /** @ignore */
+ _onChanged: function(modelChangedEvent) {
+ var start = modelChangedEvent.start;
+ var addedCharCount = modelChangedEvent.addedCharCount;
+ var removedCharCount = modelChangedEvent.removedCharCount;
+ var annotations = this._annotations, end = start + removedCharCount;
+ //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
+ var startIndex = 0;
+ if (!(0 <= startIndex && startIndex < annotations.length)) { return; }
+ var e = {
+ type: "Changed",
+ added: [],
+ removed: [],
+ changed: [],
+ textModelChangedEvent: modelChangedEvent
+ };
+ var changeCount = addedCharCount - removedCharCount, i;
+ for (i = startIndex; i < annotations.length; i++) {
+ var annotation = annotations[i];
+ if (annotation.start >= end) {
+ annotation.start += changeCount;
+ annotation.end += changeCount;
+ e.changed.push(annotation);
+ } else if (annotation.end <= start) {
+ //nothing
+ } else if (annotation.start < start && end < annotation.end) {
+ annotation.end += changeCount;
+ e.changed.push(annotation);
+ } else {
+ annotations.splice(i, 1);
+ e.removed.push(annotation);
+ i--;
+ }
+ }
+ if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) {
+ this.onChanged(e);
+ }
+ }
+ };
+ mEventTarget.EventTarget.addMixin(AnnotationModel.prototype);
+
+ /**
+ * Constructs a new styler for annotations.
+ *
+ * @param {orion.textview.TextView} view The styler view.
+ * @param {orion.textview.AnnotationModel} view The styler annotation model.
+ *
+ * @class This object represents a styler for annotation attached to a text view.
+ * @name orion.textview.AnnotationStyler
+ * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
+ * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
+ * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
+ */
+ function AnnotationStyler (view, annotationModel) {
+ this._view = view;
+ this._annotationModel = annotationModel;
+ var self = this;
+ this._listener = {
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self._onLineStyle(e);
+ },
+ onChanged: function(e) {
+ self._onAnnotationModelChanged(e);
+ }
+ };
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ view.addEventListener("LineStyle", this._listener.onLineStyle);
+ annotationModel.addEventListener("Changed", this._listener.onChanged);
+ }
+ AnnotationStyler.prototype = /** @lends orion.textview.AnnotationStyler.prototype */ {
+ /**
+ * Destroys the styler.
+ * <p>
+ * Removes all listeners added by this styler.
+ * </p>
+ */
+ destroy: function() {
+ var view = this._view;
+ if (view) {
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ view.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.view = null;
+ }
+ var annotationModel = this._annotationModel;
+ if (annotationModel) {
+ annotationModel.removeEventListener("Changed", this._listener.onChanged);
+ annotationModel = null;
+ }
+ },
+ _mergeStyle: function(result, style) {
+ if (style) {
+ if (!result) { result = {}; }
+ if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
+ result.styleClass += " " + style.styleClass;
+ } else {
+ result.styleClass = style.styleClass;
+ }
+ var prop;
+ if (style.style) {
+ if (!result.style) { result.style = {}; }
+ for (prop in style.style) {
+ if (!result.style[prop]) {
+ result.style[prop] = style.style[prop];
+ }
+ }
+ }
+ if (style.attributes) {
+ if (!result.attributes) { result.attributes = {}; }
+ for (prop in style.attributes) {
+ if (!result.attributes[prop]) {
+ result.attributes[prop] = style.attributes[prop];
+ }
+ }
+ }
+ }
+ return result;
+ },
+ _mergeStyleRanges: function(ranges, styleRange) {
+ if (!ranges) { return; }
+ for (var i=0; i<ranges.length; i++) {
+ var range = ranges[i];
+ if (styleRange.end <= range.start) { break; }
+ if (styleRange.start >= range.end) { continue; }
+ var mergedStyle = this._mergeStyle({}, range.style);
+ mergedStyle = this._mergeStyle(mergedStyle, styleRange.style);
+ if (styleRange.start <= range.start && styleRange.end >= range.end) {
+ ranges[i] = {start: range.start, end: range.end, style: mergedStyle};
+ } else if (styleRange.start > range.start && styleRange.end < range.end) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.start, style: range.style},
+ {start: styleRange.start, end: styleRange.end, style: mergedStyle},
+ {start: styleRange.end, end: range.end, style: range.style});
+ i += 2;
+ } else if (styleRange.start > range.start) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.start, style: range.style},
+ {start: styleRange.start, end: range.end, style: mergedStyle});
+ i += 1;
+ } else if (styleRange.end < range.end) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.end, style: mergedStyle},
+ {start: styleRange.end, end: range.end, style: range.style});
+ i += 1;
+ }
+ }
+ },
+ _onAnnotationModelChanged: function(e) {
+ if (e.textModelChangedEvent) {
+ return;
+ }
+ var view = this._view;
+ if (!view) { return; }
+ var self = this;
+ var model = view.getModel();
+ function redraw(changes) {
+ for (var i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ var end = changes[i].end;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ end = model.mapOffset(end, true);
+ }
+ if (start !== -1 && end !== -1) {
+ view.redrawRange(start, end);
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onLineStyle: function (e) {
+ var annotationModel = this._annotationModel;
+ var viewModel = this._view.getModel();
+ var baseModel = annotationModel.getTextModel();
+ var start = e.lineStart;
+ var end = e.lineStart + e.lineText.length;
+ if (baseModel !== viewModel) {
+ start = viewModel.mapOffset(start);
+ end = viewModel.mapOffset(end);
+ }
+ var annotations = annotationModel.getAnnotations(start, end);
+ while (annotations.hasNext()) {
+ var annotation = annotations.next();
+ if (!this.isAnnotationTypeVisible(annotation.type)) { continue; }
+ if (annotation.rangeStyle) {
+ var annotationStart = annotation.start;
+ var annotationEnd = annotation.end;
+ if (baseModel !== viewModel) {
+ annotationStart = viewModel.mapOffset(annotationStart, true);
+ annotationEnd = viewModel.mapOffset(annotationEnd, true);
+ }
+ this._mergeStyleRanges(e.ranges, {start: annotationStart, end: annotationEnd, style: annotation.rangeStyle});
+ }
+ if (annotation.lineStyle) {
+ e.style = this._mergeStyle({}, e.style);
+ e.style = this._mergeStyle(e.style, annotation.lineStyle);
+ }
+ }
+ }
+ };
+ AnnotationTypeList.addMixin(AnnotationStyler.prototype);
+
+ return {
+ FoldingAnnotation: FoldingAnnotation,
+ AnnotationTypeList: AnnotationTypeList,
+ AnnotationModel: AnnotationModel,
+ AnnotationStyler: AnnotationStyler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define setTimeout clearTimeout setInterval clearInterval Node */
+
+define("orion/textview/rulers", ['orion/textview/annotations', 'orion/textview/tooltip'], function(mAnnotations, mTooltip) {
+
+ /**
+ * Constructs a new ruler.
+ * <p>
+ * The default implementation does not implement all the methods in the interface
+ * and is useful only for objects implementing rulers.
+ * <p/>
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {String} [rulerOverview="page"] the overview for the ruler.
+ * @param {orion.textview.Style} [rulerStyle] the style for the ruler.
+ *
+ * @class This interface represents a ruler for the text view.
+ * <p>
+ * A Ruler is a graphical element that is placed either on the left or on the right side of
+ * the view. It can be used to provide the view with per line decoration such as line numbering,
+ * bookmarks, breakpoints, folding disclosures, etc.
+ * </p><p>
+ * There are two types of rulers: page and document. A page ruler only shows the content for the lines that are
+ * visible, while a document ruler always shows the whole content.
+ * </p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.LineNumberRuler}<br/>
+ * {@link orion.textview.AnnotationRuler}<br/>
+ * {@link orion.textview.OverviewRuler}<br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#addRuler}
+ * </p>
+ * @name orion.textview.Ruler
+ * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
+ * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
+ * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
+ */
+ function Ruler (annotationModel, rulerLocation, rulerOverview, rulerStyle) {
+ this._location = rulerLocation || "left";
+ this._overview = rulerOverview || "page";
+ this._rulerStyle = rulerStyle;
+ this._view = null;
+ var self = this;
+ this._listener = {
+ onTextModelChanged: function(e) {
+ self._onTextModelChanged(e);
+ },
+ onAnnotationModelChanged: function(e) {
+ self._onAnnotationModelChanged(e);
+ }
+ };
+ this.setAnnotationModel(annotationModel);
+ }
+ Ruler.prototype = /** @lends orion.textview.Ruler.prototype */ {
+ /**
+ * Returns the annotations for a given line range merging multiple
+ * annotations when necessary.
+ * <p>
+ * This method is called by the text view when the ruler is redrawn.
+ * </p>
+ *
+ * @param {Number} startLine the start line index
+ * @param {Number} endLine the end line index
+ * @return {orion.textview.Annotation[]} the annotations for the line range. The array might be sparse.
+ */
+ getAnnotations: function(startLine, endLine) {
+ var annotationModel = this._annotationModel;
+ if (!annotationModel) { return []; }
+ var model = this._view.getModel();
+ var start = model.getLineStart(startLine);
+ var end = model.getLineEnd(endLine - 1);
+ var baseModel = model;
+ if (model.getBaseModel) {
+ baseModel = model.getBaseModel();
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ var result = [];
+ var annotations = this.getAnnotationsByType(annotationModel, start, end);
+ for (var i = 0; i < annotations.length; i++) {
+ var annotation = annotations[i];
+ var annotationLineStart = baseModel.getLineAtOffset(annotation.start);
+ var annotationLineEnd = baseModel.getLineAtOffset(Math.max(annotation.start, annotation.end - 1));
+ for (var lineIndex = annotationLineStart; lineIndex<=annotationLineEnd; lineIndex++) {
+ var visualLineIndex = lineIndex;
+ if (model !== baseModel) {
+ var ls = baseModel.getLineStart(lineIndex);
+ ls = model.mapOffset(ls, true);
+ if (ls === -1) { continue; }
+ visualLineIndex = model.getLineAtOffset(ls);
+ }
+ if (!(startLine <= visualLineIndex && visualLineIndex < endLine)) { continue; }
+ var rulerAnnotation = this._mergeAnnotation(result[visualLineIndex], annotation, lineIndex - annotationLineStart, annotationLineEnd - annotationLineStart + 1);
+ if (rulerAnnotation) {
+ result[visualLineIndex] = rulerAnnotation;
+ }
+ }
+ }
+ if (!this._multiAnnotation && this._multiAnnotationOverlay) {
+ for (var k in result) {
+ if (result[k]._multiple) {
+ result[k].html = result[k].html + this._multiAnnotationOverlay.html;
+ }
+ }
+ }
+ return result;
+ },
+ /**
+ * Returns the annotation model.
+ *
+ * @returns {orion.textview.AnnotationModel} the ruler annotation model.
+ *
+ * @see #setAnnotationModel
+ */
+ getAnnotationModel: function() {
+ return this._annotationModel;
+ },
+ /**
+ * Returns the ruler location.
+ *
+ * @returns {String} the ruler location, which is either "left" or "right".
+ *
+ * @see #getOverview
+ */
+ getLocation: function() {
+ return this._location;
+ },
+ /**
+ * Returns the ruler overview type.
+ *
+ * @returns {String} the overview type, which is either "page" or "document".
+ *
+ * @see #getLocation
+ */
+ getOverview: function() {
+ return this._overview;
+ },
+ /**
+ * Returns the style information for the ruler.
+ *
+ * @returns {orion.textview.Style} the style information.
+ */
+ getRulerStyle: function() {
+ return this._rulerStyle;
+ },
+ /**
+ * Returns the widest annotation which determines the width of the ruler.
+ * <p>
+ * If the ruler does not have a fixed width it should provide the widest
+ * annotation to avoid the ruler from changing size as the view scrolls.
+ * </p>
+ * <p>
+ * This method is called by the text view when the ruler is redrawn.
+ * </p>
+ *
+ * @returns {orion.textview.Annotation} the widest annotation.
+ *
+ * @see #getAnnotations
+ */
+ getWidestAnnotation: function() {
+ return null;
+ },
+ /**
+ * Sets the annotation model for the ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
+ *
+ * @see #getAnnotationModel
+ */
+ setAnnotationModel: function (annotationModel) {
+ if (this._annotationModel) {
+ this._annotationModel.removEventListener("Changed", this._listener.onAnnotationModelChanged);
+ }
+ this._annotationModel = annotationModel;
+ if (this._annotationModel) {
+ this._annotationModel.addEventListener("Changed", this._listener.onAnnotationModelChanged);
+ }
+ },
+ /**
+ * Sets the annotation that is displayed when a given line contains multiple
+ * annotations. This annotation is used when there are different types of
+ * annotations in a given line.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation for lines with multiple annotations.
+ *
+ * @see #setMultiAnnotationOverlay
+ */
+ setMultiAnnotation: function(annotation) {
+ this._multiAnnotation = annotation;
+ },
+ /**
+ * Sets the annotation that overlays a line with multiple annotations. This annotation is displayed on
+ * top of the computed annotation for a given line when there are multiple annotations of the same type
+ * in the line. It is also used when the multiple annotation is not set.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation overlay for lines with multiple annotations.
+ *
+ * @see #setMultiAnnotation
+ */
+ setMultiAnnotationOverlay: function(annotation) {
+ this._multiAnnotationOverlay = annotation;
+ },
+ /**
+ * Sets the view for the ruler.
+ * <p>
+ * This method is called by the text view when the ruler
+ * is added to the view.
+ * </p>
+ *
+ * @param {orion.textview.TextView} view the text view.
+ */
+ setView: function (view) {
+ if (this._onTextModelChanged && this._view) {
+ this._view.removeEventListener("ModelChanged", this._listener.onTextModelChanged);
+ }
+ this._view = view;
+ if (this._onTextModelChanged && this._view) {
+ this._view.addEventListener("ModelChanged", this._listener.onTextModelChanged);
+ }
+ },
+ /**
+ * This event is sent when the user clicks a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the click event.
+ */
+ onClick: function(lineIndex, e) {
+ },
+ /**
+ * This event is sent when the user double clicks a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the double click event.
+ */
+ onDblClick: function(lineIndex, e) {
+ },
+ /**
+ * This event is sent when the user moves the mouse over a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse move event.
+ */
+ onMouseMove: function(lineIndex, e) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (!tooltip) { return; }
+ if (tooltip.isVisible() && this._tooltipLineIndex === lineIndex) { return; }
+ this._tooltipLineIndex = lineIndex;
+ var self = this;
+ tooltip.setTarget({
+ y: e.clientY,
+ getTooltipInfo: function() {
+ return self._getTooltipInfo(self._tooltipLineIndex, this.y);
+ }
+ });
+ },
+ /**
+ * This event is sent when the mouse pointer enters a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse over event.
+ */
+ onMouseOver: function(lineIndex, e) {
+ this.onMouseMove(lineIndex, e);
+ },
+ /**
+ * This event is sent when the mouse pointer exits a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse out event.
+ */
+ onMouseOut: function(lineIndex, e) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (!tooltip) { return; }
+ tooltip.setTarget(null);
+ },
+ /** @ignore */
+ _getTooltipInfo: function(lineIndex, y) {
+ if (lineIndex === undefined) { return; }
+ var view = this._view;
+ var model = view.getModel();
+ var annotationModel = this._annotationModel;
+ var annotations = [];
+ if (annotationModel) {
+ var start = model.getLineStart(lineIndex);
+ var end = model.getLineEnd(lineIndex);
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ annotations = this.getAnnotationsByType(annotationModel, start, end);
+ }
+ var contents = this._getTooltipContents(lineIndex, annotations);
+ if (!contents) { return null; }
+ var info = {
+ contents: contents,
+ anchor: this.getLocation()
+ };
+ var rect = view.getClientArea();
+ if (this.getOverview() === "document") {
+ rect.y = view.convert({y: y}, "view", "document").y;
+ } else {
+ rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y;
+ }
+ view.convert(rect, "document", "page");
+ info.x = rect.x;
+ info.y = rect.y;
+ if (info.anchor === "right") {
+ info.x += rect.width;
+ }
+ info.maxWidth = rect.width;
+ info.maxHeight = rect.height - (rect.y - view._parent.getBoundingClientRect().top);
+ return info;
+ },
+ /** @ignore */
+ _getTooltipContents: function(lineIndex, annotations) {
+ return annotations;
+ },
+ /** @ignore */
+ _onAnnotationModelChanged: function(e) {
+ var view = this._view;
+ if (!view) { return; }
+ var model = view.getModel(), self = this;
+ var lineCount = model.getLineCount();
+ if (e.textModelChangedEvent) {
+ var start = e.textModelChangedEvent.start;
+ if (model.getBaseModel) { start = model.mapOffset(start, true); }
+ var startLine = model.getLineAtOffset(start);
+ view.redrawLines(startLine, lineCount, self);
+ return;
+ }
+ function redraw(changes) {
+ for (var i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ var end = changes[i].end;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ end = model.mapOffset(end, true);
+ }
+ if (start !== -1 && end !== -1) {
+ view.redrawLines(model.getLineAtOffset(start), model.getLineAtOffset(Math.max(start, end - 1)) + 1, self);
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ },
+ /** @ignore */
+ _mergeAnnotation: function(result, annotation, annotationLineIndex, annotationLineCount) {
+ if (!result) { result = {}; }
+ if (annotationLineIndex === 0) {
+ if (result.html && annotation.html) {
+ if (annotation.html !== result.html) {
+ if (!result._multiple && this._multiAnnotation) {
+ result.html = this._multiAnnotation.html;
+ }
+ }
+ result._multiple = true;
+ } else {
+ result.html = annotation.html;
+ }
+ }
+ result.style = this._mergeStyle(result.style, annotation.style);
+ return result;
+ },
+ /** @ignore */
+ _mergeStyle: function(result, style) {
+ if (style) {
+ if (!result) { result = {}; }
+ if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
+ result.styleClass += " " + style.styleClass;
+ } else {
+ result.styleClass = style.styleClass;
+ }
+ var prop;
+ if (style.style) {
+ if (!result.style) { result.style = {}; }
+ for (prop in style.style) {
+ if (!result.style[prop]) {
+ result.style[prop] = style.style[prop];
+ }
+ }
+ }
+ if (style.attributes) {
+ if (!result.attributes) { result.attributes = {}; }
+ for (prop in style.attributes) {
+ if (!result.attributes[prop]) {
+ result.attributes[prop] = style.attributes[prop];
+ }
+ }
+ }
+ }
+ return result;
+ }
+ };
+ mAnnotations.AnnotationTypeList.addMixin(Ruler.prototype);
+
+ /**
+ * Constructs a new line numbering ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ * @param {orion.textview.Style} [oddStyle={style: {backgroundColor: "white"}] the style for lines with odd line index.
+ * @param {orion.textview.Style} [evenStyle={backgroundColor: "white"}] the style for lines with even line index.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements a line numbering ruler.
+ *
+ * <p><b>See:</b><br/>
+ * {@link orion.textview.Ruler}
+ * </p>
+ * @name orion.textview.LineNumberRuler
+ */
+ function LineNumberRuler (annotationModel, rulerLocation, rulerStyle, oddStyle, evenStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
+ this._oddStyle = oddStyle || {style: {backgroundColor: "white"}};
+ this._evenStyle = evenStyle || {style: {backgroundColor: "white"}};
+ this._numOfDigits = 0;
+ }
+ LineNumberRuler.prototype = new Ruler();
+ /** @ignore */
+ LineNumberRuler.prototype.getAnnotations = function(startLine, endLine) {
+ var result = Ruler.prototype.getAnnotations.call(this, startLine, endLine);
+ var model = this._view.getModel();
+ for (var lineIndex = startLine; lineIndex < endLine; lineIndex++) {
+ var style = lineIndex & 1 ? this._oddStyle : this._evenStyle;
+ var mapLine = lineIndex;
+ if (model.getBaseModel) {
+ var lineStart = model.getLineStart(mapLine);
+ mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
+ }
+ if (!result[lineIndex]) { result[lineIndex] = {}; }
+ result[lineIndex].html = (mapLine + 1) + "";
+ if (!result[lineIndex].style) { result[lineIndex].style = style; }
+ }
+ return result;
+ };
+ /** @ignore */
+ LineNumberRuler.prototype.getWidestAnnotation = function() {
+ var lineCount = this._view.getModel().getLineCount();
+ return this.getAnnotations(lineCount - 1, lineCount)[lineCount - 1];
+ };
+ /** @ignore */
+ LineNumberRuler.prototype._onTextModelChanged = function(e) {
+ var start = e.start;
+ var model = this._view.getModel();
+ var lineCount = model.getBaseModel ? model.getBaseModel().getLineCount() : model.getLineCount();
+ var numOfDigits = (lineCount+"").length;
+ if (this._numOfDigits !== numOfDigits) {
+ this._numOfDigits = numOfDigits;
+ var startLine = model.getLineAtOffset(start);
+ this._view.redrawLines(startLine, model.getLineCount(), this);
+ }
+ };
+
+ /**
+ * @class This is class represents an annotation for the AnnotationRuler.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.AnnotationRuler}
+ * </p>
+ *
+ * @name orion.textview.Annotation
+ *
+ * @property {String} [html=""] The html content for the annotation, typically contains an image.
+ * @property {orion.textview.Style} [style] the style for the annotation.
+ * @property {orion.textview.Style} [overviewStyle] the style for the annotation in the overview ruler.
+ */
+ /**
+ * Constructs a new annotation ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ * @param {orion.textview.Annotation} [defaultAnnotation] the default annotation.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an annotation ruler.
+ *
+ * <p><b>See:</b><br/>
+ * {@link orion.textview.Ruler}<br/>
+ * {@link orion.textview.Annotation}
+ * </p>
+ * @name orion.textview.AnnotationRuler
+ */
+ function AnnotationRuler (annotationModel, rulerLocation, rulerStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
+ }
+ AnnotationRuler.prototype = new Ruler();
+
+ /**
+ * Constructs a new overview ruler.
+ * <p>
+ * The overview ruler is used in conjunction with a AnnotationRuler, for each annotation in the
+ * AnnotationRuler this ruler displays a mark in the overview. Clicking on the mark causes the
+ * view to scroll to the annotated line.
+ * </p>
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an overview ruler.
+ *
+ * <p><b>See:</b><br/>
+ * {@link orion.textview.AnnotationRuler} <br/>
+ * {@link orion.textview.Ruler}
+ * </p>
+ * @name orion.textview.OverviewRuler
+ */
+ function OverviewRuler (annotationModel, rulerLocation, rulerStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "document", rulerStyle);
+ }
+ OverviewRuler.prototype = new Ruler();
+
+ /** @ignore */
+ OverviewRuler.prototype.getRulerStyle = function() {
+ var result = {style: {lineHeight: "1px", fontSize: "1px"}};
+ result = this._mergeStyle(result, this._rulerStyle);
+ return result;
+ };
+ /** @ignore */
+ OverviewRuler.prototype.onClick = function(lineIndex, e) {
+ if (lineIndex === undefined) { return; }
+ this._view.setTopIndex(lineIndex);
+ };
+ /** @ignore */
+ OverviewRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
+ if (annotations.length === 0) {
+ var model = this._view.getModel();
+ var mapLine = lineIndex;
+ if (model.getBaseModel) {
+ var lineStart = model.getLineStart(mapLine);
+ mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
+ }
+ return "Line: " + (mapLine + 1);
+ }
+ return Ruler.prototype._getTooltipContents.call(this, lineIndex, annotations);
+ };
+ /** @ignore */
+ OverviewRuler.prototype._mergeAnnotation = function(previousAnnotation, annotation, annotationLineIndex, annotationLineCount) {
+ if (annotationLineIndex !== 0) { return undefined; }
+ var result = previousAnnotation;
+ if (!result) {
+ //TODO annotationLineCount does not work when there are folded lines
+ var height = 3 * annotationLineCount;
+ result = {html: "&nbsp;", style: { style: {height: height + "px"}}};
+ result.style = this._mergeStyle(result.style, annotation.overviewStyle);
+ }
+ return result;
+ };
+
+ /**
+ * Constructs a new folding ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an overview ruler.
+ *
+ * <p><b>See:</b><br/>
+ * {@link orion.textview.AnnotationRuler} <br/>
+ * {@link orion.textview.Ruler}
+ * </p>
+ * @name orion.textview.OverviewRuler
+ */
+ function FoldingRuler (annotationModel, rulerLocation, rulerStyle) {
+ AnnotationRuler.call(this, annotationModel, rulerLocation, rulerStyle);
+ }
+ FoldingRuler.prototype = new AnnotationRuler();
+
+ /** @ignore */
+ FoldingRuler.prototype.onClick = function(lineIndex, e) {
+ if (lineIndex === undefined) { return; }
+ var annotationModel = this._annotationModel;
+ if (!annotationModel) { return; }
+ var view = this._view;
+ var model = view.getModel();
+ var start = model.getLineStart(lineIndex);
+ var end = model.getLineEnd(lineIndex, true);
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ var annotation, iter = annotationModel.getAnnotations(start, end);
+ while (!annotation && iter.hasNext()) {
+ var a = iter.next();
+ if (!this.isAnnotationTypeVisible(a.type)) { continue; }
+ annotation = a;
+ }
+ if (annotation) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (tooltip) {
+ tooltip.setTarget(null);
+ }
+ if (annotation.expanded) {
+ annotation.collapse();
+ } else {
+ annotation.expand();
+ }
+ this._annotationModel.modifyAnnotation(annotation);
+ }
+ };
+ /** @ignore */
+ FoldingRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
+ if (annotations.length === 1) {
+ if (annotations[0].expanded) {
+ return null;
+ }
+ }
+ return AnnotationRuler.prototype._getTooltipContents.call(this, lineIndex, annotations);
+ };
+ /** @ignore */
+ FoldingRuler.prototype._onAnnotationModelChanged = function(e) {
+ if (e.textModelChangedEvent) {
+ AnnotationRuler.prototype._onAnnotationModelChanged.call(this, e);
+ return;
+ }
+ var view = this._view;
+ if (!view) { return; }
+ var model = view.getModel(), self = this, i;
+ var lineCount = model.getLineCount(), lineIndex = lineCount;
+ function redraw(changes) {
+ for (i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ }
+ if (start !== -1) {
+ lineIndex = Math.min(lineIndex, model.getLineAtOffset(start));
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ var rulers = view.getRulers();
+ for (i = 0; i < rulers.length; i++) {
+ view.redrawLines(lineIndex, lineCount, rulers[i]);
+ }
+ };
+
+ return {
+ Ruler: Ruler,
+ AnnotationRuler: AnnotationRuler,
+ LineNumberRuler: LineNumberRuler,
+ OverviewRuler: OverviewRuler,
+ FoldingRuler: FoldingRuler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/undoStack", [], function() {
+
+ /**
+ * Constructs a new Change object.
+ *
+ * @class
+ * @name orion.textview.Change
+ * @private
+ */
+ function Change(offset, text, previousText) {
+ this.offset = offset;
+ this.text = text;
+ this.previousText = previousText;
+ }
+ Change.prototype = {
+ /** @ignore */
+ undo: function (view, select) {
+ this._doUndoRedo(this.offset, this.previousText, this.text, view, select);
+ },
+ /** @ignore */
+ redo: function (view, select) {
+ this._doUndoRedo(this.offset, this.text, this.previousText, view, select);
+ },
+ _doUndoRedo: function(offset, text, previousText, view, select) {
+ var model = view.getModel();
+ /*
+ * TODO UndoStack should be changing the text in the base model.
+ * This is code needs to change when modifications in the base
+ * model are supported properly by the projection model.
+ */
+ if (model.mapOffset && view.annotationModel) {
+ var mapOffset = model.mapOffset(offset, true);
+ if (mapOffset < 0) {
+ var annotationModel = view.annotationModel;
+ var iter = annotationModel.getAnnotations(offset, offset + 1);
+ while (iter.hasNext()) {
+ var annotation = iter.next();
+ if (annotation.type === "orion.annotation.folding") {
+ annotation.expand();
+ mapOffset = model.mapOffset(offset, true);
+ break;
+ }
+ }
+ }
+ if (mapOffset < 0) { return; }
+ offset = mapOffset;
+ }
+ view.setText(text, offset, offset + previousText.length);
+ if (select) {
+ view.setSelection(offset, offset + text.length);
+ }
+ }
+ };
+
+ /**
+ * Constructs a new CompoundChange object.
+ *
+ * @class
+ * @name orion.textview.CompoundChange
+ * @private
+ */
+ function CompoundChange () {
+ this.changes = [];
+ }
+ CompoundChange.prototype = {
+ /** @ignore */
+ add: function (change) {
+ this.changes.push(change);
+ },
+ /** @ignore */
+ end: function (view) {
+ this.endSelection = view.getSelection();
+ this.endCaret = view.getCaretOffset();
+ },
+ /** @ignore */
+ undo: function (view, select) {
+ for (var i=this.changes.length - 1; i >= 0; i--) {
+ this.changes[i].undo(view, false);
+ }
+ if (select) {
+ var start = this.startSelection.start;
+ var end = this.startSelection.end;
+ view.setSelection(this.startCaret ? start : end, this.startCaret ? end : start);
+ }
+ },
+ /** @ignore */
+ redo: function (view, select) {
+ for (var i = 0; i < this.changes.length; i++) {
+ this.changes[i].redo(view, false);
+ }
+ if (select) {
+ var start = this.endSelection.start;
+ var end = this.endSelection.end;
+ view.setSelection(this.endCaret ? start : end, this.endCaret ? end : start);
+ }
+ },
+ /** @ignore */
+ start: function (view) {
+ this.startSelection = view.getSelection();
+ this.startCaret = view.getCaretOffset();
+ }
+ };
+
+ /**
+ * Constructs a new UndoStack on a text view.
+ *
+ * @param {orion.textview.TextView} view the text view for the undo stack.
+ * @param {Number} [size=100] the size for the undo stack.
+ *
+ * @name orion.textview.UndoStack
+ * @class The UndoStack is used to record the history of a text model associated to an view. Every
+ * change to the model is added to stack, allowing the application to undo and redo these changes.
+ *
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * </p>
+ */
+ function UndoStack (view, size) {
+ this.view = view;
+ this.size = size !== undefined ? size : 100;
+ this.reset();
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model = model.getBaseModel();
+ }
+ this.model = model;
+ var self = this;
+ this._listener = {
+ onChanging: function(e) {
+ self._onChanging(e);
+ },
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ }
+ };
+ model.addEventListener("Changing", this._listener.onChanging);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ }
+ UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ {
+ /**
+ * Adds a change to the stack.
+ *
+ * @param change the change to add.
+ * @param {Number} change.offset the offset of the change
+ * @param {String} change.text the new text of the change
+ * @param {String} change.previousText the previous text of the change
+ */
+ add: function (change) {
+ if (this.compoundChange) {
+ this.compoundChange.add(change);
+ } else {
+ var length = this.stack.length;
+ this.stack.splice(this.index, length-this.index, change);
+ this.index++;
+ if (this.stack.length > this.size) {
+ this.stack.shift();
+ this.index--;
+ this.cleanIndex--;
+ }
+ }
+ },
+ /**
+ * Marks the current state of the stack as clean.
+ *
+ * <p>
+ * This function is typically called when the content of view associated with the stack is saved.
+ * </p>
+ *
+ * @see #isClean
+ */
+ markClean: function() {
+ this.endCompoundChange();
+ this._commitUndo();
+ this.cleanIndex = this.index;
+ },
+ /**
+ * Returns true if current state of stack is the same
+ * as the state when markClean() was called.
+ *
+ * <p>
+ * For example, the application calls markClean(), then calls undo() four times and redo() four times.
+ * At this point isClean() returns true.
+ * </p>
+ * <p>
+ * This function is typically called to determine if the content of the view associated with the stack
+ * has changed since the last time it was saved.
+ * </p>
+ *
+ * @return {Boolean} returns if the state is the same as the state when markClean() was called.
+ *
+ * @see #markClean
+ */
+ isClean: function() {
+ return this.cleanIndex === this.getSize().undo;
+ },
+ /**
+ * Returns true if there is at least one change to undo.
+ *
+ * @return {Boolean} returns true if there is at least one change to undo.
+ *
+ * @see #canRedo
+ * @see #undo
+ */
+ canUndo: function() {
+ return this.getSize().undo > 0;
+ },
+ /**
+ * Returns true if there is at least one change to redo.
+ *
+ * @return {Boolean} returns true if there is at least one change to redo.
+ *
+ * @see #canUndo
+ * @see #redo
+ */
+ canRedo: function() {
+ return this.getSize().redo > 0;
+ },
+ /**
+ * Finishes a compound change.
+ *
+ * @see #startCompoundChange
+ */
+ endCompoundChange: function() {
+ if (this.compoundChange) {
+ this.compoundChange.end(this.view);
+ }
+ this.compoundChange = undefined;
+ },
+ /**
+ * Returns the sizes of the stack.
+ *
+ * @return {object} a object where object.undo is the number of changes that can be un-done,
+ * and object.redo is the number of changes that can be re-done.
+ *
+ * @see #canUndo
+ * @see #canRedo
+ */
+ getSize: function() {
+ var index = this.index;
+ var length = this.stack.length;
+ if (this._undoStart !== undefined) {
+ index++;
+ }
+ return {undo: index, redo: (length - index)};
+ },
+ /**
+ * Undo the last change in the stack.
+ *
+ * @return {Boolean} returns true if a change was un-done.
+ *
+ * @see #redo
+ * @see #canUndo
+ */
+ undo: function() {
+ this._commitUndo();
+ if (this.index <= 0) {
+ return false;
+ }
+ var change = this.stack[--this.index];
+ this._ignoreUndo = true;
+ change.undo(this.view, true);
+ this._ignoreUndo = false;
+ return true;
+ },
+ /**
+ * Redo the last change in the stack.
+ *
+ * @return {Boolean} returns true if a change was re-done.
+ *
+ * @see #undo
+ * @see #canRedo
+ */
+ redo: function() {
+ this._commitUndo();
+ if (this.index >= this.stack.length) {
+ return false;
+ }
+ var change = this.stack[this.index++];
+ this._ignoreUndo = true;
+ change.redo(this.view, true);
+ this._ignoreUndo = false;
+ return true;
+ },
+ /**
+ * Reset the stack to its original state. All changes in the stack are thrown away.
+ */
+ reset: function() {
+ this.index = this.cleanIndex = 0;
+ this.stack = [];
+ this._undoStart = undefined;
+ this._undoText = "";
+ this._undoType = 0;
+ this._ignoreUndo = false;
+ this._compoundChange = undefined;
+ },
+ /**
+ * Starts a compound change.
+ * <p>
+ * All changes added to stack from the time startCompoundChange() is called
+ * to the time that endCompoundChange() is called are compound on one change that can be un-done or re-done
+ * with one single call to undo() or redo().
+ * </p>
+ *
+ * @see #endCompoundChange
+ */
+ startCompoundChange: function() {
+ this._commitUndo();
+ var change = new CompoundChange();
+ this.add(change);
+ this.compoundChange = change;
+ this.compoundChange.start(this.view);
+ },
+ _commitUndo: function () {
+ if (this._undoStart !== undefined) {
+ if (this._undoType === -1) {
+ this.add(new Change(this._undoStart, "", this._undoText, ""));
+ } else {
+ this.add(new Change(this._undoStart, this._undoText, ""));
+ }
+ this._undoStart = undefined;
+ this._undoText = "";
+ this._undoType = 0;
+ }
+ },
+ _onDestroy: function(evt) {
+ this.model.removeEventListener("Changing", this._listener.onChanging);
+ this.view.removeEventListener("Destroy", this._listener.onDestroy);
+ },
+ _onChanging: function(e) {
+ var newText = e.text;
+ var start = e.start;
+ var removedCharCount = e.removedCharCount;
+ var addedCharCount = e.addedCharCount;
+ if (this._ignoreUndo) {
+ return;
+ }
+ if (this._undoStart !== undefined &&
+ !((addedCharCount === 1 && removedCharCount === 0 && this._undoType === 1 && start === this._undoStart + this._undoText.length) ||
+ (addedCharCount === 0 && removedCharCount === 1 && this._undoType === -1 && (((start + 1) === this._undoStart) || (start === this._undoStart)))))
+ {
+ this._commitUndo();
+ }
+ if (!this.compoundChange) {
+ if (addedCharCount === 1 && removedCharCount === 0) {
+ if (this._undoStart === undefined) {
+ this._undoStart = start;
+ }
+ this._undoText = this._undoText + newText;
+ this._undoType = 1;
+ return;
+ } else if (addedCharCount === 0 && removedCharCount === 1) {
+ var deleting = this._undoText.length > 0 && this._undoStart === start;
+ this._undoStart = start;
+ this._undoType = -1;
+ if (deleting) {
+ this._undoText = this._undoText + this.model.getText(start, start + removedCharCount);
+ } else {
+ this._undoText = this.model.getText(start, start + removedCharCount) + this._undoText;
+ }
+ return;
+ }
+ }
+ this.add(new Change(start, newText, this.model.getText(start, start + removedCharCount)));
+ }
+ };
+
+ return {
+ UndoStack: UndoStack
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define window*/
+
+define("orion/textview/textModel", ['orion/textview/eventTarget'], function(mEventTarget) {
+ var isWindows = window.navigator.platform.indexOf("Win") !== -1;
+
+ /**
+ * Constructs a new TextModel with the given text and default line delimiter.
+ *
+ * @param {String} [text=""] the text that the model will store
+ * @param {String} [lineDelimiter=platform delimiter] the line delimiter used when inserting new lines to the model.
+ *
+ * @name orion.textview.TextModel
+ * @class The TextModel is an interface that provides text for the view. Applications may
+ * implement the TextModel interface to provide a custom store for the view content. The
+ * view interacts with its text model in order to access and update the text that is being
+ * displayed and edited in the view. This is the default implementation.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#setModel}
+ * </p>
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function TextModel(text, lineDelimiter) {
+ this._lastLineIndex = -1;
+ this._text = [""];
+ this._lineOffsets = [0];
+ this.setText(text);
+ this.setLineDelimiter(lineDelimiter);
+ }
+
+ TextModel.prototype = /** @lends orion.textview.TextModel.prototype */ {
+ /**
+ * Returns the number of characters in the model.
+ *
+ * @returns {Number} the number of characters in the model.
+ */
+ getCharCount: function() {
+ var count = 0;
+ for (var i = 0; i<this._text.length; i++) {
+ count += this._text[i].length;
+ }
+ return count;
+ },
+ /**
+ * Returns the text of the line at the given index.
+ * <p>
+ * The valid indices are 0 to line count exclusive. Returns <code>null</code>
+ * if the index is out of range.
+ * </p>
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
+ * @returns {String} the line text or <code>null</code> if out of range.
+ *
+ * @see #getLineAtOffset
+ */
+ getLine: function(lineIndex, includeDelimiter) {
+ var lineCount = this.getLineCount();
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ return null;
+ }
+ var start = this._lineOffsets[lineIndex];
+ if (lineIndex + 1 < lineCount) {
+ var text = this.getText(start, this._lineOffsets[lineIndex + 1]);
+ if (includeDelimiter) {
+ return text;
+ }
+ var end = text.length, c;
+ while (((c = text.charCodeAt(end - 1)) === 10) || (c === 13)) {
+ end--;
+ }
+ return text.substring(0, end);
+ } else {
+ return this.getText(start);
+ }
+ },
+ /**
+ * Returns the line index at the given character offset.
+ * <p>
+ * The valid offsets are 0 to char count inclusive. The line index for
+ * char count is <code>line count - 1</code>. Returns <code>-1</code> if
+ * the offset is out of range.
+ * </p>
+ *
+ * @param {Number} offset a character offset.
+ * @returns {Number} the zero based line index or <code>-1</code> if out of range.
+ */
+ getLineAtOffset: function(offset) {
+ var charCount = this.getCharCount();
+ if (!(0 <= offset && offset <= charCount)) {
+ return -1;
+ }
+ var lineCount = this.getLineCount();
+ if (offset === charCount) {
+ return lineCount - 1;
+ }
+ var lineStart, lineEnd;
+ var index = this._lastLineIndex;
+ if (0 <= index && index < lineCount) {
+ lineStart = this._lineOffsets[index];
+ lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
+ if (lineStart <= offset && offset < lineEnd) {
+ return index;
+ }
+ }
+ var high = lineCount;
+ var low = -1;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ lineStart = this._lineOffsets[index];
+ lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
+ if (offset <= lineStart) {
+ high = index;
+ } else if (offset < lineEnd) {
+ high = index;
+ break;
+ } else {
+ low = index;
+ }
+ }
+ this._lastLineIndex = high;
+ return high;
+ },
+ /**
+ * Returns the number of lines in the model.
+ * <p>
+ * The model always has at least one line.
+ * </p>
+ *
+ * @returns {Number} the number of lines.
+ */
+ getLineCount: function() {
+ return this._lineOffsets.length;
+ },
+ /**
+ * Returns the line delimiter that is used by the view
+ * when inserting new lines. New lines entered using key strokes
+ * and paste operations use this line delimiter.
+ *
+ * @return {String} the line delimiter that is used by the view when inserting new lines.
+ */
+ getLineDelimiter: function() {
+ return this._lineDelimiter;
+ },
+ /**
+ * Returns the end character offset for the given line.
+ * <p>
+ * The end offset is not inclusive. This means that when the line delimiter is included, the
+ * offset is either the start offset of the next line or char count. When the line delimiter is
+ * not included, the offset is the offset of the line delimiter.
+ * </p>
+ * <p>
+ * The valid indices are 0 to line count exclusive. Returns <code>-1</code>
+ * if the index is out of range.
+ * </p>
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
+ * @return {Number} the line end offset or <code>-1</code> if out of range.
+ *
+ * @see #getLineStart
+ */
+ getLineEnd: function(lineIndex, includeDelimiter) {
+ var lineCount = this.getLineCount();
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ return -1;
+ }
+ if (lineIndex + 1 < lineCount) {
+ var end = this._lineOffsets[lineIndex + 1];
+ if (includeDelimiter) {
+ return end;
+ }
+ var text = this.getText(Math.max(this._lineOffsets[lineIndex], end - 2), end);
+ var i = text.length, c;
+ while (((c = text.charCodeAt(i - 1)) === 10) || (c === 13)) {
+ i--;
+ }
+ return end - (text.length - i);
+ } else {
+ return this.getCharCount();
+ }
+ },
+ /**
+ * Returns the start character offset for the given line.
+ * <p>
+ * The valid indices are 0 to line count exclusive. Returns <code>-1</code>
+ * if the index is out of range.
+ * </p>
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @return {Number} the line start offset or <code>-1</code> if out of range.
+ *
+ * @see #getLineEnd
+ */
+ getLineStart: function(lineIndex) {
+ if (!(0 <= lineIndex && lineIndex < this.getLineCount())) {
+ return -1;
+ }
+ return this._lineOffsets[lineIndex];
+ },
+ /**
+ * Returns the text for the given range.
+ * <p>
+ * The end offset is not inclusive. This means that character at the end offset
+ * is not included in the returned text.
+ * </p>
+ *
+ * @param {Number} [start=0] the zero based start offset of text range.
+ * @param {Number} [end=char count] the zero based end offset of text range.
+ *
+ * @see #setText
+ */
+ getText: function(start, end) {
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this.getCharCount(); }
+ if (start === end) { return ""; }
+ var offset = 0, chunk = 0, length;
+ while (chunk<this._text.length) {
+ length = this._text[chunk].length;
+ if (start <= offset + length) { break; }
+ offset += length;
+ chunk++;
+ }
+ var firstOffset = offset;
+ var firstChunk = chunk;
+ while (chunk<this._text.length) {
+ length = this._text[chunk].length;
+ if (end <= offset + length) { break; }
+ offset += length;
+ chunk++;
+ }
+ var lastOffset = offset;
+ var lastChunk = chunk;
+ if (firstChunk === lastChunk) {
+ return this._text[firstChunk].substring(start - firstOffset, end - lastOffset);
+ }
+ var beforeText = this._text[firstChunk].substring(start - firstOffset);
+ var afterText = this._text[lastChunk].substring(0, end - lastOffset);
+ return beforeText + this._text.slice(firstChunk+1, lastChunk).join("") + afterText;
+ },
+ /**
+ * Notifies all listeners that the text is about to change.
+ * <p>
+ * This notification is intended to be used only by the view. Application clients should
+ * use {@link orion.textview.TextView#event:onModelChanging}.
+ * </p>
+ * <p>
+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
+ * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
+ * purposes and to allow integration with other toolkit frameworks.
+ * </p>
+ *
+ * @param {orion.textview.ModelChangingEvent} modelChangingEvent the changing event
+ */
+ onChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * Notifies all listeners that the text has changed.
+ * <p>
+ * This notification is intended to be used only by the view. Application clients should
+ * use {@link orion.textview.TextView#event:onModelChanged}.
+ * </p>
+ * <p>
+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
+ * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
+ * purposes and to allow integration with other toolkit frameworks.
+ * </p>
+ *
+ * @param {orion.textview.ModelChangedEvent} modelChangedEvent the changed event
+ */
+ onChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * Sets the line delimiter that is used by the view
+ * when new lines are inserted in the model due to key
+ * strokes and paste operations.
+ * <p>
+ * If lineDelimiter is "auto", the delimiter is computed to be
+ * the first delimiter found the in the current text. If lineDelimiter
+ * is undefined or if there are no delimiters in the current text, the
+ * platform delimiter is used.
+ * </p>
+ *
+ * @param {String} lineDelimiter the line delimiter that is used by the view when inserting new lines.
+ */
+ setLineDelimiter: function(lineDelimiter) {
+ if (lineDelimiter === "auto") {
+ lineDelimiter = undefined;
+ if (this.getLineCount() > 1) {
+ lineDelimiter = this.getText(this.getLineEnd(0), this.getLineEnd(0, true));
+ }
+ }
+ this._lineDelimiter = lineDelimiter ? lineDelimiter : (isWindows ? "\r\n" : "\n");
+ },
+ /**
+ * Replaces the text in the given range with the given text.
+ * <p>
+ * The end offset is not inclusive. This means that the character at the
+ * end offset is not replaced.
+ * </p>
+ * <p>
+ * The text model must notify the listeners before and after the
+ * the text is changed by calling {@link #onChanging} and {@link #onChanged}
+ * respectively.
+ * </p>
+ *
+ * @param {String} [text=""] the new text.
+ * @param {Number} [start=0] the zero based start offset of text range.
+ * @param {Number} [end=char count] the zero based end offset of text range.
+ *
+ * @see #getText
+ */
+ setText: function(text, start, end) {
+ if (text === undefined) { text = ""; }
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this.getCharCount(); }
+ if (start === end && text === "") { return; }
+ var startLine = this.getLineAtOffset(start);
+ var endLine = this.getLineAtOffset(end);
+ var eventStart = start;
+ var removedCharCount = end - start;
+ var removedLineCount = endLine - startLine;
+ var addedCharCount = text.length;
+ var addedLineCount = 0;
+ var lineCount = this.getLineCount();
+
+ var cr = 0, lf = 0, index = 0;
+ var newLineOffsets = [];
+ while (true) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ if (lf === -1 && cr === -1) { break; }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ index = lf + 1;
+ } else {
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ index = cr + 1;
+ } else {
+ index = lf + 1;
+ }
+ newLineOffsets.push(start + index);
+ addedLineCount++;
+ }
+
+ var modelChangingEvent = {
+ type: "Changing",
+ text: text,
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+
+ //TODO this should be done the loops below to avoid getText()
+ if (newLineOffsets.length === 0) {
+ var startLineOffset = this.getLineStart(startLine), endLineOffset;
+ if (endLine + 1 < lineCount) {
+ endLineOffset = this.getLineStart(endLine + 1);
+ } else {
+ endLineOffset = this.getCharCount();
+ }
+ if (start !== startLineOffset) {
+ text = this.getText(startLineOffset, start) + text;
+ start = startLineOffset;
+ }
+ if (end !== endLineOffset) {
+ text = text + this.getText(end, endLineOffset);
+ end = endLineOffset;
+ }
+ }
+
+ var changeCount = addedCharCount - removedCharCount;
+ for (var j = startLine + removedLineCount + 1; j < lineCount; j++) {
+ this._lineOffsets[j] += changeCount;
+ }
+ var args = [startLine + 1, removedLineCount].concat(newLineOffsets);
+ Array.prototype.splice.apply(this._lineOffsets, args);
+
+ var offset = 0, chunk = 0, length;
+ while (chunk<this._text.length) {
+ length = this._text[chunk].length;
+ if (start <= offset + length) { break; }
+ offset += length;
+ chunk++;
+ }
+ var firstOffset = offset;
+ var firstChunk = chunk;
+ while (chunk<this._text.length) {
+ length = this._text[chunk].length;
+ if (end <= offset + length) { break; }
+ offset += length;
+ chunk++;
+ }
+ var lastOffset = offset;
+ var lastChunk = chunk;
+ var firstText = this._text[firstChunk];
+ var lastText = this._text[lastChunk];
+ var beforeText = firstText.substring(0, start - firstOffset);
+ var afterText = lastText.substring(end - lastOffset);
+ var params = [firstChunk, lastChunk - firstChunk + 1];
+ if (beforeText) { params.push(beforeText); }
+ if (text) { params.push(text); }
+ if (afterText) { params.push(afterText); }
+ Array.prototype.splice.apply(this._text, params);
+ if (this._text.length === 0) { this._text = [""]; }
+
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ }
+ };
+ mEventTarget.EventTarget.addMixin(TextModel.prototype);
+
+ return {TextModel: TextModel};
+});/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/projectionTextModel", ['orion/textview/textModel', 'orion/textview/eventTarget'], function(mTextModel, mEventTarget) {
+
+ /**
+ * @class This object represents a projection range. A projection specifies a
+ * range of text and the replacement text. The range of text is relative to the
+ * base text model associated to a projection model.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.ProjectionTextModel}<br/>
+ * {@link orion.textview.ProjectionTextModel#addProjection}<br/>
+ * </p>
+ * @name orion.textview.Projection
+ *
+ * @property {Number} start The start offset of the projection range.
+ * @property {Number} end The end offset of the projection range. This offset is exclusive.
+ * @property {String|orion.textview.TextModel} [text=""] The projection text to be inserted
+ */
+ /**
+ * Constructs a new <code>ProjectionTextModel</code> based on the specified <code>TextModel</code>.
+ *
+ * @param {orion.textview.TextModel} baseModel The base text model.
+ *
+ * @name orion.textview.ProjectionTextModel
+ * @class The <code>ProjectionTextModel</code> represents a projection of its base text
+ * model. Projection ranges can be added to the projection text model to hide and/or insert
+ * ranges to the base text model.
+ * <p>
+ * The contents of the projection text model is modified when changes occur in the base model,
+ * projection model or by calls to {@link #addProjection} and {@link #removeProjection}.
+ * </p>
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextModel}
+ * {@link orion.textview.TextView#setModel}
+ * </p>
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function ProjectionTextModel(baseModel) {
+ this._model = baseModel; /* Base Model */
+ this._projections = [];
+ }
+
+ ProjectionTextModel.prototype = /** @lends orion.textview.ProjectionTextModel.prototype */ {
+ /**
+ * Adds a projection range to the model.
+ * <p>
+ * The model must notify the listeners before and after the the text is
+ * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
+ * </p>
+ * @param {orion.textview.Projection} projection The projection range to be added.
+ *
+ * @see #removeProjection
+ */
+ addProjection: function(projection) {
+ if (!projection) {return;}
+ //start and end can't overlap any exist projection
+ var model = this._model, projections = this._projections;
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+ projection._lineCount = model.getLineAtOffset(projection.end) - projection._lineIndex;
+ var text = projection.text;
+ if (!text) { text = ""; }
+ if (typeof text === "string") {
+ projection._model = new mTextModel.TextModel(text, model.getLineDelimiter());
+ } else {
+ projection._model = text;
+ }
+ var eventStart = this.mapOffset(projection.start, true);
+ var removedCharCount = projection.end - projection.start;
+ var removedLineCount = projection._lineCount;
+ var addedCharCount = projection._model.getCharCount();
+ var addedLineCount = projection._model.getLineCount() - 1;
+ var modelChangingEvent = {
+ type: "Changing",
+ text: projection._model.getText(),
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+ var index = this._binarySearch(projections, projection.start);
+ projections.splice(index, 0, projection);
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ },
+ /**
+ * Returns all projection ranges of this model.
+ *
+ * @return {orion.textview.Projection[]} The projection ranges.
+ *
+ * @see #addProjection
+ */
+ getProjections: function() {
+ return this._projections.slice(0);
+ },
+ /**
+ * Gets the base text model.
+ *
+ * @return {orion.textview.TextModel} The base text model.
+ */
+ getBaseModel: function() {
+ return this._model;
+ },
+ /**
+ * Maps offsets between the projection model and its base model.
+ *
+ * @param {Number} offset The offset to be mapped.
+ * @param {Boolean} [baseOffset=false] <code>true</code> if <code>offset</code> is in base model and
+ * should be mapped to the projection model.
+ * @return {Number} The mapped offset
+ */
+ mapOffset: function(offset, baseOffset) {
+ var projections = this._projections, delta = 0, i, projection;
+ if (baseOffset) {
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > offset) { break; }
+ if (projection.end > offset) { return -1; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ return offset + delta;
+ }
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > offset - delta) { break; }
+ var charCount = projection._model.getCharCount();
+ if (projection.start + charCount > offset - delta) {
+ return -1;
+ }
+ delta += charCount - (projection.end - projection.start);
+ }
+ return offset - delta;
+ },
+ /**
+ * Removes a projection range from the model.
+ * <p>
+ * The model must notify the listeners before and after the the text is
+ * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
+ * </p>
+ *
+ * @param {orion.textview.Projection} projection The projection range to be removed.
+ *
+ * @see #addProjection
+ */
+ removeProjection: function(projection) {
+ //TODO remove listeners from model
+ var i, delta = 0;
+ for (i = 0; i < this._projections.length; i++) {
+ var p = this._projections[i];
+ if (p === projection) {
+ projection = p;
+ break;
+ }
+ delta += p._model.getCharCount() - (p.end - p.start);
+ }
+ if (i < this._projections.length) {
+ var model = this._model;
+ var eventStart = projection.start + delta;
+ var addedCharCount = projection.end - projection.start;
+ var addedLineCount = projection._lineCount;
+ var removedCharCount = projection._model.getCharCount();
+ var removedLineCount = projection._model.getLineCount() - 1;
+ var modelChangingEvent = {
+ type: "Changing",
+ text: model.getText(projection.start, projection.end),
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+ this._projections.splice(i, 1);
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ }
+ },
+ /** @ignore */
+ _binarySearch: function (array, offset) {
+ var high = array.length, low = -1, index;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ /**
+ * @see orion.textview.TextModel#getCharCount
+ */
+ getCharCount: function() {
+ var count = this._model.getCharCount(), projections = this._projections;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ count += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ return count;
+ },
+ /**
+ * @see orion.textview.TextModel#getLine
+ */
+ getLine: function(lineIndex, includeDelimiter) {
+ if (lineIndex < 0) { return null; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, result = [], offset = 0, i, lineCount, projection;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection._lineIndex >= lineIndex - delta) { break; }
+ lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount >= lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ if (projectionLineIndex < lineCount) {
+ return projection._model.getLine(projectionLineIndex, includeDelimiter);
+ } else {
+ result.push(projection._model.getLine(lineCount));
+ }
+ }
+ offset = projection.end;
+ delta += lineCount - projection._lineCount;
+ }
+ offset = Math.max(offset, model.getLineStart(lineIndex - delta));
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection._lineIndex > lineIndex - delta) { break; }
+ result.push(model.getText(offset, projection.start));
+ lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount > lineIndex - delta) {
+ result.push(projection._model.getLine(0, includeDelimiter));
+ return result.join("");
+ }
+ result.push(projection._model.getText());
+ offset = projection.end;
+ delta += lineCount - projection._lineCount;
+ }
+ var end = model.getLineEnd(lineIndex - delta, includeDelimiter);
+ if (offset < end) {
+ result.push(model.getText(offset, end));
+ }
+ return result.join("");
+ },
+ /**
+ * @see orion.textview.TextModel#getLineAtOffset
+ */
+ getLineAtOffset: function(offset) {
+ var model = this._model, projections = this._projections;
+ var delta = 0, lineDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection.start > offset - delta) { break; }
+ var charCount = projection._model.getCharCount();
+ if (projection.start + charCount > offset - delta) {
+ var projectionOffset = offset - (projection.start + delta);
+ lineDelta += projection._model.getLineAtOffset(projectionOffset);
+ delta += projectionOffset;
+ break;
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ return model.getLineAtOffset(offset - delta) + lineDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineCount
+ */
+ getLineCount: function() {
+ var model = this._model, projections = this._projections;
+ var count = model.getLineCount();
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ count += projection._model.getLineCount() - 1 - projection._lineCount;
+ }
+ return count;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineDelimiter
+ */
+ getLineDelimiter: function() {
+ return this._model.getLineDelimiter();
+ },
+ /**
+ * @see orion.textview.TextModel#getLineEnd
+ */
+ getLineEnd: function(lineIndex, includeDelimiter) {
+ if (lineIndex < 0) { return -1; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, offsetDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection._lineIndex > lineIndex - delta) { break; }
+ var lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount > lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ return projection._model.getLineEnd (projectionLineIndex, includeDelimiter) + projection.start + offsetDelta;
+ }
+ offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
+ delta += lineCount - projection._lineCount;
+ }
+ return model.getLineEnd(lineIndex - delta, includeDelimiter) + offsetDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineStart
+ */
+ getLineStart: function(lineIndex) {
+ if (lineIndex < 0) { return -1; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, offsetDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection._lineIndex >= lineIndex - delta) { break; }
+ var lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount >= lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ return projection._model.getLineStart (projectionLineIndex) + projection.start + offsetDelta;
+ }
+ offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
+ delta += lineCount - projection._lineCount;
+ }
+ return model.getLineStart(lineIndex - delta) + offsetDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getText
+ */
+ getText: function(start, end) {
+ if (start === undefined) { start = 0; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, result = [], i, projection, charCount;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > start - delta) {
+ if (end !== undefined && projection.start + charCount > end - delta) {
+ return projection._model.getText(start - (projection.start + delta), end - (projection.start + delta));
+ } else {
+ result.push(projection._model.getText(start - (projection.start + delta)));
+ start = projection.end + delta + charCount - (projection.end - projection.start);
+ }
+ }
+ delta += charCount - (projection.end - projection.start);
+ }
+ var offset = start - delta;
+ if (end !== undefined) {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end - delta) { break; }
+ result.push(model.getText(offset, projection.start));
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > end - delta) {
+ result.push(projection._model.getText(0, end - (projection.start + delta)));
+ return result.join("");
+ }
+ result.push(projection._model.getText());
+ offset = projection.end;
+ delta += charCount - (projection.end - projection.start);
+ }
+ result.push(model.getText(offset, end - delta));
+ } else {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ result.push(model.getText(offset, projection.start));
+ result.push(projection._model.getText());
+ offset = projection.end;
+ }
+ result.push(model.getText(offset));
+ }
+ return result.join("");
+ },
+ /** @ignore */
+ _onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
+ var model = this._model, projections = this._projections, i, projection, delta = 0, lineDelta;
+ var end = start + removedCharCount;
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start) { break; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ /*TODO add stuff saved by setText*/
+ var mapStart = start + delta, rangeStart = i;
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end) { break; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ }
+ /*TODO add stuff saved by setText*/
+ var mapEnd = end + delta, rangeEnd = i;
+ this.onChanging(mapStart, mapEnd - mapStart, addedCharCount/*TODO add stuff saved by setText*/, removedLineCount + lineDelta/*TODO add stuff saved by setText*/, addedLineCount/*TODO add stuff saved by setText*/);
+ projections.splice(projections, rangeEnd - rangeStart);
+ var count = text.length - (mapEnd - mapStart);
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ projection.start += count;
+ projection.end += count;
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+ }
+ },
+ /**
+ * @see orion.textview.TextModel#onChanging
+ */
+ onChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * @see orion.textview.TextModel#onChanged
+ */
+ onChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * @see orion.textview.TextModel#setLineDelimiter
+ */
+ setLineDelimiter: function(lineDelimiter) {
+ this._model.setLineDelimiter(lineDelimiter);
+ },
+ /**
+ * @see orion.textview.TextModel#setText
+ */
+ setText: function(text, start, end) {
+ if (text === undefined) { text = ""; }
+ if (start === undefined) { start = 0; }
+ var eventStart = start, eventEnd = end;
+ var model = this._model, projections = this._projections;
+ var delta = 0, lineDelta = 0, i, projection, charCount, startProjection, endProjection, startLineDelta = 0;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > start - delta) {
+ if (end !== undefined && projection.start + charCount > end - delta) {
+ projection._model.setText(text, start - (projection.start + delta), end - (projection.start + delta));
+ //TODO events - special case
+ return;
+ } else {
+ startLineDelta = projection._model.getLineCount() - 1 - projection._model.getLineAtOffset(start - (projection.start + delta));
+ startProjection = {
+ projection: projection,
+ start: start - (projection.start + delta)
+ };
+ start = projection.end + delta + charCount - (projection.end - projection.start);
+ }
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ var mapStart = start - delta, rangeStart = i, startLine = model.getLineAtOffset(mapStart) + lineDelta - startLineDelta;
+ if (end !== undefined) {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > end - delta) {
+ lineDelta += projection._model.getLineAtOffset(end - (projection.start + delta));
+ charCount = end - (projection.start + delta);
+ end = projection.end + delta;
+ endProjection = {
+ projection: projection,
+ end: charCount
+ };
+ break;
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ } else {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ end = eventEnd = model.getCharCount() + delta;
+ }
+ var mapEnd = end - delta, rangeEnd = i, endLine = model.getLineAtOffset(mapEnd) + lineDelta;
+
+ //events
+ var removedCharCount = eventEnd - eventStart;
+ var removedLineCount = endLine - startLine;
+ var addedCharCount = text.length;
+ var addedLineCount = 0;
+ var cr = 0, lf = 0, index = 0;
+ while (true) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ if (lf === -1 && cr === -1) { break; }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ index = lf + 1;
+ } else {
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ index = cr + 1;
+ } else {
+ index = lf + 1;
+ }
+ addedLineCount++;
+ }
+
+ var modelChangingEvent = {
+ type: "Changing",
+ text: text,
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+
+// var changeLineCount = model.getLineAtOffset(mapEnd) - model.getLineAtOffset(mapStart) + addedLineCount;
+ model.setText(text, mapStart, mapEnd);
+ if (startProjection) {
+ projection = startProjection.projection;
+ projection._model.setText("", startProjection.start);
+ }
+ if (endProjection) {
+ projection = endProjection.projection;
+ projection._model.setText("", 0, endProjection.end);
+ projection.start = projection.end;
+ projection._lineCount = 0;
+ }
+ projections.splice(rangeStart, rangeEnd - rangeStart);
+ var changeCount = text.length - (mapEnd - mapStart);
+ for (i = rangeEnd; i < projections.length; i++) {
+ projection = projections[i];
+ projection.start += changeCount;
+ projection.end += changeCount;
+// if (projection._lineIndex + changeLineCount !== model.getLineAtOffset(projection.start)) {
+// log("here");
+// }
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+// projection._lineIndex += changeLineCount;
+ }
+
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ }
+ };
+ mEventTarget.EventTarget.addMixin(ProjectionTextModel.prototype);
+
+ return {ProjectionTextModel: ProjectionTextModel};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define setTimeout clearTimeout setInterval clearInterval Node */
+
+define("orion/textview/tooltip", ['orion/textview/textView', 'orion/textview/textModel', 'orion/textview/projectionTextModel'], function(mTextView, mTextModel, mProjectionTextModel) {
+
+ /** @private */
+ function Tooltip (view) {
+ this._view = view;
+ //TODO add API to get the parent of the view
+ this._create(view._parent.ownerDocument);
+ view.addEventListener("Destroy", this, this.destroy);
+ }
+ Tooltip.getTooltip = function(view) {
+ if (!view._tooltip) {
+ view._tooltip = new Tooltip(view);
+ }
+ return view._tooltip;
+ };
+ Tooltip.prototype = /** @lends orion.textview.Tooltip.prototype */ {
+ _create: function(document) {
+ if (this._domNode) { return; }
+ this._document = document;
+ var domNode = this._domNode = document.createElement("DIV");
+ domNode.className = "viewTooltip";
+ var viewParent = this._viewParent = document.createElement("DIV");
+ domNode.appendChild(viewParent);
+ var htmlParent = this._htmlParent = document.createElement("DIV");
+ domNode.appendChild(htmlParent);
+ document.body.appendChild(domNode);
+ this.hide();
+ },
+ destroy: function() {
+ if (!this._domNode) { return; }
+ if (this._contentsView) {
+ this._contentsView.destroy();
+ this._contentsView = null;
+ this._emptyModel = null;
+ }
+ var parent = this._domNode.parentNode;
+ if (parent) { parent.removeChild(this._domNode); }
+ this._domNode = null;
+ },
+ hide: function() {
+ if (this._contentsView) {
+ this._contentsView.setModel(this._emptyModel);
+ }
+ if (this._viewParent) {
+ this._viewParent.style.left = "-10000px";
+ this._viewParent.style.position = "fixed";
+ this._viewParent.style.visibility = "hidden";
+ }
+ if (this._htmlParent) {
+ this._htmlParent.style.left = "-10000px";
+ this._htmlParent.style.position = "fixed";
+ this._htmlParent.style.visibility = "hidden";
+ this._htmlParent.innerHTML = "";
+ }
+ if (this._domNode) {
+ this._domNode.style.visibility = "hidden";
+ }
+ if (this._showTimeout) {
+ clearTimeout(this._showTimeout);
+ this._showTimeout = null;
+ }
+ if (this._hideTimeout) {
+ clearTimeout(this._hideTimeout);
+ this._hideTimeout = null;
+ }
+ if (this._fadeTimeout) {
+ clearInterval(this._fadeTimeout);
+ this._fadeTimeout = null;
+ }
+ },
+ isVisible: function() {
+ return this._domNode && this._domNode.style.visibility === "visible";
+ },
+ setTarget: function(target) {
+ if (this.target === target) { return; }
+ this._target = target;
+ this.hide();
+ if (target) {
+ var self = this;
+ self._showTimeout = setTimeout(function() {
+ self.show(true);
+ }, 1000);
+ }
+ },
+ show: function(autoHide) {
+ if (!this._target) { return; }
+ var info = this._target.getTooltipInfo();
+ if (!info) { return; }
+ var domNode = this._domNode;
+ domNode.style.left = domNode.style.right = domNode.style.width = domNode.style.height = "auto";
+ var contents = info.contents, contentsDiv;
+ if (contents instanceof Array) {
+ contents = this._getAnnotationContents(contents);
+ }
+ if (typeof contents === "string") {
+ (contentsDiv = this._htmlParent).innerHTML = contents;
+ } else if (contents instanceof Node) {
+ (contentsDiv = this._htmlParent).appendChild(contents);
+ } else if (contents instanceof mProjectionTextModel.ProjectionTextModel) {
+ if (!this._contentsView) {
+ this._emptyModel = new mTextModel.TextModel("");
+ //TODO need hook into setup.js (or editor.js) to create a text view (and styler)
+ var newView = this._contentsView = new mTextView.TextView({
+ model: this._emptyModel,
+ parent: this._viewParent,
+ tabSize: 4,
+ sync: true,
+ stylesheet: ["/orion/textview/tooltip.css", "/orion/textview/rulers.css",
+ "/examples/textview/textstyler.css", "/css/default-theme.css"]
+ });
+ //TODO this is need to avoid IE from getting focus
+ newView._clientDiv.contentEditable = false;
+ //TODO need to find a better way of sharing the styler for multiple views
+ var view = this._view;
+ var listener = {
+ onLineStyle: function(e) {
+ view.onLineStyle(e);
+ }
+ };
+ newView.addEventListener("LineStyle", listener.onLineStyle);
+ }
+ var contentsView = this._contentsView;
+ contentsView.setModel(contents);
+ var size = contentsView.computeSize();
+ contentsDiv = this._viewParent;
+ //TODO always make the width larger than the size of the scrollbar to avoid bug in updatePage
+ contentsDiv.style.width = (size.width + 20) + "px";
+ contentsDiv.style.height = size.height + "px";
+ } else {
+ return;
+ }
+ contentsDiv.style.left = "auto";
+ contentsDiv.style.position = "static";
+ contentsDiv.style.visibility = "visible";
+ var left = parseInt(this._getNodeStyle(domNode, "padding-left", "0"), 10);
+ left += parseInt(this._getNodeStyle(domNode, "border-left-width", "0"), 10);
+ if (info.anchor === "right") {
+ var right = parseInt(this._getNodeStyle(domNode, "padding-right", "0"), 10);
+ right += parseInt(this._getNodeStyle(domNode, "border-right-width", "0"), 10);
+ domNode.style.right = (domNode.ownerDocument.body.getBoundingClientRect().right - info.x + left + right) + "px";
+ } else {
+ domNode.style.left = (info.x - left) + "px";
+ }
+ var top = parseInt(this._getNodeStyle(domNode, "padding-top", "0"), 10);
+ top += parseInt(this._getNodeStyle(domNode, "border-top-width", "0"), 10);
+ domNode.style.top = (info.y - top) + "px";
+ domNode.style.maxWidth = info.maxWidth + "px";
+ domNode.style.maxHeight = info.maxHeight + "px";
+ domNode.style.opacity = "1";
+ domNode.style.visibility = "visible";
+ if (autoHide) {
+ var self = this;
+ self._hideTimeout = setTimeout(function() {
+ var opacity = parseFloat(self._getNodeStyle(domNode, "opacity", "1"));
+ self._fadeTimeout = setInterval(function() {
+ if (domNode.style.visibility === "visible" && opacity > 0) {
+ opacity -= 0.1;
+ domNode.style.opacity = opacity;
+ return;
+ }
+ self.hide();
+ }, 50);
+ }, 5000);
+ }
+ },
+ _getAnnotationContents: function(annotations) {
+ if (annotations.length === 0) {
+ return null;
+ }
+ var model = this._view.getModel(), annotation;
+ var baseModel = model.getBaseModel ? model.getBaseModel() : model;
+ function getText(start, end) {
+ var textStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
+ var textEnd = baseModel.getLineEnd(baseModel.getLineAtOffset(end), true);
+ return baseModel.getText(textStart, textEnd);
+ }
+ var title;
+ if (annotations.length === 1) {
+ annotation = annotations[0];
+ if (annotation.title) {
+ title = annotation.title.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ return "<div>" + annotation.html + "&nbsp;<span style='vertical-align:middle;'>" + title + "</span><div>";
+ } else {
+ var newModel = new mProjectionTextModel.ProjectionTextModel(baseModel);
+ var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(annotation.start));
+ newModel.addProjection({start: annotation.end, end: newModel.getCharCount()});
+ newModel.addProjection({start: 0, end: lineStart});
+ return newModel;
+ }
+ } else {
+ var tooltipHTML = "<div><em>Multiple annotations:</em></div>";
+ for (var i = 0; i < annotations.length; i++) {
+ annotation = annotations[i];
+ title = annotation.title;
+ if (!title) {
+ title = getText(annotation.start, annotation.end);
+ }
+ title = title.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ tooltipHTML += "<div>" + annotation.html + "&nbsp;<span style='vertical-align:middle;'>" + title + "</span><div>";
+ }
+ return tooltipHTML;
+ }
+ },
+ _getNodeStyle: function(node, prop, defaultValue) {
+ var value;
+ if (node) {
+ value = node.style[prop];
+ if (!value) {
+ if (node.currentStyle) {
+ var index = 0, p = prop;
+ while ((index = p.indexOf("-", index)) !== -1) {
+ p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2);
+ }
+ value = node.currentStyle[p];
+ } else {
+ var css = node.ownerDocument.defaultView.getComputedStyle(node, null);
+ value = css ? css.getPropertyValue(prop) : null;
+ }
+ }
+ }
+ return value || defaultValue;
+ }
+ };
+ return {Tooltip: Tooltip};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 Bug#360726 Bug#361180 Bug#362835 Bug#362428 Bug#362286 Bug#354270 Bug#361474 Bug#363945 Bug#366312 Bug#370584
+ ******************************************************************************/
+
+/*global window document navigator setTimeout clearTimeout XMLHttpRequest define DOMException */
+
+define("orion/textview/textView", ['orion/textview/textModel', 'orion/textview/keyBinding', 'orion/textview/eventTarget'], function(mTextModel, mKeyBinding, mEventTarget) {
+
+ /** @private */
+ function addHandler(node, type, handler, capture) {
+ if (typeof node.addEventListener === "function") {
+ node.addEventListener(type, handler, capture === true);
+ } else {
+ node.attachEvent("on" + type, handler);
+ }
+ }
+ /** @private */
+ function removeHandler(node, type, handler, capture) {
+ if (typeof node.removeEventListener === "function") {
+ node.removeEventListener(type, handler, capture === true);
+ } else {
+ node.detachEvent("on" + type, handler);
+ }
+ }
+ var userAgent = navigator.userAgent;
+ var isIE;
+ if (document.selection && window.ActiveXObject && /MSIE/.test(userAgent)) {
+ isIE = document.documentMode ? document.documentMode : 7;
+ }
+ var isFirefox = parseFloat(userAgent.split("Firefox/")[1] || userAgent.split("Minefield/")[1]) || undefined;
+ var isOpera = userAgent.indexOf("Opera") !== -1;
+ var isChrome = userAgent.indexOf("Chrome") !== -1;
+ var isSafari = userAgent.indexOf("Safari") !== -1 && !isChrome;
+ var isWebkit = userAgent.indexOf("WebKit") !== -1;
+ var isPad = userAgent.indexOf("iPad") !== -1;
+ var isMac = navigator.platform.indexOf("Mac") !== -1;
+ var isWindows = navigator.platform.indexOf("Win") !== -1;
+ var isLinux = navigator.platform.indexOf("Linux") !== -1;
+ var isW3CEvents = typeof window.document.documentElement.addEventListener === "function";
+ var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function";
+ var platformDelimiter = isWindows ? "\r\n" : "\n";
+
+ /**
+ * Constructs a new Selection object.
+ *
+ * @class A Selection represents a range of selected text in the view.
+ * @name orion.textview.Selection
+ */
+ function Selection (start, end, caret) {
+ /**
+ * The selection start offset.
+ *
+ * @name orion.textview.Selection#start
+ */
+ this.start = start;
+ /**
+ * The selection end offset.
+ *
+ * @name orion.textview.Selection#end
+ */
+ this.end = end;
+ /** @private */
+ this.caret = caret; //true if the start, false if the caret is at end
+ }
+ Selection.prototype = /** @lends orion.textview.Selection.prototype */ {
+ /** @private */
+ clone: function() {
+ return new Selection(this.start, this.end, this.caret);
+ },
+ /** @private */
+ collapse: function() {
+ if (this.caret) {
+ this.end = this.start;
+ } else {
+ this.start = this.end;
+ }
+ },
+ /** @private */
+ extend: function (offset) {
+ if (this.caret) {
+ this.start = offset;
+ } else {
+ this.end = offset;
+ }
+ if (this.start > this.end) {
+ var tmp = this.start;
+ this.start = this.end;
+ this.end = tmp;
+ this.caret = !this.caret;
+ }
+ },
+ /** @private */
+ setCaret: function(offset) {
+ this.start = offset;
+ this.end = offset;
+ this.caret = false;
+ },
+ /** @private */
+ getCaret: function() {
+ return this.caret ? this.start : this.end;
+ },
+ /** @private */
+ toString: function() {
+ return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end");
+ },
+ /** @private */
+ isEmpty: function() {
+ return this.start === this.end;
+ },
+ /** @private */
+ equals: function(object) {
+ return this.caret === object.caret && this.start === object.start && this.end === object.end;
+ }
+ };
+ /**
+ * @class This object describes the options for the text view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#setOptions}
+ * {@link orion.textview.TextView#getOptions}
+ * </p>
+ * @name orion.textview.TextViewOptions
+ *
+ * @property {String|DOMElement} parent the parent element for the view, it can be either a DOM element or an ID for a DOM element.
+ * @property {orion.textview.TextModel} [model] the text model for the view. If it is not set the view creates an empty {@link orion.textview.TextModel}.
+ * @property {Boolean} [readonly=false] whether or not the view is read-only.
+ * @property {Boolean} [fullSelection=true] whether or not the view is in full selection mode.
+ * @property {Boolean} [sync=false] whether or not the view creation should be synchronous (if possible).
+ * @property {Boolean} [expandTab=false] whether or not the tab key inserts white spaces.
+ * @property {String|String[]} [stylesheet] one or more stylesheet for the view. Each stylesheet can be either a URI or a string containing the CSS rules.
+ * @property {String} [themeClass] the CSS class for the view theming.
+ * @property {Number} [tabSize] The number of spaces in a tab.
+ */
+ /**
+ * Constructs a new text view.
+ *
+ * @param {orion.textview.TextViewOptions} options the view options.
+ *
+ * @class A TextView is a user interface for editing text.
+ * @name orion.textview.TextView
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function TextView (options) {
+ this._init(options);
+ }
+
+ TextView.prototype = /** @lends orion.textview.TextView.prototype */ {
+ /**
+ * Adds a ruler to the text view.
+ *
+ * @param {orion.textview.Ruler} ruler the ruler.
+ */
+ addRuler: function (ruler) {
+ this._rulers.push(ruler);
+ ruler.setView(this);
+ this._createRuler(ruler);
+ this._updatePage();
+ },
+ computeSize: function() {
+ var w = 0, h = 0;
+ var model = this._model, clientDiv = this._clientDiv;
+ if (!clientDiv) { return {width: w, height: h}; }
+ var clientWidth = clientDiv.style.width;
+ /*
+ * Feature in WekKit. Webkit limits the width of the lines
+ * computed below to the width of the client div. This causes
+ * the lines to be wrapped even though "pre" is set. The fix
+ * is to set the width of the client div to a larger number
+ * before computing the lines width. Note that this value is
+ * reset to the appropriate value further down.
+ */
+ if (isWebkit) {
+ clientDiv.style.width = (0x7FFFF).toString() + "px";
+ }
+ var lineCount = model.getLineCount();
+ var document = this._frameDocument;
+ for (var lineIndex=0; lineIndex<lineCount; lineIndex++) {
+ var child = this._getLineNode(lineIndex), dummy = null;
+ if (!child || child.lineChanged || child.lineRemoved) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var rect = this._getLineBoundingClientRect(child);
+ w = Math.max(w, rect.right - rect.left);
+ h += rect.bottom - rect.top;
+ if (dummy) { clientDiv.removeChild(dummy); }
+ }
+ if (isWebkit) {
+ clientDiv.style.width = clientWidth;
+ }
+ var viewPadding = this._getViewPadding();
+ w += viewPadding.right - viewPadding.left;
+ h += viewPadding.bottom - viewPadding.top;
+ return {width: w, height: h};
+ },
+ /**
+ * Converts the given rectangle from one coordinate spaces to another.
+ * <p>The supported coordinate spaces are:
+ * <ul>
+ * <li>"document" - relative to document, the origin is the top-left corner of first line</li>
+ * <li>"page" - relative to html page that contains the text view</li>
+ * <li>"view" - relative to text view, the origin is the top-left corner of the view container</li>
+ * </ul>
+ * </p>
+ * <p>All methods in the view that take or return a position are in the document coordinate space.</p>
+ *
+ * @param rect the rectangle to convert.
+ * @param rect.x the x of the rectangle.
+ * @param rect.y the y of the rectangle.
+ * @param rect.width the width of the rectangle.
+ * @param rect.height the height of the rectangle.
+ * @param {String} from the source coordinate space.
+ * @param {String} to the destination coordinate space.
+ *
+ * @see #getLocationAtOffset
+ * @see #getOffsetAtLocation
+ * @see #getTopPixel
+ * @see #setTopPixel
+ */
+ convert: function(rect, from, to) {
+ if (!this._clientDiv) { return; }
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var frame = this._frame.getBoundingClientRect();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ switch(from) {
+ case "document":
+ if (rect.x !== undefined) {
+ rect.x += - scroll.x + viewRect.left + viewPad.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += - scroll.y + viewRect.top + viewPad.top;
+ }
+ break;
+ case "page":
+ if (rect.x !== undefined) {
+ rect.x += - frame.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += - frame.top;
+ }
+ break;
+ }
+ //At this point rect is in the widget coordinate space
+ switch (to) {
+ case "document":
+ if (rect.x !== undefined) {
+ rect.x += scroll.x - viewRect.left - viewPad.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += scroll.y - viewRect.top - viewPad.top;
+ }
+ break;
+ case "page":
+ if (rect.x !== undefined) {
+ rect.x += frame.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += frame.top;
+ }
+ break;
+ }
+ return rect;
+ },
+ /**
+ * Destroys the text view.
+ * <p>
+ * Removes the view from the page and frees all resources created by the view.
+ * Calling this function causes the "Destroy" event to be fire so that all components
+ * attached to view can release their references.
+ * </p>
+ *
+ * @see #onDestroy
+ */
+ destroy: function() {
+ /* Destroy rulers*/
+ for (var i=0; i< this._rulers.length; i++) {
+ this._rulers[i].setView(null);
+ }
+ this.rulers = null;
+
+ /*
+ * Note that when the frame is removed, the unload event is trigged
+ * and the view contents and handlers is released properly by
+ * destroyView().
+ */
+ this._destroyFrame();
+
+ var e = {type: "Destroy"};
+ this.onDestroy(e);
+
+ this._parent = null;
+ this._parentDocument = null;
+ this._model = null;
+ this._selection = null;
+ this._doubleClickSelection = null;
+ this._keyBindings = null;
+ this._actions = null;
+ },
+ /**
+ * Gives focus to the text view.
+ */
+ focus: function() {
+ if (!this._clientDiv) { return; }
+ /*
+ * Feature in Chrome. When focus is called in the clientDiv without
+ * setting selection the browser will set the selection to the first dom
+ * element, which can be above the client area. When this happen the
+ * browser also scrolls the window to show that element.
+ * The fix is to call _updateDOMSelection() before calling focus().
+ */
+ this._updateDOMSelection();
+ if (isPad) {
+ this._textArea.focus();
+ } else {
+ if (isOpera) { this._clientDiv.blur(); }
+ this._clientDiv.focus();
+ }
+ /*
+ * Feature in Safari. When focus is called the browser selects the clientDiv
+ * itself. The fix is to call _updateDOMSelection() after calling focus().
+ */
+ this._updateDOMSelection();
+ },
+ /**
+ * Check if the text view has focus.
+ *
+ * @returns {Boolean} <code>true</code> if the text view has focus, otherwise <code>false</code>.
+ */
+ hasFocus: function() {
+ return this._hasFocus;
+ },
+ /**
+ * Returns all action names defined in the text view.
+ * <p>
+ * There are two types of actions, the predefined actions of the view
+ * and the actions added by application code.
+ * </p>
+ * <p>
+ * The predefined actions are:
+ * <ul>
+ * <li>Navigation actions. These actions move the caret collapsing the selection.</li>
+ * <ul>
+ * <li>"lineUp" - moves the caret up by one line</li>
+ * <li>"lineDown" - moves the caret down by one line</li>
+ * <li>"lineStart" - moves the caret to beginning of the current line</li>
+ * <li>"lineEnd" - moves the caret to end of the current line </li>
+ * <li>"charPrevious" - moves the caret to the previous character</li>
+ * <li>"charNext" - moves the caret to the next character</li>
+ * <li>"pageUp" - moves the caret up by one page</li>
+ * <li>"pageDown" - moves the caret down by one page</li>
+ * <li>"wordPrevious" - moves the caret to the previous word</li>
+ * <li>"wordNext" - moves the caret to the next word</li>
+ * <li>"textStart" - moves the caret to the beginning of the document</li>
+ * <li>"textEnd" - moves the caret to the end of the document</li>
+ * </ul>
+ * <li>Selection actions. These actions move the caret extending the selection.</li>
+ * <ul>
+ * <li>"selectLineUp" - moves the caret up by one line</li>
+ * <li>"selectLineDown" - moves the caret down by one line</li>
+ * <li>"selectLineStart" - moves the caret to beginning of the current line</li>
+ * <li>"selectLineEnd" - moves the caret to end of the current line </li>
+ * <li>"selectCharPrevious" - moves the caret to the previous character</li>
+ * <li>"selectCharNext" - moves the caret to the next character</li>
+ * <li>"selectPageUp" - moves the caret up by one page</li>
+ * <li>"selectPageDown" - moves the caret down by one page</li>
+ * <li>"selectWordPrevious" - moves the caret to the previous word</li>
+ * <li>"selectWordNext" - moves the caret to the next word</li>
+ * <li>"selectTextStart" - moves the caret to the beginning of the document</li>
+ * <li>"selectTextEnd" - moves the caret to the end of the document</li>
+ * <li>"selectAll" - selects the entire document</li>
+ * </ul>
+ * <li>Edit actions. These actions modify the text view text</li>
+ * <ul>
+ * <li>"deletePrevious" - deletes the character preceding the caret</li>
+ * <li>"deleteNext" - deletes the charecter following the caret</li>
+ * <li>"deleteWordPrevious" - deletes the word preceding the caret</li>
+ * <li>"deleteWordNext" - deletes the word following the caret</li>
+ * <li>"tab" - inserts a tab character at the caret</li>
+ * <li>"enter" - inserts a line delimiter at the caret</li>
+ * </ul>
+ * <li>Clipboard actions.</li>
+ * <ul>
+ * <li>"copy" - copies the selected text to the clipboard</li>
+ * <li>"cut" - copies the selected text to the clipboard and deletes the selection</li>
+ * <li>"paste" - replaces the selected text with the clipboard contents</li>
+ * </ul>
+ * </ul>
+ * </p>
+ *
+ * @param {Boolean} [defaultAction=false] whether or not the predefined actions are included.
+ * @returns {String[]} an array of action names defined in the text view.
+ *
+ * @see #invokeAction
+ * @see #setAction
+ * @see #setKeyBinding
+ * @see #getKeyBindings
+ */
+ getActions: function (defaultAction) {
+ var result = [];
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ if (!defaultAction && actions[i].defaultHandler) { continue; }
+ result.push(actions[i].name);
+ }
+ return result;
+ },
+ /**
+ * Returns the bottom index.
+ * <p>
+ * The bottom index is the line that is currently at the bottom of the view. This
+ * line may be partially visible depending on the vertical scroll of the view. The parameter
+ * <code>fullyVisible</code> determines whether to return only fully visible lines.
+ * </p>
+ *
+ * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the last fully visible line. This
+ * parameter is ignored if the view is not big enough to show one line.
+ * @returns {Number} the index of the bottom line.
+ *
+ * @see #getTopIndex
+ * @see #setTopIndex
+ */
+ getBottomIndex: function(fullyVisible) {
+ if (!this._clientDiv) { return 0; }
+ return this._getBottomIndex(fullyVisible);
+ },
+ /**
+ * Returns the bottom pixel.
+ * <p>
+ * The bottom pixel is the pixel position that is currently at
+ * the bottom edge of the view. This position is relative to the
+ * beginning of the document.
+ * </p>
+ *
+ * @returns {Number} the bottom pixel.
+ *
+ * @see #getTopPixel
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getBottomPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().y + this._getClientHeight();
+ },
+ /**
+ * Returns the caret offset relative to the start of the document.
+ *
+ * @returns the caret offset relative to the start of the document.
+ *
+ * @see #setCaretOffset
+ * @see #setSelection
+ * @see #getSelection
+ */
+ getCaretOffset: function () {
+ var s = this._getSelection();
+ return s.getCaret();
+ },
+ /**
+ * Returns the client area.
+ * <p>
+ * The client area is the portion in pixels of the document that is visible. The
+ * client area position is relative to the beginning of the document.
+ * </p>
+ *
+ * @returns the client area rectangle {x, y, width, height}.
+ *
+ * @see #getTopPixel
+ * @see #getBottomPixel
+ * @see #getHorizontalPixel
+ * @see #convert
+ */
+ getClientArea: function() {
+ if (!this._clientDiv) { return {x: 0, y: 0, width: 0, height: 0}; }
+ var scroll = this._getScroll();
+ return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()};
+ },
+ /**
+ * Returns the horizontal pixel.
+ * <p>
+ * The horizontal pixel is the pixel position that is currently at
+ * the left edge of the view. This position is relative to the
+ * beginning of the document.
+ * </p>
+ *
+ * @returns {Number} the horizontal pixel.
+ *
+ * @see #setHorizontalPixel
+ * @see #convert
+ */
+ getHorizontalPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().x;
+ },
+ /**
+ * Returns all the key bindings associated to the given action name.
+ *
+ * @param {String} name the action name.
+ * @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name.
+ *
+ * @see #setKeyBinding
+ * @see #setAction
+ */
+ getKeyBindings: function (name) {
+ var result = [];
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ if (keyBindings[i].name === name) {
+ result.push(keyBindings[i].keyBinding);
+ }
+ }
+ return result;
+ },
+ /**
+ * Returns the line height for a given line index. Returns the default line
+ * height if the line index is not specified.
+ *
+ * @param {Number} [lineIndex] the line index.
+ * @returns {Number} the height of the line in pixels.
+ *
+ * @see #getLinePixel
+ */
+ getLineHeight: function(lineIndex) {
+ if (!this._clientDiv) { return 0; }
+ return this._getLineHeight();
+ },
+ /**
+ * Returns the top pixel position of a given line index relative to the beginning
+ * of the document.
+ * <p>
+ * Clamps out of range indices.
+ * </p>
+ *
+ * @param {Number} lineIndex the line index.
+ * @returns {Number} the pixel position of the line.
+ *
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getLinePixel: function(lineIndex) {
+ if (!this._clientDiv) { return 0; }
+ lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount());
+ var lineHeight = this._getLineHeight();
+ return lineHeight * lineIndex;
+ },
+ /**
+ * Returns the {x, y} pixel location of the top-left corner of the character
+ * bounding box at the specified offset in the document. The pixel location
+ * is relative to the document.
+ * <p>
+ * Clamps out of range offsets.
+ * </p>
+ *
+ * @param {Number} offset the character offset
+ * @returns the {x, y} pixel location of the given offset.
+ *
+ * @see #getOffsetAtLocation
+ * @see #convert
+ */
+ getLocationAtOffset: function(offset) {
+ if (!this._clientDiv) { return {x: 0, y: 0}; }
+ var model = this._model;
+ offset = Math.min(Math.max(0, offset), model.getCharCount());
+ var lineIndex = model.getLineAtOffset(offset);
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left;
+ var y = this.getLinePixel(lineIndex);
+ return {x: x, y: y};
+ },
+ /**
+ * Returns the specified view options.
+ * <p>
+ * The returned value is either a <code>orion.textview.TextViewOptions</code> or an option value. An option value is returned when only one string paremeter
+ * is specified. A <code>orion.textview.TextViewOptions</code> is returned when there are no paremeters, or the parameters are a list of options names or a
+ * <code>orion.textview.TextViewOptions</code>. All view options are returned when there no paremeters.
+ * </p>
+ *
+ * @param {String|orion.textview.TextViewOptions} [options] The options to return.
+ * @return {Object|orion.textview.TextViewOptions} The requested options or an option value.
+ *
+ * @see #setOptions
+ */
+ getOptions: function() {
+ var options;
+ if (arguments.length === 0) {
+ options = this._defaultOptions();
+ } else if (arguments.length === 1) {
+ var arg = arguments[0];
+ if (typeof arg === "string") {
+ return this._clone(this["_" + arg]);
+ }
+ options = arg;
+ } else {
+ options = {};
+ for (var index in arguments) {
+ if (arguments.hasOwnProperty(index)) {
+ options[arguments[index]] = undefined;
+ }
+ }
+ }
+ for (var option in options) {
+ if (options.hasOwnProperty(option)) {
+ options[option] = this._clone(this["_" + option]);
+ }
+ }
+ return options;
+ },
+ /**
+ * Returns the text model of the text view.
+ *
+ * @returns {orion.textview.TextModel} the text model of the view.
+ */
+ getModel: function() {
+ return this._model;
+ },
+ /**
+ * Returns the character offset nearest to the given pixel location. The
+ * pixel location is relative to the document.
+ *
+ * @param x the x of the location
+ * @param y the y of the location
+ * @returns the character offset at the given location.
+ *
+ * @see #getLocationAtOffset
+ */
+ getOffsetAtLocation: function(x, y) {
+ if (!this._clientDiv) { return 0; }
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var lineIndex = this._getYToLine(y - scroll.y);
+ x += -scroll.x + viewRect.left + viewPad.left;
+ var offset = this._getXToOffset(lineIndex, x);
+ return offset;
+ },
+ /**
+ * Get the view rulers.
+ *
+ * @returns the view rulers
+ *
+ * @see #addRuler
+ */
+ getRulers: function() {
+ return this._rulers.slice(0);
+ },
+ /**
+ * Returns the text view selection.
+ * <p>
+ * The selection is defined by a start and end character offset relative to the
+ * document. The character at end offset is not included in the selection.
+ * </p>
+ *
+ * @returns {orion.textview.Selection} the view selection
+ *
+ * @see #setSelection
+ */
+ getSelection: function () {
+ var s = this._getSelection();
+ return {start: s.start, end: s.end};
+ },
+ /**
+ * Returns the text for the given range.
+ * <p>
+ * The text does not include the character at the end offset.
+ * </p>
+ *
+ * @param {Number} [start=0] the start offset of text range.
+ * @param {Number} [end=char count] the end offset of text range.
+ *
+ * @see #setText
+ */
+ getText: function(start, end) {
+ var model = this._model;
+ return model.getText(start, end);
+ },
+ /**
+ * Returns the top index.
+ * <p>
+ * The top index is the line that is currently at the top of the view. This
+ * line may be partially visible depending on the vertical scroll of the view. The parameter
+ * <code>fullyVisible</code> determines whether to return only fully visible lines.
+ * </p>
+ *
+ * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the first fully visible line. This
+ * parameter is ignored if the view is not big enough to show one line.
+ * @returns {Number} the index of the top line.
+ *
+ * @see #getBottomIndex
+ * @see #setTopIndex
+ */
+ getTopIndex: function(fullyVisible) {
+ if (!this._clientDiv) { return 0; }
+ return this._getTopIndex(fullyVisible);
+ },
+ /**
+ * Returns the top pixel.
+ * <p>
+ * The top pixel is the pixel position that is currently at
+ * the top edge of the view. This position is relative to the
+ * beginning of the document.
+ * </p>
+ *
+ * @returns {Number} the top pixel.
+ *
+ * @see #getBottomPixel
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getTopPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().y;
+ },
+ /**
+ * Executes the action handler associated with the given name.
+ * <p>
+ * The application defined action takes precedence over predefined actions unless
+ * the <code>defaultAction</code> paramater is <code>true</code>.
+ * </p>
+ * <p>
+ * If the application defined action returns <code>false</code>, the text view predefined
+ * action is executed if present.
+ * </p>
+ *
+ * @param {String} name the action name.
+ * @param {Boolean} [defaultAction] whether to always execute the predefined action.
+ * @returns {Boolean} <code>true</code> if the action was executed.
+ *
+ * @see #setAction
+ * @see #getActions
+ */
+ invokeAction: function (name, defaultAction) {
+ if (!this._clientDiv) { return; }
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ var a = actions[i];
+ if (a.name && a.name === name) {
+ if (!defaultAction && a.userHandler) {
+ if (a.userHandler()) { return; }
+ }
+ if (a.defaultHandler) { return a.defaultHandler(); }
+ return false;
+ }
+ }
+ return false;
+ },
+ /**
+ * Returns if the view is loaded.
+ * <p>
+ * @returns {Boolean} <code>true</code> if the view is loaded.
+ *
+ * @see #onLoad
+ */
+ isLoaded: function () {
+ return !!this._clientDiv;
+ },
+ /**
+ * @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onContextMenu}
+ * </p>
+ *
+ * @name orion.textview.ContextMenuEvent
+ *
+ * @property {Number} x The pointer location on the x axis, relative to the document the user is editing.
+ * @property {Number} y The pointer location on the y axis, relative to the document the user is editing.
+ * @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property.
+ * @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property.
+ */
+ /**
+ * This event is sent when the user invokes the view context menu.
+ *
+ * @event
+ * @param {orion.textview.ContextMenuEvent} contextMenuEvent the event
+ */
+ onContextMenu: function(contextMenuEvent) {
+ return this.dispatchEvent(contextMenuEvent);
+ },
+ onDragStart: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDrag: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragEnd: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragEnter: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragOver: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragLeave: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDrop: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ /**
+ * @class This is the event sent when the text view is destroyed.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onDestroy}
+ * </p>
+ * @name orion.textview.DestroyEvent
+ */
+ /**
+ * This event is sent when the text view has been destroyed.
+ *
+ * @event
+ * @param {orion.textview.DestroyEvent} destroyEvent the event
+ *
+ * @see #destroy
+ */
+ onDestroy: function(destroyEvent) {
+ return this.dispatchEvent(destroyEvent);
+ },
+ /**
+ * @class This object is used to define style information for the text view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onLineStyle}
+ * </p>
+ * @name orion.textview.Style
+ *
+ * @property {String} styleClass A CSS class name.
+ * @property {Object} style An object with CSS properties.
+ * @property {String} tagName A DOM tag name.
+ * @property {Object} attributes An object with DOM attributes.
+ */
+ /**
+ * @class This object is used to style range.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onLineStyle}
+ * </p>
+ * @name orion.textview.StyleRange
+ *
+ * @property {Number} start The start character offset, relative to the document, where the style should be applied.
+ * @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied.
+ * @property {orion.textview.Style} style The style for the range.
+ */
+ /**
+ * @class This is the event sent when the text view needs the style information for a line.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onLineStyle}
+ * </p>
+ * @name orion.textview.LineStyleEvent
+ *
+ * @property {orion.textview.TextView} textView The text view.
+ * @property {Number} lineIndex The line index.
+ * @property {String} lineText The line text.
+ * @property {Number} lineStart The character offset, relative to document, of the first character in the line.
+ * @property {orion.textview.Style} style The style for the entire line (output argument).
+ * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument).
+ */
+ /**
+ * This event is sent when the text view needs the style information for a line.
+ *
+ * @event
+ * @param {orion.textview.LineStyleEvent} lineStyleEvent the event
+ */
+ onLineStyle: function(lineStyleEvent) {
+ return this.dispatchEvent(lineStyleEvent);
+ },
+ /**
+ * @class This is the event sent when the text view has loaded its contents.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onLoad}
+ * </p>
+ * @name orion.textview.LoadEvent
+ */
+ /**
+ * This event is sent when the text view has loaded its contents.
+ *
+ * @event
+ * @param {orion.textview.LoadEvent} loadEvent the event
+ */
+ onLoad: function(loadEvent) {
+ return this.dispatchEvent(loadEvent);
+ },
+ /**
+ * @class This is the event sent when the text in the model has changed.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onModelChanged}<br/>
+ * {@link orion.textview.TextModel#onChanged}
+ * </p>
+ * @name orion.textview.ModelChangedEvent
+ *
+ * @property {Number} start The character offset in the model where the change has occurred.
+ * @property {Number} removedCharCount The number of characters removed from the model.
+ * @property {Number} addedCharCount The number of characters added to the model.
+ * @property {Number} removedLineCount The number of lines removed from the model.
+ * @property {Number} addedLineCount The number of lines added to the model.
+ */
+ /**
+ * This event is sent when the text in the model has changed.
+ *
+ * @event
+ * @param {orion.textview.ModelChangedEvent} modelChangedEvent the event
+ */
+ onModelChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * @class This is the event sent when the text in the model is about to change.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onModelChanging}<br/>
+ * {@link orion.textview.TextModel#onChanging}
+ * </p>
+ * @name orion.textview.ModelChangingEvent
+ *
+ * @property {String} text The text that is about to be inserted in the model.
+ * @property {Number} start The character offset in the model where the change will occur.
+ * @property {Number} removedCharCount The number of characters being removed from the model.
+ * @property {Number} addedCharCount The number of characters being added to the model.
+ * @property {Number} removedLineCount The number of lines being removed from the model.
+ * @property {Number} addedLineCount The number of lines being added to the model.
+ */
+ /**
+ * This event is sent when the text in the model is about to change.
+ *
+ * @event
+ * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event
+ */
+ onModelChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * @class This is the event sent when the text is modified by the text view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onModify}
+ * </p>
+ * @name orion.textview.ModifyEvent
+ */
+ /**
+ * This event is sent when the text view has changed text in the model.
+ * <p>
+ * If the text is changed directly through the model API, this event
+ * is not sent.
+ * </p>
+ *
+ * @event
+ * @param {orion.textview.ModifyEvent} modifyEvent the event
+ */
+ onModify: function(modifyEvent) {
+ return this.dispatchEvent(modifyEvent);
+ },
+ onMouseDown: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseUp: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseMove: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseOver: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseOut: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ /**
+ * @class This is the event sent when the selection changes in the text view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onSelection}
+ * </p>
+ * @name orion.textview.SelectionEvent
+ *
+ * @property {orion.textview.Selection} oldValue The old selection.
+ * @property {orion.textview.Selection} newValue The new selection.
+ */
+ /**
+ * This event is sent when the text view selection has changed.
+ *
+ * @event
+ * @param {orion.textview.SelectionEvent} selectionEvent the event
+ */
+ onSelection: function(selectionEvent) {
+ return this.dispatchEvent(selectionEvent);
+ },
+ /**
+ * @class This is the event sent when the text view scrolls.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onScroll}
+ * </p>
+ * @name orion.textview.ScrollEvent
+ *
+ * @property oldValue The old scroll {x,y}.
+ * @property newValue The new scroll {x,y}.
+ */
+ /**
+ * This event is sent when the text view scrolls vertically or horizontally.
+ *
+ * @event
+ * @param {orion.textview.ScrollEvent} scrollEvent the event
+ */
+ onScroll: function(scrollEvent) {
+ return this.dispatchEvent(scrollEvent);
+ },
+ /**
+ * @class This is the event sent when the text is about to be modified by the text view.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onVerify}
+ * </p>
+ * @name orion.textview.VerifyEvent
+ *
+ * @property {String} text The text being inserted.
+ * @property {Number} start The start offset of the text range to be replaced.
+ * @property {Number} end The end offset (exclusive) of the text range to be replaced.
+ */
+ /**
+ * This event is sent when the text view is about to change text in the model.
+ * <p>
+ * If the text is changed directly through the model API, this event
+ * is not sent.
+ * </p>
+ * <p>
+ * Listeners are allowed to change these parameters. Setting text to null
+ * or undefined stops the change.
+ * </p>
+ *
+ * @event
+ * @param {orion.textview.VerifyEvent} verifyEvent the event
+ */
+ onVerify: function(verifyEvent) {
+ return this.dispatchEvent(verifyEvent);
+ },
+ /**
+ * @class This is the event sent when the text view has unloaded its contents.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onLoad}
+ * </p>
+ * @name orion.textview.UnloadEvent
+ */
+ /**
+ * This event is sent when the text view has unloaded its contents.
+ *
+ * @event
+ * @param {orion.textview.UnloadEvent} unloadEvent the event
+ */
+ onUnload: function(unloadEvent) {
+ return this.dispatchEvent(unloadEvent);
+ },
+ /**
+ * @class This is the event sent when the text view is focused.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onFocus}<br/>
+ * </p>
+ * @name orion.textview.FocusEvent
+ */
+ /**
+ * This event is sent when the text view is focused.
+ *
+ * @event
+ * @param {orion.textview.FocusEvent} focusEvent the event
+ */
+ onFocus: function(focusEvent) {
+ return this.dispatchEvent(focusEvent);
+ },
+ /**
+ * @class This is the event sent when the text view goes out of focus.
+ * <p>
+ * <b>See:</b><br/>
+ * {@link orion.textview.TextView}<br/>
+ * {@link orion.textview.TextView#event:onBlur}<br/>
+ * </p>
+ * @name orion.textview.BlurEvent
+ */
+ /**
+ * This event is sent when the text view goes out of focus.
+ *
+ * @event
+ * @param {orion.textview.BlurEvent} blurEvent the event
+ */
+ onBlur: function(blurEvent) {
+ return this.dispatchEvent(blurEvent);
+ },
+ /**
+ * Redraws the entire view, including rulers.
+ *
+ * @see #redrawLines
+ * @see #redrawRange
+ * @see #setRedraw
+ */
+ redraw: function() {
+ if (this._redrawCount > 0) { return; }
+ var lineCount = this._model.getLineCount();
+ var rulers = this.getRulers();
+ for (var i = 0; i < rulers.length; i++) {
+ this.redrawLines(0, lineCount, rulers[i]);
+ }
+ this.redrawLines(0, lineCount);
+ },
+ /**
+ * Redraws the text in the given line range.
+ * <p>
+ * The line at the end index is not redrawn.
+ * </p>
+ *
+ * @param {Number} [startLine=0] the start line
+ * @param {Number} [endLine=line count] the end line
+ *
+ * @see #redraw
+ * @see #redrawRange
+ * @see #setRedraw
+ */
+ redrawLines: function(startLine, endLine, ruler) {
+ if (this._redrawCount > 0) { return; }
+ if (startLine === undefined) { startLine = 0; }
+ if (endLine === undefined) { endLine = this._model.getLineCount(); }
+ if (startLine === endLine) { return; }
+ var div = this._clientDiv;
+ if (!div) { return; }
+ if (ruler) {
+ var location = ruler.getLocation();//"left" or "right"
+ var divRuler = location === "left" ? this._leftDiv : this._rightDiv;
+ var cells = divRuler.firstChild.rows[0].cells;
+ for (var i = 0; i < cells.length; i++) {
+ if (cells[i].firstChild._ruler === ruler) {
+ div = cells[i].firstChild;
+ break;
+ }
+ }
+ }
+ if (ruler) {
+ div.rulerChanged = true;
+ }
+ if (!ruler || ruler.getOverview() === "page") {
+ var child = div.firstChild;
+ while (child) {
+ var lineIndex = child.lineIndex;
+ if (startLine <= lineIndex && lineIndex < endLine) {
+ child.lineChanged = true;
+ }
+ child = child.nextSibling;
+ }
+ }
+ if (!ruler) {
+ if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) {
+ this._checkMaxLineIndex = this._maxLineIndex;
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ }
+ }
+ this._queueUpdatePage();
+ },
+ /**
+ * Redraws the text in the given range.
+ * <p>
+ * The character at the end offset is not redrawn.
+ * </p>
+ *
+ * @param {Number} [start=0] the start offset of text range
+ * @param {Number} [end=char count] the end offset of text range
+ *
+ * @see #redraw
+ * @see #redrawLines
+ * @see #setRedraw
+ */
+ redrawRange: function(start, end) {
+ if (this._redrawCount > 0) { return; }
+ var model = this._model;
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = model.getCharCount(); }
+ var startLine = model.getLineAtOffset(start);
+ var endLine = model.getLineAtOffset(Math.max(start, end - 1)) + 1;
+ this.redrawLines(startLine, endLine);
+ },
+ /**
+ * Removes a ruler from the text view.
+ *
+ * @param {orion.textview.Ruler} ruler the ruler.
+ */
+ removeRuler: function (ruler) {
+ var rulers = this._rulers;
+ for (var i=0; i<rulers.length; i++) {
+ if (rulers[i] === ruler) {
+ rulers.splice(i, 1);
+ ruler.setView(null);
+ this._destroyRuler(ruler);
+ this._updatePage();
+ break;
+ }
+ }
+ },
+ /**
+ * Associates an application defined handler to an action name.
+ * <p>
+ * If the action name is a predefined action, the given handler executes before
+ * the default action handler. If the given handler returns <code>true</code>, the
+ * default action handler is not called.
+ * </p>
+ *
+ * @param {String} name the action name.
+ * @param {Function} handler the action handler.
+ *
+ * @see #getActions
+ * @see #invokeAction
+ */
+ setAction: function(name, handler) {
+ if (!name) { return; }
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ var a = actions[i];
+ if (a.name === name) {
+ a.userHandler = handler;
+ return;
+ }
+ }
+ actions.push({name: name, userHandler: handler});
+ },
+ /**
+ * Associates a key binding with the given action name. Any previous
+ * association with the specified key binding is overwriten. If the
+ * action name is <code>null</code>, the association is removed.
+ *
+ * @param {orion.textview.KeyBinding} keyBinding the key binding
+ * @param {String} name the action
+ */
+ setKeyBinding: function(keyBinding, name) {
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ var kb = keyBindings[i];
+ if (kb.keyBinding.equals(keyBinding)) {
+ if (name) {
+ kb.name = name;
+ } else {
+ if (kb.predefined) {
+ kb.name = null;
+ } else {
+ var oldName = kb.name;
+ keyBindings.splice(i, 1);
+ var index = 0;
+ while (index < keyBindings.length && oldName !== keyBindings[index].name) {
+ index++;
+ }
+ if (index === keyBindings.length) {
+ /* <p>
+ * Removing all the key bindings associated to an user action will cause
+ * the user action to be removed. TextView predefined actions are never
+ * removed (so they can be reinstalled in the future).
+ * </p>
+ */
+ var actions = this._actions;
+ for (var j = 0; j < actions.length; j++) {
+ if (actions[j].name === oldName) {
+ if (!actions[j].defaultHandler) {
+ actions.splice(j, 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ return;
+ }
+ }
+ if (name) {
+ keyBindings.push({keyBinding: keyBinding, name: name});
+ }
+ },
+ /**
+ * Sets the caret offset relative to the start of the document.
+ *
+ * @param {Number} caret the caret offset relative to the start of the document.
+ * @param {Boolean} [show=true] if <code>true</code>, the view will scroll if needed to show the caret location.
+ *
+ * @see #getCaretOffset
+ * @see #setSelection
+ * @see #getSelection
+ */
+ setCaretOffset: function(offset, show) {
+ var charCount = this._model.getCharCount();
+ offset = Math.max(0, Math.min (offset, charCount));
+ var selection = new Selection(offset, offset, false);
+ this._setSelection (selection, show === undefined || show);
+ },
+ /**
+ * Sets the horizontal pixel.
+ * <p>
+ * The horizontal pixel is the pixel position that is currently at
+ * the left edge of the view. This position is relative to the
+ * beginning of the document.
+ * </p>
+ *
+ * @param {Number} pixel the horizontal pixel.
+ *
+ * @see #getHorizontalPixel
+ * @see #convert
+ */
+ setHorizontalPixel: function(pixel) {
+ if (!this._clientDiv) { return; }
+ pixel = Math.max(0, pixel);
+ this._scrollView(pixel - this._getScroll().x, 0);
+ },
+ /**
+ * Sets whether the view should update the DOM.
+ * <p>
+ * This can be used to improve the performance.
+ * </p><p>
+ * When the flag is set to <code>true</code>,
+ * the entire view is marked as needing to be redrawn.
+ * Nested calls to this method are stacked.
+ * </p>
+ *
+ * @param {Boolean} redraw the new redraw state
+ *
+ * @see #redraw
+ */
+ setRedraw: function(redraw) {
+ if (redraw) {
+ if (--this._redrawCount === 0) {
+ this.redraw();
+ }
+ } else {
+ this._redrawCount++;
+ }
+ },
+ /**
+ * Sets the text model of the text view.
+ *
+ * @param {orion.textview.TextModel} model the text model of the view.
+ */
+ setModel: function(model) {
+ if (!model) { return; }
+ if (model === this._model) { return; }
+ this._model.removeEventListener("Changing", this._modelListener.onChanging);
+ this._model.removeEventListener("Changed", this._modelListener.onChanged);
+ var oldLineCount = this._model.getLineCount();
+ var oldCharCount = this._model.getCharCount();
+ var newLineCount = model.getLineCount();
+ var newCharCount = model.getCharCount();
+ var newText = model.getText();
+ var e = {
+ type: "ModelChanging",
+ text: newText,
+ start: 0,
+ removedCharCount: oldCharCount,
+ addedCharCount: newCharCount,
+ removedLineCount: oldLineCount,
+ addedLineCount: newLineCount
+ };
+ this.onModelChanging(e);
+ this._model = model;
+ e = {
+ type: "ModelChanged",
+ start: 0,
+ removedCharCount: oldCharCount,
+ addedCharCount: newCharCount,
+ removedLineCount: oldLineCount,
+ addedLineCount: newLineCount
+ };
+ this.onModelChanged(e);
+ this._model.addEventListener("Changing", this._modelListener.onChanging);
+ this._model.addEventListener("Changed", this._modelListener.onChanged);
+ this._reset();
+ this._updatePage();
+ },
+ /**
+ * Sets the view options for the view.
+ *
+ * @param {orion.textview.TextViewOptions} options the view options.
+ *
+ * @see #getOptions
+ */
+ setOptions: function (options) {
+ var defaultOptions = this._defaultOptions();
+ var recreate = false, option, created = this._clientDiv;
+ if (created) {
+ for (option in options) {
+ if (options.hasOwnProperty(option)) {
+ if (defaultOptions[option].recreate) {
+ recreate = true;
+ break;
+ }
+ }
+ }
+ }
+ var changed = false;
+ for (option in options) {
+ if (options.hasOwnProperty(option)) {
+ var newValue = options[option], oldValue = this["_" + option];
+ if (this._compare(oldValue, newValue)) { continue; }
+ changed = true;
+ if (!recreate) {
+ var update = defaultOptions[option].update;
+ if (created && update) {
+ if (update.call(this, newValue)) {
+ recreate = true;
+ }
+ continue;
+ }
+ }
+ this["_" + option] = this._clone(newValue);
+ }
+ }
+ if (changed) {
+ if (recreate) {
+ var oldParent = this._frame.parentNode;
+ oldParent.removeChild(this._frame);
+ this._parent.appendChild(this._frame);
+ }
+ }
+ },
+ /**
+ * Sets the text view selection.
+ * <p>
+ * The selection is defined by a start and end character offset relative to the
+ * document. The character at end offset is not included in the selection.
+ * </p>
+ * <p>
+ * The caret is always placed at the end offset. The start offset can be
+ * greater than the end offset to place the caret at the beginning of the
+ * selection.
+ * </p>
+ * <p>
+ * Clamps out of range offsets.
+ * </p>
+ *
+ * @param {Number} start the start offset of the selection
+ * @param {Number} end the end offset of the selection
+ * @param {Boolean} [show=true] if <code>true</code>, the view will scroll if needed to show the caret location.
+ *
+ * @see #getSelection
+ */
+ setSelection: function (start, end, show) {
+ var caret = start > end;
+ if (caret) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+ var charCount = this._model.getCharCount();
+ start = Math.max(0, Math.min (start, charCount));
+ end = Math.max(0, Math.min (end, charCount));
+ var selection = new Selection(start, end, caret);
+ this._setSelection(selection, show === undefined || show);
+ },
+ /**
+ * Replaces the text in the given range with the given text.
+ * <p>
+ * The character at the end offset is not replaced.
+ * </p>
+ * <p>
+ * When both <code>start</code> and <code>end</code> parameters
+ * are not specified, the text view places the caret at the beginning
+ * of the document and scrolls to make it visible.
+ * </p>
+ *
+ * @param {String} text the new text.
+ * @param {Number} [start=0] the start offset of text range.
+ * @param {Number} [end=char count] the end offset of text range.
+ *
+ * @see #getText
+ */
+ setText: function (text, start, end) {
+ var reset = start === undefined && end === undefined;
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this._model.getCharCount(); }
+ this._modifyContent({text: text, start: start, end: end, _code: true}, !reset);
+ if (reset) {
+ this._columnX = -1;
+ this._setSelection(new Selection (0, 0, false), true);
+
+ /*
+ * Bug in Firefox. For some reason, the caret does not show after the
+ * view is refreshed. The fix is to toggle the contentEditable state and
+ * force the clientDiv to loose and receive focus if it is focused.
+ */
+ if (isFirefox) {
+ this._fixCaret();
+ }
+ }
+ },
+ /**
+ * Sets the top index.
+ * <p>
+ * The top index is the line that is currently at the top of the text view. This
+ * line may be partially visible depending on the vertical scroll of the view.
+ * </p>
+ *
+ * @param {Number} topIndex the index of the top line.
+ *
+ * @see #getBottomIndex
+ * @see #getTopIndex
+ */
+ setTopIndex: function(topIndex) {
+ if (!this._clientDiv) { return; }
+ var model = this._model;
+ if (model.getCharCount() === 0) {
+ return;
+ }
+ var lineCount = model.getLineCount();
+ var lineHeight = this._getLineHeight();
+ var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight)));
+ if (topIndex < 0) {
+ topIndex = 0;
+ } else if (topIndex > lineCount - pageSize) {
+ topIndex = lineCount - pageSize;
+ }
+ var pixel = topIndex * lineHeight - this._getScroll().y;
+ this._scrollView(0, pixel);
+ },
+ /**
+ * Sets the top pixel.
+ * <p>
+ * The top pixel is the pixel position that is currently at
+ * the top edge of the view. This position is relative to the
+ * beginning of the document.
+ * </p>
+ *
+ * @param {Number} pixel the top pixel.
+ *
+ * @see #getBottomPixel
+ * @see #getTopPixel
+ * @see #convert
+ */
+ setTopPixel: function(pixel) {
+ if (!this._clientDiv) { return; }
+ var lineHeight = this._getLineHeight();
+ var clientHeight = this._getClientHeight();
+ var lineCount = this._model.getLineCount();
+ pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight);
+ this._scrollView(0, pixel - this._getScroll().y);
+ },
+ /**
+ * Scrolls the selection into view if needed.
+ *
+ * @returns true if the view was scrolled.
+ *
+ * @see #getSelection
+ * @see #setSelection
+ */
+ showSelection: function() {
+ return this._showCaret(true);
+ },
+
+ /**************************************** Event handlers *********************************/
+ _handleBodyMouseDown: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && e.which === 1) {
+ this._clientDiv.contentEditable = false;
+ (this._overlayDiv || this._clientDiv).draggable = true;
+ this._ignoreBlur = true;
+ }
+
+ /*
+ * Prevent clicks outside of the view from taking focus
+ * away the view. Note that in Firefox and Opera clicking on the
+ * scrollbar also take focus from the view. Other browsers
+ * do not have this problem and stopping the click over the
+ * scrollbar for them causes mouse capture problems.
+ */
+ var topNode = isOpera || (isFirefox && !this._overlayDiv) ? this._clientDiv : this._overlayDiv || this._viewDiv;
+
+ var temp = e.target ? e.target : e.srcElement;
+ while (temp) {
+ if (topNode === temp) {
+ return;
+ }
+ temp = temp.parentNode;
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ if (e.stopPropagation){ e.stopPropagation(); }
+ if (!isW3CEvents) {
+ /* In IE 8 is not possible to prevent the default handler from running
+ * during mouse down event using usual API. The workaround is to use
+ * setCapture/releaseCapture.
+ */
+ topNode.setCapture();
+ setTimeout(function() { topNode.releaseCapture(); }, 0);
+ }
+ },
+ _handleBodyMouseUp: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && e.which === 1) {
+ this._clientDiv.contentEditable = true;
+ (this._overlayDiv || this._clientDiv).draggable = false;
+
+ /*
+ * Bug in Firefox. For some reason, Firefox stops showing the caret
+ * in some cases. For example when the user cancels a drag operation
+ * by pressing ESC. The fix is to detect that the drag operation was
+ * cancelled, toggle the contentEditable state and force the clientDiv
+ * to loose and receive focus if it is focused.
+ */
+ this._fixCaret();
+ this._ignoreBlur = false;
+ }
+ },
+ _handleBlur: function (e) {
+ if (!e) { e = window.event; }
+ if (this._ignoreBlur) { return; }
+ this._hasFocus = false;
+ /*
+ * Bug in IE 8 and earlier. For some reason when text is deselected
+ * the overflow selection at the end of some lines does not get redrawn.
+ * The fix is to create a DOM element in the body to force a redraw.
+ */
+ if (isIE < 9) {
+ if (!this._getSelection().isEmpty()) {
+ var document = this._frameDocument;
+ var child = document.createElement("DIV");
+ var body = document.body;
+ body.appendChild(child);
+ body.removeChild(child);
+ }
+ }
+ if (isFirefox || isIE) {
+ if (this._selDiv1) {
+ var color = isIE ? "transparent" : "#AFAFAF";
+ this._selDiv1.style.background = color;
+ this._selDiv2.style.background = color;
+ this._selDiv3.style.background = color;
+ }
+ }
+ if (!this._ignoreFocus) {
+ this.onBlur({type: "Blur"});
+ }
+ },
+ _handleContextMenu: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && this._lastMouseButton === 3) {
+ // We need to update the DOM selection, because on
+ // right-click the caret moves to the mouse location.
+ // See bug 366312.
+ var timeDiff = e.timeStamp - this._lastMouseTime;
+ if (timeDiff <= this._clickTime) {
+ this._updateDOMSelection();
+ }
+ }
+ if (this.isListening("ContextMenu")) {
+ var evt = this._createMouseEvent("ContextMenu", e);
+ evt.screenX = e.screenX;
+ evt.screenY = e.screenY;
+ this.onContextMenu(evt);
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ },
+ _handleCopy: function (e) {
+ if (this._ignoreCopy) { return; }
+ if (!e) { e = window.event; }
+ if (this._doCopy(e)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleCut: function (e) {
+ if (!e) { e = window.event; }
+ if (this._doCut(e)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDOMAttrModified: function (e) {
+ if (!e) { e = window.event; }
+ var ancestor = false;
+ var parent = this._parent;
+ while (parent) {
+ if (parent === e.target) {
+ ancestor = true;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!ancestor) { return; }
+ var state = this._getVisible();
+ if (state === "visible") {
+ this._createView();
+ } else if (state === "hidden") {
+ this._destroyView();
+ }
+ },
+ _handleDataModified: function(e) {
+ this._startIME();
+ },
+ _handleDblclick: function (e) {
+ if (!e) { e = window.event; }
+ var time = e.timeStamp ? e.timeStamp : new Date().getTime();
+ this._lastMouseTime = time;
+ if (this._clickCount !== 2) {
+ this._clickCount = 2;
+ this._handleMouse(e);
+ }
+ },
+ _handleDragStart: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox) {
+ var self = this;
+ setTimeout(function() {
+ self._clientDiv.contentEditable = true;
+ self._clientDiv.draggable = false;
+ self._ignoreBlur = false;
+ }, 0);
+ }
+ if (this.isListening("DragStart") && this._dragOffset !== -1) {
+ this._isMouseDown = false;
+ this.onDragStart(this._createMouseEvent("DragStart", e));
+ this._dragOffset = -1;
+ } else {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDrag: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("Drag")) {
+ this.onDrag(this._createMouseEvent("Drag", e));
+ }
+ },
+ _handleDragEnd: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ this._dragOffset = -1;
+ if (this.isListening("DragEnd")) {
+ this.onDragEnd(this._createMouseEvent("DragEnd", e));
+ }
+ if (isFirefox) {
+ this._fixCaret();
+ /*
+ * Bug in Firefox. For some reason, Firefox stops showing the caret when the
+ * selection is dropped onto itself. The fix is to detected the case and
+ * call fixCaret() a second time.
+ */
+ if (e.dataTransfer.dropEffect === "none" && !e.dataTransfer.mozUserCancelled) {
+ this._fixCaret();
+ }
+ }
+ },
+ _handleDragEnter: function (e) {
+ if (!e) { e = window.event; }
+ var prevent = true;
+ this._dropTarget = true;
+ if (this.isListening("DragEnter")) {
+ prevent = false;
+ this.onDragEnter(this._createMouseEvent("DragEnter", e));
+ }
+ /*
+ * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
+ * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
+ * event will result is loss of functionality (insertion mark, etc).
+ */
+ if (isWebkit || prevent) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDragOver: function (e) {
+ if (!e) { e = window.event; }
+ var prevent = true;
+ if (this.isListening("DragOver")) {
+ prevent = false;
+ this.onDragOver(this._createMouseEvent("DragOver", e));
+ }
+ /*
+ * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
+ * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
+ * event will result is loss of functionality (insertion mark, etc).
+ */
+ if (isWebkit || prevent) {
+ if (prevent) { e.dataTransfer.dropEffect = "none"; }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDragLeave: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ if (this.isListening("DragLeave")) {
+ this.onDragLeave(this._createMouseEvent("DragLeave", e));
+ }
+ },
+ _handleDrop: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ if (this.isListening("Drop")) {
+ this.onDrop(this._createMouseEvent("Drop", e));
+ }
+ /*
+ * This event must be prevented otherwise the user agent will modify
+ * the DOM. Note that preventing the event on some user agents (i.e. IE)
+ * indicates that the operation is cancelled. This causes the dropEffect to
+ * be set to none in the dragend event causing the implementor to not execute
+ * the code responsible by the move effect.
+ */
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ },
+ _handleDocFocus: function (e) {
+ if (!e) { e = window.event; }
+ this._clientDiv.focus();
+ },
+ _handleFocus: function (e) {
+ if (!e) { e = window.event; }
+ this._hasFocus = true;
+ /*
+ * Feature in IE. The selection is not restored when the
+ * view gets focus and the caret is always placed at the
+ * beginning of the document. The fix is to update the DOM
+ * selection during the focus event.
+ */
+ if (isIE) {
+ this._updateDOMSelection();
+ }
+ if (isFirefox || isIE) {
+ if (this._selDiv1) {
+ var color = this._hightlightRGB;
+ this._selDiv1.style.background = color;
+ this._selDiv2.style.background = color;
+ this._selDiv3.style.background = color;
+ }
+ }
+ if (!this._ignoreFocus) {
+ this.onFocus({type: "Focus"});
+ }
+ },
+ _handleKeyDown: function (e) {
+ if (!e) { e = window.event; }
+ if (isPad) {
+ if (e.keyCode === 8) {
+ this._doBackspace({});
+ e.preventDefault();
+ }
+ return;
+ }
+ switch (e.keyCode) {
+ case 16: /* Shift */
+ case 17: /* Control */
+ case 18: /* Alt */
+ case 91: /* Command */
+ break;
+ default:
+ this._setLinksVisible(false);
+ }
+ if (e.keyCode === 229) {
+ if (this._readonly) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ var startIME = true;
+
+ /*
+ * Bug in Safari. Some Control+key combinations send key events
+ * with keyCode equals to 229. This is unexpected and causes the
+ * view to start an IME composition. The fix is to ignore these
+ * events.
+ */
+ if (isSafari && isMac) {
+ if (e.ctrlKey) {
+ startIME = false;
+ }
+ }
+ if (startIME) {
+ this._startIME();
+ }
+ } else {
+ this._commitIME();
+ }
+ /*
+ * Feature in Firefox. When a key is held down the browser sends
+ * right number of keypress events but only one keydown. This is
+ * unexpected and causes the view to only execute an action
+ * just one time. The fix is to ignore the keydown event and
+ * execute the actions from the keypress handler.
+ * Note: This only happens on the Mac and Linux (Firefox 3.6).
+ *
+ * Feature in Opera. Opera sends keypress events even for non-printable
+ * keys. The fix is to handle actions in keypress instead of keydown.
+ */
+ if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
+ this._keyDownEvent = e;
+ return true;
+ }
+
+ if (this._doAction(e)) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.cancelBubble = true;
+ e.returnValue = false;
+ e.keyCode = 0;
+ }
+ return false;
+ }
+ },
+ _handleKeyPress: function (e) {
+ if (!e) { e = window.event; }
+ /*
+ * Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and
+ * generates key press events for these Unicode values (Function keys). This does not
+ * happen in Safari or Chrome. The fix is to ignore these key events.
+ */
+ if (isMac && isWebkit) {
+ if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
+ if (this._doAction(this._keyDownEvent)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (e.charCode !== undefined) {
+ if (ctrlKey) {
+ switch (e.charCode) {
+ /*
+ * In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled
+ * the clipboard events are not sent. The fix to allow
+ * the browser to handles these key events.
+ */
+ case 99://c
+ case 118://v
+ case 120://x
+ return true;
+ }
+ }
+ }
+ var ignore = false;
+ if (isMac) {
+ if (e.ctrlKey || e.metaKey) { ignore = true; }
+ } else {
+ if (isFirefox) {
+ //Firefox clears the state mask when ALT GR generates input
+ if (e.ctrlKey || e.altKey) { ignore = true; }
+ } else {
+ //IE and Chrome only send ALT GR when input is generated
+ if (e.ctrlKey ^ e.altKey) { ignore = true; }
+ }
+ }
+ if (!ignore) {
+ var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode);
+ if (key > 31) {
+ this._doContent(String.fromCharCode (key));
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ },
+ _handleKeyUp: function (e) {
+ if (!e) { e = window.event; }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (!ctrlKey) {
+ this._setLinksVisible(false);
+ }
+ // don't commit for space (it happens during JP composition)
+ if (e.keyCode === 13) {
+ this._commitIME();
+ }
+ },
+ _handleLinkClick: function (e) {
+ if (!e) { e = window.event; }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (!ctrlKey) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleLoad: function (e) {
+ var state = this._getVisible();
+ if (state === "visible" || (state === "hidden" && isWebkit)) {
+ this._createView();
+ }
+ },
+ _handleMouse: function (e) {
+ var result = true;
+ var target = this._frameWindow;
+ if (isIE || (isFirefox && !this._overlayDiv)) { target = this._clientDiv; }
+ if (this._overlayDiv) {
+ if (this._hasFocus) {
+ this._ignoreFocus = true;
+ }
+ var self = this;
+ setTimeout(function () {
+ self.focus();
+ self._ignoreFocus = false;
+ }, 0);
+ }
+ if (this._clickCount === 1) {
+ result = this._setSelectionTo(e.clientX, e.clientY, e.shiftKey, !isOpera && this.isListening("DragStart"));
+ if (result) { this._setGrab(target); }
+ } else {
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ */
+ if (isW3CEvents) { this._setGrab(target); }
+
+ this._doubleClickSelection = null;
+ this._setSelectionTo(e.clientX, e.clientY, e.shiftKey);
+ this._doubleClickSelection = this._getSelection();
+ }
+ return result;
+ },
+ _handleMouseDown: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseDown")) {
+ this.onMouseDown(this._createMouseEvent("MouseDown", e));
+ }
+ if (this._linksVisible) {
+ var target = e.target || e.srcElement;
+ if (target.tagName !== "A") {
+ this._setLinksVisible(false);
+ } else {
+ return;
+ }
+ }
+ this._commitIME();
+
+ var button = e.which; // 1 - left, 2 - middle, 3 - right
+ if (!button) {
+ // if IE 8 or older
+ if (e.button === 4) { button = 2; }
+ if (e.button === 2) { button = 3; }
+ if (e.button === 1) { button = 1; }
+ }
+
+ // For middle click we always need getTime(). See _getClipboardText().
+ var time = button !== 2 && e.timeStamp ? e.timeStamp : new Date().getTime();
+ var timeDiff = time - this._lastMouseTime;
+ var deltaX = Math.abs(this._lastMouseX - e.clientX);
+ var deltaY = Math.abs(this._lastMouseY - e.clientY);
+ var sameButton = this._lastMouseButton === button;
+ this._lastMouseX = e.clientX;
+ this._lastMouseY = e.clientY;
+ this._lastMouseTime = time;
+ this._lastMouseButton = button;
+
+ if (button === 1) {
+ this._isMouseDown = true;
+ if (sameButton && timeDiff <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) {
+ this._clickCount++;
+ } else {
+ this._clickCount = 1;
+ }
+ if (this._handleMouse(e) && (isOpera || isChrome || (isFirefox && !this._overlayDiv))) {
+ if (!this._hasFocus) {
+ this.focus();
+ }
+ e.preventDefault();
+ }
+ }
+ },
+ _handleMouseOver: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseOver")) {
+ this.onMouseOver(this._createMouseEvent("MouseOver", e));
+ }
+ },
+ _handleMouseOut: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseOut")) {
+ this.onMouseOut(this._createMouseEvent("MouseOut", e));
+ }
+ },
+ _handleMouseMove: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseMove")) {
+ var topNode = this._overlayDiv || this._clientDiv;
+ var temp = e.target ? e.target : e.srcElement;
+ while (temp) {
+ if (topNode === temp) {
+ this.onMouseMove(this._createMouseEvent("MouseMove", e));
+ break;
+ }
+ temp = temp.parentNode;
+ }
+ }
+ if (this._dropTarget) {
+ return;
+ }
+ /*
+ * Bug in IE9. IE sends one mouse event when the user changes the text by
+ * pasting or undo. These operations usually happen with the Ctrl key
+ * down which causes the view to enter link mode. Link mode does not end
+ * because there are no further events. The fix is to only enter link
+ * mode when the coordinates of the mouse move event have changed.
+ */
+ var changed = this._linksVisible || this._lastMouseMoveX !== e.clientX || this._lastMouseMoveY !== e.clientY;
+ this._lastMouseMoveX = e.clientX;
+ this._lastMouseMoveY = e.clientY;
+ this._setLinksVisible(changed && !this._isMouseDown && (isMac ? e.metaKey : e.ctrlKey));
+
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ *
+ * In order to detect double-click and drag gestures, it is necessary to send
+ * a mouse down event from mouse move when the button is still down and isMouseDown
+ * flag is not set.
+ */
+ if (!isW3CEvents) {
+ if (e.button === 0) {
+ this._setGrab(null);
+ return true;
+ }
+ if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) {
+ this._clickCount = 2;
+ return this._handleMouse(e, this._clickCount);
+ }
+ }
+ if (!this._isMouseDown || this._dragOffset !== -1) {
+ return;
+ }
+
+ var x = e.clientX;
+ var y = e.clientY;
+ if (isChrome) {
+ if (e.currentTarget !== this._frameWindow) {
+ var rect = this._frame.getBoundingClientRect();
+ x -= rect.left;
+ y -= rect.top;
+ }
+ }
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var width = this._getClientWidth (), height = this._getClientHeight();
+ var leftEdge = viewRect.left + viewPad.left;
+ var topEdge = viewRect.top + viewPad.top;
+ var rightEdge = viewRect.left + viewPad.left + width;
+ var bottomEdge = viewRect.top + viewPad.top + height;
+ var model = this._model;
+ var caretLine = model.getLineAtOffset(this._getSelection().getCaret());
+ if (y < topEdge && caretLine !== 0) {
+ this._doAutoScroll("up", x, y - topEdge);
+ } else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) {
+ this._doAutoScroll("down", x, y - bottomEdge);
+ } else if (x < leftEdge) {
+ this._doAutoScroll("left", x - leftEdge, y);
+ } else if (x > rightEdge) {
+ this._doAutoScroll("right", x - rightEdge, y);
+ } else {
+ this._endAutoScroll();
+ this._setSelectionTo(x, y, true);
+ /*
+ * Feature in IE. IE does redraw the selection background right
+ * away after the selection changes because of mouse move events.
+ * The fix is to call getBoundingClientRect() on the
+ * body element to force the selection to be redraw. Some how
+ * calling this method forces a redraw.
+ */
+ if (isIE) {
+ var body = this._frameDocument.body;
+ body.getBoundingClientRect();
+ }
+ }
+ },
+ _createMouseEvent: function(type, e) {
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var x = e.clientX + scroll.x - viewRect.left - viewPad.left;
+ var y = e.clientY + scroll.y - viewRect.top;
+ return {
+ type: type,
+ event: e,
+ x: x,
+ y: y
+ };
+ },
+ _handleMouseUp: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseUp")) {
+ this.onMouseUp(this._createMouseEvent("MouseUp", e));
+ }
+ if (this._linksVisible) {
+ return;
+ }
+ var left = e.which ? e.button === 0 : e.button === 1;
+ if (left) {
+ if (this._dragOffset !== -1) {
+ var selection = this._getSelection();
+ selection.extend(this._dragOffset);
+ selection.collapse();
+ this._setSelection(selection, true, true);
+ this._dragOffset = -1;
+ }
+ this._isMouseDown = false;
+ this._endAutoScroll();
+
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ */
+ if (isW3CEvents) { this._setGrab(null); }
+
+ /*
+ * Note that there cases when Firefox sets the DOM selection in mouse up.
+ * This happens for example after a cancelled drag operation.
+ *
+ * Note that on Chrome and IE, the caret stops blicking if mouse up is
+ * prevented.
+ */
+ if (isFirefox) {
+ e.preventDefault();
+ }
+ }
+ },
+ _handleMouseWheel: function (e) {
+ if (!e) { e = window.event; }
+ var lineHeight = this._getLineHeight();
+ var pixelX = 0, pixelY = 0;
+ // Note: On the Mac the correct behaviour is to scroll by pixel.
+ if (isFirefox) {
+ var pixel;
+ if (isMac) {
+ pixel = e.detail * 3;
+ } else {
+ var limit = 256;
+ pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight;
+ }
+ if (e.axis === e.HORIZONTAL_AXIS) {
+ pixelX = pixel;
+ } else {
+ pixelY = pixel;
+ }
+ } else {
+ //Webkit
+ if (isMac) {
+ /*
+ * In Safari, the wheel delta is a multiple of 120. In order to
+ * convert delta to pixel values, it is necessary to divide delta
+ * by 40.
+ *
+ * In Chrome and Safari 5, the wheel delta depends on the type of the
+ * mouse. In general, it is the pixel value for Mac mice and track pads,
+ * but it is a multiple of 120 for other mice. There is no presise
+ * way to determine if it is pixel value or a multiple of 120.
+ *
+ * Note that the current approach does not calculate the correct
+ * pixel value for Mac mice when the delta is a multiple of 120.
+ */
+ var denominatorX = 40, denominatorY = 40;
+ if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; }
+ if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; }
+ pixelX = -e.wheelDeltaX / denominatorX;
+ if (-1 < pixelX && pixelX < 0) { pixelX = -1; }
+ if (0 < pixelX && pixelX < 1) { pixelX = 1; }
+ pixelY = -e.wheelDeltaY / denominatorY;
+ if (-1 < pixelY && pixelY < 0) { pixelY = -1; }
+ if (0 < pixelY && pixelY < 1) { pixelY = 1; }
+ } else {
+ pixelX = -e.wheelDeltaX;
+ var linesToScroll = 8;
+ pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight;
+ }
+ }
+ /*
+ * Feature in Safari. If the event target is removed from the DOM
+ * safari stops smooth scrolling. The fix is keep the element target
+ * in the DOM and remove it on a later time.
+ *
+ * Note: Using a timer is not a solution, because the timeout needs to
+ * be at least as long as the gesture (which is too long).
+ */
+ if (isSafari) {
+ var lineDiv = e.target;
+ while (lineDiv && lineDiv.lineIndex === undefined) {
+ lineDiv = lineDiv.parentNode;
+ }
+ this._mouseWheelLine = lineDiv;
+ }
+ var oldScroll = this._getScroll();
+ this._scrollView(pixelX, pixelY);
+ var newScroll = this._getScroll();
+ if (isSafari) { this._mouseWheelLine = null; }
+ if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handlePaste: function (e) {
+ if (this._ignorePaste) { return; }
+ if (!e) { e = window.event; }
+ if (this._doPaste(e)) {
+ if (isIE) {
+ /*
+ * Bug in IE,
+ */
+ var self = this;
+ this._ignoreFocus = true;
+ setTimeout(function() {
+ self._updateDOMSelection();
+ this._ignoreFocus = false;
+ }, 0);
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleResize: function (e) {
+ if (!e) { e = window.event; }
+ var element = this._frameDocument.documentElement;
+ var newWidth = element.clientWidth;
+ var newHeight = element.clientHeight;
+ if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) {
+ this._frameWidth = newWidth;
+ this._frameHeight = newHeight;
+ /*
+ * Feature in IE7. For some reason, sometimes Internet Explorer 7
+ * returns incorrect values for element.getBoundingClientRect() when
+ * inside a resize handler. The fix is to queue the work.
+ */
+ if (isIE < 9) {
+ this._queueUpdatePage();
+ } else {
+ this._updatePage();
+ }
+ }
+ },
+ _handleRulerEvent: function (e) {
+ if (!e) { e = window.event; }
+ var target = e.target ? e.target : e.srcElement;
+ var lineIndex = target.lineIndex;
+ var element = target;
+ while (element && !element._ruler) {
+ if (lineIndex === undefined && element.lineIndex !== undefined) {
+ lineIndex = element.lineIndex;
+ }
+ element = element.parentNode;
+ }
+ var ruler = element ? element._ruler : null;
+ if (lineIndex === undefined && ruler && ruler.getOverview() === "document") {
+ var buttonHeight = isPad ? 0 : 17;
+ var clientHeight = this._getClientHeight ();
+ var lineCount = this._model.getLineCount ();
+ var viewPad = this._getViewPadding();
+ var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
+ lineIndex = Math.floor((e.clientY - buttonHeight) * lineCount / trackHeight);
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ lineIndex = undefined;
+ }
+ }
+ if (ruler) {
+ switch (e.type) {
+ case "click":
+ if (ruler.onClick) { ruler.onClick(lineIndex, e); }
+ break;
+ case "dblclick":
+ if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); }
+ break;
+ case "mousemove":
+ if (ruler.onMouseMove) { ruler.onMouseMove(lineIndex, e); }
+ break;
+ case "mouseover":
+ if (ruler.onMouseOver) { ruler.onMouseOver(lineIndex, e); }
+ break;
+ case "mouseout":
+ if (ruler.onMouseOut) { ruler.onMouseOut(lineIndex, e); }
+ break;
+ }
+ }
+ },
+ _handleScroll: function () {
+ var scroll = this._getScroll();
+ var oldX = this._hScroll;
+ var oldY = this._vScroll;
+ if (oldX !== scroll.x || oldY !== scroll.y) {
+ this._hScroll = scroll.x;
+ this._vScroll = scroll.y;
+ this._commitIME();
+ this._updatePage(oldY === scroll.y);
+ var e = {
+ type: "Scroll",
+ oldValue: {x: oldX, y: oldY},
+ newValue: scroll
+ };
+ this.onScroll(e);
+ }
+ },
+ _handleSelectStart: function (e) {
+ if (!e) { e = window.event; }
+ if (this._ignoreSelect) {
+ if (e && e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleUnload: function (e) {
+ if (!e) { e = window.event; }
+ this._destroyView();
+ },
+ _handleInput: function (e) {
+ var textArea = this._textArea;
+ this._doContent(textArea.value);
+ textArea.selectionStart = textArea.selectionEnd = 0;
+ textArea.value = "";
+ e.preventDefault();
+ },
+ _handleTextInput: function (e) {
+ this._doContent(e.data);
+ e.preventDefault();
+ },
+ _touchConvert: function (touch) {
+ var rect = this._frame.getBoundingClientRect();
+ var body = this._parentDocument.body;
+ return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop};
+ },
+ _handleTextAreaClick: function (e) {
+ var pt = this._touchConvert(e);
+ this._clickCount = 1;
+ this._ignoreDOMSelection = false;
+ this._setSelectionTo(pt.left, pt.top, false);
+ var textArea = this._textArea;
+ textArea.focus();
+ },
+ _handleTouchStart: function (e) {
+ var touches = e.touches, touch, pt, sel;
+ this._touchMoved = false;
+ this._touchStartScroll = undefined;
+ if (touches.length === 1) {
+ touch = touches[0];
+ var pageX = touch.pageX;
+ var pageY = touch.pageY;
+ this._touchStartX = pageX;
+ this._touchStartY = pageY;
+ this._touchStartTime = e.timeStamp;
+ this._touchStartScroll = this._getScroll();
+ sel = this._getSelection();
+ pt = this._touchConvert(touches[0]);
+ this._touchGesture = "none";
+ if (!sel.isEmpty()) {
+ if (this._hitOffset(sel.end, pt.left, pt.top)) {
+ this._touchGesture = "extendEnd";
+ } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
+ this._touchGesture = "extendStart";
+ }
+ }
+ if (this._touchGesture === "none") {
+ var textArea = this._textArea;
+ textArea.value = "";
+ textArea.style.left = "-1000px";
+ textArea.style.top = "-1000px";
+ textArea.style.width = "3000px";
+ textArea.style.height = "3000px";
+ }
+ } else if (touches.length === 2) {
+ this._touchGesture = "select";
+ if (this._touchTimeout) {
+ clearTimeout(this._touchTimeout);
+ this._touchTimeout = null;
+ }
+ pt = this._touchConvert(touches[0]);
+ var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ pt = this._touchConvert(touches[1]);
+ var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ sel = this._getSelection();
+ sel.setCaret(offset1);
+ sel.extend(offset2);
+ this._setSelection(sel, true, true);
+ }
+ //Cannot prevent to show magnifier
+// e.preventDefault();
+ },
+ _handleTouchMove: function (e) {
+ this._touchMoved = true;
+ var touches = e.touches, pt, sel;
+ if (touches.length === 1) {
+ var touch = touches[0];
+ var pageX = touch.pageX;
+ var pageY = touch.pageY;
+ var deltaX = this._touchStartX - pageX;
+ var deltaY = this._touchStartY - pageY;
+ pt = this._touchConvert(touch);
+ sel = this._getSelection();
+ if (this._touchGesture === "none") {
+ if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
+ this._touchGesture = "scroll";
+ } else {
+ this._touchGesture = "caret";
+ }
+ }
+ if (this._touchGesture === "select") {
+ if (this._hitOffset(sel.end, pt.left, pt.top)) {
+ this._touchGesture = "extendEnd";
+ } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
+ this._touchGesture = "extendStart";
+ } else {
+ this._touchGesture = "caret";
+ }
+ }
+ switch (this._touchGesture) {
+ case "scroll":
+ this._touchStartX = pageX;
+ this._touchStartY = pageY;
+ this._scrollView(deltaX, deltaY);
+ break;
+ case "extendStart":
+ case "extendEnd":
+ this._clickCount = 1;
+ var lineIndex = this._getYToLine(pt.top);
+ var offset = this._getXToOffset(lineIndex, pt.left);
+ sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start);
+ sel.extend(offset);
+ if (offset >= sel.end && this._touchGesture === "extendStart") {
+ this._touchGesture = "extendEnd";
+ }
+ if (offset <= sel.start && this._touchGesture === "extendEnd") {
+ this._touchGesture = "extendStart";
+ }
+ this._setSelection(sel, true, true);
+ break;
+ case "caret":
+ this._setSelectionTo(pt.left, pt.top, false);
+ break;
+ }
+ } else if (touches.length === 2) {
+ pt = this._touchConvert(touches[0]);
+ var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ pt = this._touchConvert(touches[1]);
+ var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ sel = this._getSelection();
+ sel.setCaret(offset1);
+ sel.extend(offset2);
+ this._setSelection(sel, true, true);
+ }
+ e.preventDefault();
+ },
+ _handleTouchEnd: function (e) {
+ var self = this;
+ if (!this._touchMoved) {
+ if (e.touches.length === 0 && e.changedTouches.length === 1) {
+ var touch = e.changedTouches[0];
+ var pt = this._touchConvert(touch);
+ var textArea = this._textArea;
+ textArea.value = "";
+ textArea.style.left = "-1000px";
+ textArea.style.top = "-1000px";
+ textArea.style.width = "3000px";
+ textArea.style.height = "3000px";
+ setTimeout(function() {
+ self._clickCount = 1;
+ self._ignoreDOMSelection = false;
+ self._setSelectionTo(pt.left, pt.top, false);
+ }, 300);
+ }
+ }
+ if (e.touches.length === 0) {
+ setTimeout(function() {
+ var selection = self._getSelection();
+ var text = self._model.getText(selection.start, selection.end);
+ var textArea = self._textArea;
+ textArea.value = text;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = text.length;
+ if (!selection.isEmpty()) {
+ var touchRect = self._touchDiv.getBoundingClientRect();
+ var bounds = self._getOffsetBounds(selection.start);
+ textArea.style.left = (touchRect.width / 2) + "px";
+ textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px";
+ }
+ }, 0);
+ }
+// e.preventDefault();
+ },
+
+ /************************************ Actions ******************************************/
+ _doAction: function (e) {
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ var kb = keyBindings[i];
+ if (kb.keyBinding.match(e)) {
+ if (kb.name) {
+ var actions = this._actions;
+ for (var j = 0; j < actions.length; j++) {
+ var a = actions[j];
+ if (a.name === kb.name) {
+ if (a.userHandler) {
+ if (!a.userHandler()) {
+ if (a.defaultHandler) {
+ a.defaultHandler();
+ } else {
+ return false;
+ }
+ }
+ } else if (a.defaultHandler) {
+ a.defaultHandler();
+ }
+ break;
+ }
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ },
+ _doBackspace: function (args) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) {
+ var model = this._model;
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ var lineStart = model.getLineStart(lineIndex);
+ if (caret === lineStart) {
+ if (lineIndex > 0) {
+ selection.extend(model.getLineEnd(lineIndex - 1));
+ }
+ } else {
+ var removeTab = false;
+ if (this._expandTab && args.unit === "character" && (caret - lineStart) % this._tabSize === 0) {
+ var lineText = model.getText(lineStart, caret);
+ removeTab = !/[^ ]/.test(lineText); // Only spaces between line start and caret.
+ }
+ if (removeTab) {
+ selection.extend(caret - this._tabSize);
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, -1));
+ }
+ }
+ }
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ return true;
+ },
+ _doContent: function (text) {
+ var selection = this._getSelection();
+ this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true);
+ },
+ _doCopy: function (e) {
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ var text = this._getBaseText(selection.start, selection.end);
+ return this._setClipboardText(text, e);
+ }
+ return true;
+ },
+ _doCursorNext: function (args) {
+ if (!args.select) {
+ if (this._clearSelection("next")) { return true; }
+ }
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineEnd(lineIndex)) {
+ if (lineIndex + 1 < model.getLineCount()) {
+ selection.extend(model.getLineStart(lineIndex + 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, 1));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doCursorPrevious: function (args) {
+ if (!args.select) {
+ if (this._clearSelection("previous")) { return true; }
+ }
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineStart(lineIndex)) {
+ if (lineIndex > 0) {
+ selection.extend(model.getLineEnd(lineIndex - 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, -1));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doCut: function (e) {
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ var text = this._getBaseText(selection.start, selection.end);
+ this._doContent("");
+ return this._setClipboardText(text, e);
+ }
+ return true;
+ },
+ _doDelete: function (args) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) {
+ var model = this._model;
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineEnd (lineIndex)) {
+ if (lineIndex + 1 < model.getLineCount()) {
+ selection.extend(model.getLineStart(lineIndex + 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, 1));
+ }
+ }
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ return true;
+ },
+ _doEnd: function (args) {
+ var selection = this._getSelection();
+ var model = this._model;
+ if (args.ctrl) {
+ selection.extend(model.getCharCount());
+ } else {
+ var lineIndex = model.getLineAtOffset(selection.getCaret());
+ selection.extend(model.getLineEnd(lineIndex));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doEnter: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ this._doContent(model.getLineDelimiter());
+ if (args && args.noCursor) {
+ selection.end = selection.start;
+ this._setSelection(selection);
+ }
+ return true;
+ },
+ _doHome: function (args) {
+ var selection = this._getSelection();
+ var model = this._model;
+ if (args.ctrl) {
+ selection.extend(0);
+ } else {
+ var lineIndex = model.getLineAtOffset(selection.getCaret());
+ selection.extend(model.getLineStart(lineIndex));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doLineDown: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (lineIndex + 1 < model.getLineCount()) {
+ var scrollX = this._getScroll().x;
+ var x = this._columnX;
+ if (x === -1 || args.wholeLine || (args.select && isIE)) {
+ var offset = args.wholeLine ? model.getLineEnd(lineIndex + 1) : caret;
+ x = this._getOffsetToX(offset) + scrollX;
+ }
+ selection.extend(this._getXToOffset(lineIndex + 1, x - scrollX));
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true, true);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doLineUp: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (lineIndex > 0) {
+ var scrollX = this._getScroll().x;
+ var x = this._columnX;
+ if (x === -1 || args.wholeLine || (args.select && isIE)) {
+ var offset = args.wholeLine ? model.getLineStart(lineIndex - 1) : caret;
+ x = this._getOffsetToX(offset) + scrollX;
+ }
+ selection.extend(this._getXToOffset(lineIndex - 1, x - scrollX));
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true, true);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPageDown: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var caretLine = model.getLineAtOffset(caret);
+ var lineCount = model.getLineCount();
+ if (caretLine < lineCount - 1) {
+ var scroll = this._getScroll();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var lines = Math.floor(clientHeight / lineHeight);
+ var scrollLines = Math.min(lineCount - caretLine - 1, lines);
+ scrollLines = Math.max(1, scrollLines);
+ var x = this._columnX;
+ if (x === -1 || (args.select && isIE)) {
+ x = this._getOffsetToX(caret) + scroll.x;
+ }
+ selection.extend(this._getXToOffset(caretLine + scrollLines, x - scroll.x));
+ if (!args.select) { selection.collapse(); }
+ var verticalMaximum = lineCount * lineHeight;
+ var scrollOffset = scroll.y + scrollLines * lineHeight;
+ if (scrollOffset + clientHeight > verticalMaximum) {
+ scrollOffset = verticalMaximum - clientHeight;
+ }
+ this._setSelection(selection, true, true, scrollOffset - scroll.y);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPageUp: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var caretLine = model.getLineAtOffset(caret);
+ if (caretLine > 0) {
+ var scroll = this._getScroll();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var lines = Math.floor(clientHeight / lineHeight);
+ var scrollLines = Math.max(1, Math.min(caretLine, lines));
+ var x = this._columnX;
+ if (x === -1 || (args.select && isIE)) {
+ x = this._getOffsetToX(caret) + scroll.x;
+ }
+ selection.extend(this._getXToOffset(caretLine - scrollLines, x - scroll.x));
+ if (!args.select) { selection.collapse(); }
+ var scrollOffset = Math.max(0, scroll.y - scrollLines * lineHeight);
+ this._setSelection(selection, true, true, scrollOffset - scroll.y);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPaste: function(e) {
+ var self = this;
+ var result = this._getClipboardText(e, function(text) {
+ if (text) {
+ if (isLinux && self._lastMouseButton === 2) {
+ var timeDiff = new Date().getTime() - self._lastMouseTime;
+ if (timeDiff <= self._clickTime) {
+ self._setSelectionTo(self._lastMouseX, self._lastMouseY);
+ }
+ }
+ self._doContent(text);
+ }
+ });
+ return result !== null;
+ },
+ _doScroll: function (args) {
+ var type = args.type;
+ var model = this._model;
+ var lineCount = model.getLineCount();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var verticalMaximum = lineCount * lineHeight;
+ var verticalScrollOffset = this._getScroll().y;
+ var pixel;
+ switch (type) {
+ case "textStart": pixel = 0; break;
+ case "textEnd": pixel = verticalMaximum - clientHeight; break;
+ case "pageDown": pixel = verticalScrollOffset + clientHeight; break;
+ case "pageUp": pixel = verticalScrollOffset - clientHeight; break;
+ case "centerLine":
+ var selection = this._getSelection();
+ var lineStart = model.getLineAtOffset(selection.start);
+ var lineEnd = model.getLineAtOffset(selection.end);
+ var selectionHeight = (lineEnd - lineStart + 1) * lineHeight;
+ pixel = (lineStart * lineHeight) - (clientHeight / 2) + (selectionHeight / 2);
+ break;
+ }
+ if (pixel !== undefined) {
+ pixel = Math.min(Math.max(0, pixel), verticalMaximum - clientHeight);
+ this._scrollView(0, pixel - verticalScrollOffset);
+ }
+ },
+ _doSelectAll: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ selection.setCaret(0);
+ selection.extend(model.getCharCount());
+ this._setSelection(selection, false);
+ return true;
+ },
+ _doTab: function (args) {
+ var text = "\t";
+ if (this._expandTab) {
+ var model = this._model;
+ var caret = this._getSelection().getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ var lineStart = model.getLineStart(lineIndex);
+ var spaces = this._tabSize - ((caret - lineStart) % this._tabSize);
+ text = (new Array(spaces + 1)).join(" ");
+ }
+ this._doContent(text);
+ return true;
+ },
+
+ /************************************ Internals ******************************************/
+ _applyStyle: function(style, node, reset) {
+ if (reset) {
+ var attrs = node.attributes;
+ for (var i= attrs.length; i-->0;) {
+ if (attrs[i].specified) {
+ node.removeAttributeNode(attrs[i]);
+ }
+ }
+ }
+ if (!style) {
+ return;
+ }
+ if (style.styleClass) {
+ node.className = style.styleClass;
+ }
+ var properties = style.style;
+ if (properties) {
+ for (var s in properties) {
+ if (properties.hasOwnProperty(s)) {
+ node.style[s] = properties[s];
+ }
+ }
+ }
+ var attributes = style.attributes;
+ if (attributes) {
+ for (var a in attributes) {
+ if (attributes.hasOwnProperty(a)) {
+ node.setAttribute(a, attributes[a]);
+ }
+ }
+ }
+ },
+ _autoScroll: function () {
+ var selection = this._getSelection();
+ var line;
+ var x = this._autoScrollX;
+ if (this._autoScrollDir === "up" || this._autoScrollDir === "down") {
+ var scroll = this._autoScrollY / this._getLineHeight();
+ scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll);
+ line = this._model.getLineAtOffset(selection.getCaret());
+ line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll));
+ } else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") {
+ line = this._getYToLine(this._autoScrollY);
+ x += this._getOffsetToX(selection.getCaret());
+ }
+ selection.extend(this._getXToOffset(line, x));
+ this._setSelection(selection, true);
+ },
+ _autoScrollTimer: function () {
+ this._autoScroll();
+ var self = this;
+ this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE);
+ },
+ _calculateLineHeight: function() {
+ var parent = this._clientDiv;
+ var document = this._frameDocument;
+ var c = " ";
+ var line = document.createElement("DIV");
+ line.style.position = "fixed";
+ line.style.left = "-1000px";
+ var span1 = document.createElement("SPAN");
+ span1.appendChild(document.createTextNode(c));
+ line.appendChild(span1);
+ var span2 = document.createElement("SPAN");
+ span2.style.fontStyle = "italic";
+ span2.appendChild(document.createTextNode(c));
+ line.appendChild(span2);
+ var span3 = document.createElement("SPAN");
+ span3.style.fontWeight = "bold";
+ span3.appendChild(document.createTextNode(c));
+ line.appendChild(span3);
+ var span4 = document.createElement("SPAN");
+ span4.style.fontWeight = "bold";
+ span4.style.fontStyle = "italic";
+ span4.appendChild(document.createTextNode(c));
+ line.appendChild(span4);
+ parent.appendChild(line);
+ var lineRect = line.getBoundingClientRect();
+ var spanRect1 = span1.getBoundingClientRect();
+ var spanRect2 = span2.getBoundingClientRect();
+ var spanRect3 = span3.getBoundingClientRect();
+ var spanRect4 = span4.getBoundingClientRect();
+ var h1 = spanRect1.bottom - spanRect1.top;
+ var h2 = spanRect2.bottom - spanRect2.top;
+ var h3 = spanRect3.bottom - spanRect3.top;
+ var h4 = spanRect4.bottom - spanRect4.top;
+ var fontStyle = 0;
+ var lineHeight = lineRect.bottom - lineRect.top;
+ if (h2 > h1) {
+ fontStyle = 1;
+ }
+ if (h3 > h2) {
+ fontStyle = 2;
+ }
+ if (h4 > h3) {
+ fontStyle = 3;
+ }
+ var style;
+ if (fontStyle !== 0) {
+ style = {style: {}};
+ if ((fontStyle & 1) !== 0) {
+ style.style.fontStyle = "italic";
+ }
+ if ((fontStyle & 2) !== 0) {
+ style.style.fontWeight = "bold";
+ }
+ }
+ this._largestFontStyle = style;
+ parent.removeChild(line);
+ return lineHeight;
+ },
+ _calculatePadding: function() {
+ var document = this._frameDocument;
+ var parent = this._clientDiv;
+ var pad = this._getPadding(this._viewDiv);
+ var div1 = document.createElement("DIV");
+ div1.style.position = "fixed";
+ div1.style.left = "-1000px";
+ div1.style.paddingLeft = pad.left + "px";
+ div1.style.paddingTop = pad.top + "px";
+ div1.style.paddingRight = pad.right + "px";
+ div1.style.paddingBottom = pad.bottom + "px";
+ div1.style.width = "100px";
+ div1.style.height = "100px";
+ var div2 = document.createElement("DIV");
+ div2.style.width = "100%";
+ div2.style.height = "100%";
+ div1.appendChild(div2);
+ parent.appendChild(div1);
+ var rect1 = div1.getBoundingClientRect();
+ var rect2 = div2.getBoundingClientRect();
+ parent.removeChild(div1);
+ pad = {
+ left: rect2.left - rect1.left,
+ top: rect2.top - rect1.top,
+ right: rect1.right - rect2.right,
+ bottom: rect1.bottom - rect2.bottom
+ };
+ return pad;
+ },
+ _clearSelection: function (direction) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) { return false; }
+ if (direction === "next") {
+ selection.start = selection.end;
+ } else {
+ selection.end = selection.start;
+ }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _clone: function (obj) {
+ /*Note that this code only works because of the limited types used in TextViewOptions */
+ if (obj instanceof Array) {
+ return obj.slice(0);
+ }
+ return obj;
+ },
+ _compare: function (s1, s2) {
+ if (s1 === s2) { return true; }
+ if (s1 && !s2 || !s1 && s2) { return false; }
+ if ((s1 && s1.constructor === String) || (s2 && s2.constructor === String)) { return false; }
+ if (s1 instanceof Array || s2 instanceof Array) {
+ if (!(s1 instanceof Array && s2 instanceof Array)) { return false; }
+ if (s1.length !== s2.length) { return false; }
+ for (var i = 0; i < s1.length; i++) {
+ if (!this._compare(s1[i], s2[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (!(s1 instanceof Object) || !(s2 instanceof Object)) { return false; }
+ var p;
+ for (p in s1) {
+ if (s1.hasOwnProperty(p)) {
+ if (!s2.hasOwnProperty(p)) { return false; }
+ if (!this._compare(s1[p], s2[p])) {return false; }
+ }
+ }
+ for (p in s2) {
+ if (!s1.hasOwnProperty(p)) { return false; }
+ }
+ return true;
+ },
+ _commitIME: function () {
+ if (this._imeOffset === -1) { return; }
+ // make the state of the IME match the state the view expects it be in
+ // when the view commits the text and IME also need to be committed
+ // this can be accomplished by changing the focus around
+ this._scrollDiv.focus();
+ this._clientDiv.focus();
+
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(this._imeOffset);
+ var lineStart = model.getLineStart(lineIndex);
+ var newText = this._getDOMText(lineIndex);
+ var oldText = model.getLine(lineIndex);
+ var start = this._imeOffset - lineStart;
+ var end = start + newText.length - oldText.length;
+ if (start !== end) {
+ var insertText = newText.substring(start, end);
+ this._doContent(insertText);
+ }
+ this._imeOffset = -1;
+ },
+ _convertDelimiter: function (text, addTextFunc, addDelimiterFunc) {
+ var cr = 0, lf = 0, index = 0, length = text.length;
+ while (index < length) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ var start = index, end;
+ if (lf === -1 && cr === -1) {
+ addTextFunc(text.substring(index));
+ break;
+ }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ end = cr;
+ index = lf + 1;
+ } else {
+ end = cr < lf ? cr : lf;
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ end = cr;
+ index = cr + 1;
+ } else {
+ end = lf;
+ index = lf + 1;
+ }
+ addTextFunc(text.substring(start, end));
+ addDelimiterFunc();
+ }
+ },
+ _createActions: function () {
+ var KeyBinding = mKeyBinding.KeyBinding;
+ //no duplicate keybindings
+ var bindings = this._keyBindings = [];
+
+ // Cursor Navigation
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true});
+ bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true});
+ bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true});
+ if (isMac) {
+ bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(33), predefined: true});
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(34), predefined: true});
+ bindings.push({name: "pageUp", keyBinding: new KeyBinding(33, null, null, true), predefined: true});
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding(34, null, null, true), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true});
+ bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true});
+ bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true});
+ bindings.push({name: "scrollTextStart", keyBinding: new KeyBinding(36), predefined: true});
+ bindings.push({name: "scrollTextEnd", keyBinding: new KeyBinding(35), predefined: true});
+ bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true});
+ bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true});
+ bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(38, null, null, null, true), predefined: true});
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(40, null, null, null, true), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, null, null, null, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, null, null, null, true), predefined: true});
+ //TODO These two actions should be changed to paragraph start and paragraph end when word wrap is implemented
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(38, null, null, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(40, null, null, true), predefined: true});
+ } else {
+ bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true});
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true});
+ bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true});
+ bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true});
+ bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true});
+ bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true});
+ }
+ if (isFirefox && isLinux) {
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding(38, true), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding(40, true), predefined: true});
+ }
+
+ // Select Cursor Navigation
+ bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true});
+ bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true});
+ bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true});
+ bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true});
+ bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true});
+ bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true});
+ if (isMac) {
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true});
+ bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true});
+ bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true});
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, null, true, null, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, null, true, null, true), predefined: true});
+ //TODO These two actions should be changed to select paragraph start and select paragraph end when word wrap is implemented
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(38, null, true, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(40, null, true, true), predefined: true});
+ } else {
+ if (isLinux) {
+ bindings.push({name: "selectWholeLineUp", keyBinding: new KeyBinding(38, true, true), predefined: true});
+ bindings.push({name: "selectWholeLineDown", keyBinding: new KeyBinding(40, true, true), predefined: true});
+ }
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
+ bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true});
+ bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true});
+ }
+
+ //Misc
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true});
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true});
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true});
+ bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true});
+ bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true});
+ bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true});
+ bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true});
+ bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true});
+ if (isMac) {
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true});
+ bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true});
+ }
+
+ /*
+ * Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text.
+ *
+ * Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd.
+ */
+ if (!isFirefox) {
+ var isMacChrome = isMac && isChrome;
+ bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true});
+ bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true});
+ bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true});
+ }
+
+ if (isFirefox) {
+ bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true});
+ bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true});
+ bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true});
+ }
+
+ // Add the emacs Control+ ... key bindings.
+ if (isMac) {
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding("a", false, false, false, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding("e", false, false, false, true), predefined: true});
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding("p", false, false, false, true), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding("n", false, false, false, true), predefined: true});
+ bindings.push({name: "charPrevious", keyBinding: new KeyBinding("b", false, false, false, true), predefined: true});
+ bindings.push({name: "charNext", keyBinding: new KeyBinding("f", false, false, false, true), predefined: true});
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding("h", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding("d", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteLineEnd", keyBinding: new KeyBinding("k", false, false, false, true), predefined: true});
+ if (isFirefox) {
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteLineStart", keyBinding: new KeyBinding("u", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding("w", false, false, false, true), predefined: true});
+ } else {
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
+ bindings.push({name: "centerLine", keyBinding: new KeyBinding("l", false, false, false, true), predefined: true});
+ bindings.push({name: "enterNoCursor", keyBinding: new KeyBinding("o", false, false, false, true), predefined: true});
+ //TODO implement: y (yank), t (transpose)
+ }
+ }
+
+ //1 to 1, no duplicates
+ var self = this;
+ this._actions = [
+ {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}},
+ {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}},
+ {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}},
+ {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}},
+ {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}},
+ {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}},
+ {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}},
+ {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}},
+ {name: "scrollPageUp", defaultHandler: function() {return self._doScroll({type: "pageUp"});}},
+ {name: "scrollPageDown", defaultHandler: function() {return self._doScroll({type: "pageDown"});}},
+ {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}},
+ {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}},
+ {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}},
+ {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}},
+ {name: "scrollTextStart", defaultHandler: function() {return self._doScroll({type: "textStart"});}},
+ {name: "scrollTextEnd", defaultHandler: function() {return self._doScroll({type: "textEnd"});}},
+ {name: "centerLine", defaultHandler: function() {return self._doScroll({type: "centerLine"});}},
+
+ {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}},
+ {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}},
+ {name: "selectWholeLineUp", defaultHandler: function() {return self._doLineUp({select: true, wholeLine: true});}},
+ {name: "selectWholeLineDown", defaultHandler: function() {return self._doLineDown({select: true, wholeLine: true});}},
+ {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}},
+ {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}},
+ {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}},
+ {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}},
+ {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}},
+ {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}},
+ {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}},
+ {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}},
+ {name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}},
+ {name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}},
+
+ {name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}},
+ {name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}},
+ {name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}},
+ {name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}},
+ {name: "deleteLineStart", defaultHandler: function() {return self._doBackspace({unit: "line"});}},
+ {name: "deleteLineEnd", defaultHandler: function() {return self._doDelete({unit: "line"});}},
+ {name: "tab", defaultHandler: function() {return self._doTab();}},
+ {name: "enter", defaultHandler: function() {return self._doEnter();}},
+ {name: "enterNoCursor", defaultHandler: function() {return self._doEnter({noCursor:true});}},
+ {name: "selectAll", defaultHandler: function() {return self._doSelectAll();}},
+ {name: "copy", defaultHandler: function() {return self._doCopy();}},
+ {name: "cut", defaultHandler: function() {return self._doCut();}},
+ {name: "paste", defaultHandler: function() {return self._doPaste();}}
+ ];
+ },
+ _createLine: function(parent, div, document, lineIndex, model) {
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var e = {type:"LineStyle", textView: this, lineIndex: lineIndex, lineText: lineText, lineStart: lineStart};
+ this.onLineStyle(e);
+ var lineDiv = div || document.createElement("DIV");
+ if (!div || !this._compare(div.viewStyle, e.style)) {
+ this._applyStyle(e.style, lineDiv, div);
+ lineDiv.viewStyle = e.style;
+ }
+ lineDiv.lineIndex = lineIndex;
+ var ranges = [];
+ var data = {tabOffset: 0, ranges: ranges};
+ this._createRanges(e.ranges, lineText, 0, lineText.length, lineStart, data);
+
+ /*
+ * A trailing span with a whitespace is added for three different reasons:
+ * 1. Make sure the height of each line is the largest of the default font
+ * in normal, italic, bold, and italic-bold.
+ * 2. When full selection is off, Firefox, Opera and IE9 do not extend the
+ * selection at the end of the line when the line is fully selected.
+ * 3. The height of a div with only an empty span is zero.
+ */
+ var c = " ";
+ if (!this._fullSelection && isIE < 9) {
+ /*
+ * IE8 already selects extra space at end of a line fully selected,
+ * adding another space at the end of the line causes the selection
+ * to look too big. The fix is to use a zero-width space (\uFEFF) instead.
+ */
+ c = "\uFEFF";
+ }
+ if (isWebkit) {
+ /*
+ * Feature in WekKit. Adding a regular white space to the line will
+ * cause the longest line in the view to wrap even though "pre" is set.
+ * The fix is to use the zero-width non-joiner character (\u200C) instead.
+ * Note: To not use \uFEFF because in old version of Chrome this character
+ * shows a glyph;
+ */
+ c = "\u200C";
+ }
+ ranges.push({text: c, style: this._largestFontStyle, ignoreChars: 1});
+
+ var range, span, style, oldSpan, oldStyle, text, oldText, end = 0, oldEnd = 0, next;
+ var changeCount, changeStart;
+ if (div) {
+ var modelChangedEvent = div.modelChangedEvent;
+ if (modelChangedEvent) {
+ if (modelChangedEvent.removedLineCount === 0 && modelChangedEvent.addedLineCount === 0) {
+ changeStart = modelChangedEvent.start - lineStart;
+ changeCount = modelChangedEvent.addedCharCount - modelChangedEvent.removedCharCount;
+ } else {
+ changeStart = -1;
+ }
+ div.modelChangedEvent = undefined;
+ }
+ oldSpan = div.firstChild;
+ }
+ for (var i = 0; i < ranges.length; i++) {
+ range = ranges[i];
+ text = range.text;
+ end += text.length;
+ style = range.style;
+ if (oldSpan) {
+ oldText = oldSpan.firstChild.data;
+ oldStyle = oldSpan.viewStyle;
+ if (oldText === text && this._compare(style, oldStyle)) {
+ oldEnd += oldText.length;
+ oldSpan._rectsCache = undefined;
+ span = oldSpan = oldSpan.nextSibling;
+ continue;
+ } else {
+ while (oldSpan) {
+ if (changeStart !== -1) {
+ var spanEnd = end;
+ if (spanEnd >= changeStart) {
+ spanEnd -= changeCount;
+ }
+ var length = oldSpan.firstChild.data.length;
+ if (oldEnd + length > spanEnd) { break; }
+ oldEnd += length;
+ }
+ next = oldSpan.nextSibling;
+ lineDiv.removeChild(oldSpan);
+ oldSpan = next;
+ }
+ }
+ }
+ span = this._createSpan(lineDiv, document, text, style, range.ignoreChars);
+ if (oldSpan) {
+ lineDiv.insertBefore(span, oldSpan);
+ } else {
+ lineDiv.appendChild(span);
+ }
+ if (div) {
+ div.lineWidth = undefined;
+ }
+ }
+ if (div) {
+ var tmp = span ? span.nextSibling : null;
+ while (tmp) {
+ next = tmp.nextSibling;
+ div.removeChild(tmp);
+ tmp = next;
+ }
+ } else {
+ parent.appendChild(lineDiv);
+ }
+ return lineDiv;
+ },
+ _createRanges: function(ranges, text, start, end, lineStart, data) {
+ if (start >= end) { return; }
+ if (ranges) {
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.end <= lineStart + start) { continue; }
+ var styleStart = Math.max(lineStart + start, range.start) - lineStart;
+ if (styleStart >= end) { break; }
+ var styleEnd = Math.min(lineStart + end, range.end) - lineStart;
+ if (styleStart < styleEnd) {
+ styleStart = Math.max(start, styleStart);
+ styleEnd = Math.min(end, styleEnd);
+ if (start < styleStart) {
+ this._createRange(text, start, styleStart, null, data);
+ }
+ while (i + 1 < ranges.length && ranges[i + 1].start - lineStart === styleEnd && this._compare(range.style, ranges[i + 1].style)) {
+ range = ranges[i + 1];
+ styleEnd = Math.min(lineStart + end, range.end) - lineStart;
+ i++;
+ }
+ this._createRange(text, styleStart, styleEnd, range.style, data);
+ start = styleEnd;
+ }
+ }
+ }
+ if (start < end) {
+ this._createRange(text, start, end, null, data);
+ }
+ },
+ _createRange: function(text, start, end, style, data) {
+ if (start >= end) { return; }
+ var tabSize = this._customTabSize, range;
+ if (tabSize && tabSize !== 8) {
+ var tabIndex = text.indexOf("\t", start);
+ while (tabIndex !== -1 && tabIndex < end) {
+ if (start < tabIndex) {
+ range = {text: text.substring(start, tabIndex), style: style};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ var spacesCount = tabSize - (data.tabOffset % tabSize);
+ if (spacesCount > 0) {
+ //TODO hack to preserve text length in getDOMText()
+ var spaces = "\u00A0";
+ for (var i = 1; i < spacesCount; i++) {
+ spaces += " ";
+ }
+ range = {text: spaces, style: style, ignoreChars: spacesCount - 1};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ start = tabIndex + 1;
+ tabIndex = text.indexOf("\t", start);
+ }
+ }
+ if (start < end) {
+ range = {text: text.substring(start, end), style: style};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ },
+ _createSpan: function(parent, document, text, style, ignoreChars) {
+ var isLink = style && style.tagName === "A";
+ if (isLink) { parent.hasLink = true; }
+ var tagName = isLink && this._linksVisible ? "A" : "SPAN";
+ var child = document.createElement(tagName);
+ child.appendChild(document.createTextNode(text));
+ this._applyStyle(style, child);
+ if (tagName === "A") {
+ var self = this;
+ addHandler(child, "click", function(e) { return self._handleLinkClick(e); }, false);
+ }
+ child.viewStyle = style;
+ if (ignoreChars) {
+ child.ignoreChars = ignoreChars;
+ }
+ return child;
+ },
+ _createRuler: function(ruler) {
+ if (!this._clientDiv) { return; }
+ var document = this._frameDocument;
+ var body = document.body;
+ var side = ruler.getLocation();
+ var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
+ if (!rulerParent) {
+ rulerParent = document.createElement("DIV");
+ rulerParent.style.overflow = "hidden";
+ rulerParent.style.MozUserSelect = "none";
+ rulerParent.style.WebkitUserSelect = "none";
+ if (isIE) {
+ rulerParent.attachEvent("onselectstart", function() {return false;});
+ }
+ rulerParent.style.position = "absolute";
+ rulerParent.style.top = "0px";
+ rulerParent.style.cursor = "default";
+ body.appendChild(rulerParent);
+ if (side === "left") {
+ this._leftDiv = rulerParent;
+ rulerParent.className = "viewLeftRuler";
+ } else {
+ this._rightDiv = rulerParent;
+ rulerParent.className = "viewRightRuler";
+ }
+ var table = document.createElement("TABLE");
+ rulerParent.appendChild(table);
+ table.cellPadding = "0px";
+ table.cellSpacing = "0px";
+ table.border = "0px";
+ table.insertRow(0);
+ var self = this;
+ addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mousemove", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mouseover", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mouseout", function(e) { self._handleRulerEvent(e); });
+ }
+ var div = document.createElement("DIV");
+ div._ruler = ruler;
+ div.rulerChanged = true;
+ div.style.position = "relative";
+ var row = rulerParent.firstChild.rows[0];
+ var index = row.cells.length;
+ var cell = row.insertCell(index);
+ cell.vAlign = "top";
+ cell.appendChild(div);
+ },
+ _createFrame: function() {
+ if (this.frame) { return; }
+ var parent = this._parent;
+ while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
+ var parentDocument = parent.ownerDocument;
+ this._parentDocument = parentDocument;
+ var frame = parentDocument.createElement("IFRAME");
+ this._frame = frame;
+ frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent
+ frame.style.border = "0px";
+ frame.style.width = "100%";
+ frame.style.height = "100%";
+ frame.scrolling = "no";
+ var self = this;
+ /*
+ * Note that it is not possible to create the contents of the frame if the
+ * parent is not connected to the document. Only create it when the load
+ * event is trigged.
+ */
+ this._loadHandler = function(e) {
+ self._handleLoad(e);
+ };
+ addHandler(frame, "load", this._loadHandler, !!isFirefox);
+ if (!isWebkit) {
+ /*
+ * Feature in IE and Firefox. It is not possible to get the style of an
+ * element if it is not layed out because one of the ancestor has
+ * style.display = none. This means that the view cannot be created in this
+ * situations, since no measuring can be performed. The fix is to listen
+ * for DOMAttrModified and create or destroy the view when the style.display
+ * attribute changes.
+ */
+ addHandler(parentDocument, "DOMAttrModified", this._attrModifiedHandler = function(e) {
+ self._handleDOMAttrModified(e);
+ });
+ }
+ parent.appendChild(frame);
+ /* create synchronously if possible */
+ if (this._sync) {
+ this._handleLoad();
+ }
+ },
+ _getFrameHTML: function() {
+ var html = [];
+ html.push("<!DOCTYPE html>");
+ html.push("<html>");
+ html.push("<head>");
+ if (isIE < 9) {
+ html.push("<meta http-equiv='X-UA-Compatible' content='IE=EmulateIE7'/>");
+ }
+ html.push("<style>");
+ html.push(".viewContainer {font-family: monospace; font-size: 10pt;}");
+ html.push(".view {padding: 1px 2px;}");
+ html.push(".viewContent {}");
+ html.push("</style>");
+ if (this._stylesheet) {
+ var stylesheet = typeof(this._stylesheet) === "string" ? [this._stylesheet] : this._stylesheet;
+ for (var i = 0; i < stylesheet.length; i++) {
+ var sheet = stylesheet[i];
+ var isLink = this._isLinkURL(sheet);
+ if (isLink && this._sync) {
+ try {
+ var objXml = new XMLHttpRequest();
+ if (objXml.overrideMimeType) {
+ objXml.overrideMimeType("text/css");
+ }
+ objXml.open("GET", sheet, false);
+ objXml.send(null);
+ sheet = objXml.responseText;
+ isLink = false;
+ } catch (e) {}
+ }
+ if (isLink) {
+ html.push("<link rel='stylesheet' type='text/css' ");
+ /*
+ * Bug in IE7. The window load event is not sent unless a load handler is added to the link node.
+ */
+ if (isIE < 9) {
+ html.push("onload='window' ");
+ }
+ html.push("href='");
+ html.push(sheet);
+ html.push("'></link>");
+ } else {
+ html.push("<style>");
+ html.push(sheet);
+ html.push("</style>");
+ }
+ }
+ }
+ /*
+ * Feature in WebKit. In WebKit, window load will not wait for the style sheets
+ * to be loaded unless there is script element after the style sheet link elements.
+ */
+ html.push("<script>");
+ html.push("var waitForStyleSheets = true;");
+ html.push("</script>");
+ html.push("</head>");
+ html.push("<body spellcheck='false'></body>");
+ html.push("</html>");
+ return html.join("");
+ },
+ _createView: function() {
+ if (this._frameDocument) { return; }
+ var frameWindow = this._frameWindow = this._frame.contentWindow;
+ var frameDocument = this._frameDocument = frameWindow.document;
+ var self = this;
+ function write() {
+ frameDocument.open("text/html", "replace");
+ frameDocument.write(self._getFrameHTML());
+ frameDocument.close();
+ self._windowLoadHandler = function(e) {
+ /*
+ * Bug in Safari. Safari sends the window load event before the
+ * style sheets are loaded. The fix is to defer creation of the
+ * contents until the document readyState changes to complete.
+ */
+ if (self._isDocumentReady()) {
+ self._createContent();
+ }
+ };
+ addHandler(frameWindow, "load", self._windowLoadHandler);
+ }
+ write();
+ if (this._sync) {
+ this._createContent();
+ } else {
+ /*
+ * Bug in Webkit. Webkit does not send the load event for the iframe window when the main page
+ * loads as a result of backward or forward navigation.
+ * The fix is to use a timer to create the content only when the document is ready.
+ */
+ this._createViewTimer = function() {
+ if (self._clientDiv) { return; }
+ if (self._isDocumentReady()) {
+ self._createContent();
+ } else {
+ setTimeout(self._createViewTimer, 10);
+ }
+ };
+ setTimeout(this._createViewTimer, 10);
+ }
+ },
+ _isDocumentReady: function() {
+ var frameDocument = this._frameDocument;
+ if (!frameDocument) { return false; }
+ if (frameDocument.readyState === "complete") {
+ return true;
+ } else if (frameDocument.readyState === "interactive" && isFirefox) {
+ /*
+ * Bug in Firefox. Firefox does not change the document ready state to complete
+ * all the time. The fix is to wait for the ready state to be "interactive" and check that
+ * all css rules are initialized.
+ */
+ var styleSheets = frameDocument.styleSheets;
+ var styleSheetCount = 1;
+ if (this._stylesheet) {
+ styleSheetCount += typeof(this._stylesheet) === "string" ? 1 : this._stylesheet.length;
+ }
+ if (styleSheetCount === styleSheets.length) {
+ var index = 0;
+ while (index < styleSheets.length) {
+ var count = 0;
+ try {
+ count = styleSheets.item(index).cssRules.length;
+ } catch (ex) {
+ /*
+ * Feature in Firefox. To determine if a stylesheet is loaded the number of css rules is used, if the
+ * stylesheet is not loaded this operation will throw an invalid access error. When a stylesheet from
+ * a different domain is loaded, accessing the css rules will result in a security exception. In this
+ * case count is set to 1 to indicate the stylesheet is loaded.
+ */
+ if (ex.code !== DOMException.INVALID_ACCESS_ERR) {
+ count = 1;
+ }
+ }
+ if (count === 0) { break; }
+ index++;
+ }
+ return index === styleSheets.length;
+ }
+ }
+ return false;
+ },
+ _createContent: function() {
+ if (this._clientDiv) { return; }
+ var parent = this._parent;
+ var parentDocument = this._parentDocument;
+ var frameDocument = this._frameDocument;
+ var body = frameDocument.body;
+ this._setThemeClass(this._themeClass, true);
+ body.style.margin = "0px";
+ body.style.borderWidth = "0px";
+ body.style.padding = "0px";
+
+ var textArea;
+ if (isPad) {
+ var touchDiv = parentDocument.createElement("DIV");
+ this._touchDiv = touchDiv;
+ touchDiv.style.position = "absolute";
+ touchDiv.style.border = "0px";
+ touchDiv.style.padding = "0px";
+ touchDiv.style.margin = "0px";
+ touchDiv.style.zIndex = "2";
+ touchDiv.style.overflow = "hidden";
+ touchDiv.style.background="transparent";
+ touchDiv.style.WebkitUserSelect = "none";
+ parent.appendChild(touchDiv);
+
+ textArea = parentDocument.createElement("TEXTAREA");
+ this._textArea = textArea;
+ textArea.style.position = "absolute";
+ textArea.style.whiteSpace = "pre";
+ textArea.style.left = "-1000px";
+ textArea.tabIndex = 1;
+ textArea.autocapitalize = "off";
+ textArea.autocorrect = "off";
+ textArea.className = "viewContainer";
+ textArea.style.background = "transparent";
+ textArea.style.color = "transparent";
+ textArea.style.border = "0px";
+ textArea.style.padding = "0px";
+ textArea.style.margin = "0px";
+ textArea.style.borderRadius = "0px";
+ textArea.style.WebkitAppearance = "none";
+ textArea.style.WebkitTapHighlightColor = "transparent";
+ touchDiv.appendChild(textArea);
+ }
+ if (isFirefox) {
+ var clipboardDiv = frameDocument.createElement("DIV");
+ this._clipboardDiv = clipboardDiv;
+ clipboardDiv.style.position = "fixed";
+ clipboardDiv.style.whiteSpace = "pre";
+ clipboardDiv.style.left = "-1000px";
+ body.appendChild(clipboardDiv);
+ }
+
+ var viewDiv = frameDocument.createElement("DIV");
+ viewDiv.className = "view";
+ this._viewDiv = viewDiv;
+ viewDiv.id = "viewDiv";
+ viewDiv.tabIndex = -1;
+ viewDiv.style.overflow = "auto";
+ viewDiv.style.position = "absolute";
+ viewDiv.style.top = "0px";
+ viewDiv.style.borderWidth = "0px";
+ viewDiv.style.margin = "0px";
+ viewDiv.style.outline = "none";
+ body.appendChild(viewDiv);
+
+ var scrollDiv = frameDocument.createElement("DIV");
+ this._scrollDiv = scrollDiv;
+ scrollDiv.id = "scrollDiv";
+ scrollDiv.style.margin = "0px";
+ scrollDiv.style.borderWidth = "0px";
+ scrollDiv.style.padding = "0px";
+ viewDiv.appendChild(scrollDiv);
+
+ if (isFirefox) {
+ var clipDiv = frameDocument.createElement("DIV");
+ this._clipDiv = clipDiv;
+ clipDiv.id = "clipDiv";
+ clipDiv.style.position = "fixed";
+ clipDiv.style.overflow = "hidden";
+ clipDiv.style.margin = "0px";
+ clipDiv.style.borderWidth = "0px";
+ clipDiv.style.padding = "0px";
+ scrollDiv.appendChild(clipDiv);
+
+ var clipScrollDiv = frameDocument.createElement("DIV");
+ this._clipScrollDiv = clipScrollDiv;
+ clipScrollDiv.id = "clipScrollDiv";
+ clipScrollDiv.style.position = "absolute";
+ clipScrollDiv.style.height = "1px";
+ clipScrollDiv.style.top = "-1000px";
+ clipDiv.appendChild(clipScrollDiv);
+ }
+
+ this._setFullSelection(this._fullSelection, true);
+
+ var clientDiv = frameDocument.createElement("DIV");
+ clientDiv.className = "viewContent";
+ this._clientDiv = clientDiv;
+ clientDiv.id = "clientDiv";
+ clientDiv.style.whiteSpace = "pre";
+ clientDiv.style.position = this._clipDiv ? "absolute" : "fixed";
+ clientDiv.style.borderWidth = "0px";
+ clientDiv.style.margin = "0px";
+ clientDiv.style.padding = "0px";
+ clientDiv.style.outline = "none";
+ clientDiv.style.zIndex = "1";
+ if (isPad) {
+ clientDiv.style.WebkitTapHighlightColor = "transparent";
+ }
+ (this._clipDiv || scrollDiv).appendChild(clientDiv);
+
+ if (isFirefox && !clientDiv.setCapture) {
+ var overlayDiv = frameDocument.createElement("DIV");
+ this._overlayDiv = overlayDiv;
+ overlayDiv.id = "overlayDiv";
+ overlayDiv.style.position = clientDiv.style.position;
+ overlayDiv.style.borderWidth = clientDiv.style.borderWidth;
+ overlayDiv.style.margin = clientDiv.style.margin;
+ overlayDiv.style.padding = clientDiv.style.padding;
+ overlayDiv.style.cursor = "text";
+ overlayDiv.style.zIndex = "2";
+ (this._clipDiv || scrollDiv).appendChild(overlayDiv);
+ }
+ if (!isPad) {
+ clientDiv.contentEditable = "true";
+ }
+ this._lineHeight = this._calculateLineHeight();
+ this._viewPadding = this._calculatePadding();
+ if (isIE) {
+ body.style.lineHeight = this._lineHeight + "px";
+ }
+ this._setTabSize(this._tabSize, true);
+ this._hookEvents();
+ var rulers = this._rulers;
+ for (var i=0; i<rulers.length; i++) {
+ this._createRuler(rulers[i]);
+ }
+ this._updatePage();
+ var h = this._hScroll, v = this._vScroll;
+ this._vScroll = this._hScroll = 0;
+ if (h > 0 || v > 0) {
+ viewDiv.scrollLeft = h;
+ viewDiv.scrollTop = v;
+ }
+ this.onLoad({type: "Load"});
+ },
+ _defaultOptions: function() {
+ return {
+ parent: {value: undefined, recreate: true, update: null},
+ model: {value: undefined, recreate: false, update: this.setModel},
+ readonly: {value: false, recreate: false, update: null},
+ fullSelection: {value: true, recreate: false, update: this._setFullSelection},
+ tabSize: {value: 8, recreate: false, update: this._setTabSize},
+ expandTab: {value: false, recreate: false, update: null},
+ stylesheet: {value: [], recreate: false, update: this._setStyleSheet},
+ themeClass: {value: undefined, recreate: false, update: this._setThemeClass},
+ sync: {value: false, recreate: false, update: null}
+ };
+ },
+ _destroyFrame: function() {
+ var frame = this._frame;
+ if (!frame) { return; }
+ if (this._loadHandler) {
+ removeHandler(frame, "load", this._loadHandler, !!isFirefox);
+ this._loadHandler = null;
+ }
+ if (this._attrModifiedHandler) {
+ removeHandler(this._parentDocument, "DOMAttrModified", this._attrModifiedHandler);
+ this._attrModifiedHandler = null;
+ }
+ frame.parentNode.removeChild(frame);
+ this._frame = null;
+ },
+ _destroyRuler: function(ruler) {
+ var side = ruler.getLocation();
+ var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
+ if (rulerParent) {
+ var row = rulerParent.firstChild.rows[0];
+ var cells = row.cells;
+ for (var index = 0; index < cells.length; index++) {
+ var cell = cells[index];
+ if (cell.firstChild._ruler === ruler) { break; }
+ }
+ if (index === cells.length) { return; }
+ row.cells[index]._ruler = undefined;
+ row.deleteCell(index);
+ }
+ },
+ _destroyView: function() {
+ var clientDiv = this._clientDiv;
+ if (!clientDiv) { return; }
+ this._setGrab(null);
+ this._unhookEvents();
+ if (this._windowLoadHandler) {
+ removeHandler(this._frameWindow, "load", this._windowLoadHandler);
+ this._windowLoadHandler = null;
+ }
+
+ /* Destroy timers */
+ if (this._autoScrollTimerID) {
+ clearTimeout(this._autoScrollTimerID);
+ this._autoScrollTimerID = null;
+ }
+ if (this._updateTimer) {
+ clearTimeout(this._updateTimer);
+ this._updateTimer = null;
+ }
+
+ /* Destroy DOM */
+ var parent = this._frameDocument.body;
+ while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
+ if (this._touchDiv) {
+ this._parent.removeChild(this._touchDiv);
+ this._touchDiv = null;
+ }
+ this._selDiv1 = null;
+ this._selDiv2 = null;
+ this._selDiv3 = null;
+ this._insertedSelRule = false;
+ this._textArea = null;
+ this._clipboardDiv = null;
+ this._scrollDiv = null;
+ this._viewDiv = null;
+ this._clipDiv = null;
+ this._clipScrollDiv = null;
+ this._clientDiv = null;
+ this._overlayDiv = null;
+ this._leftDiv = null;
+ this._rightDiv = null;
+ this._frameDocument = null;
+ this._frameWindow = null;
+ this.onUnload({type: "Unload"});
+ },
+ _doAutoScroll: function (direction, x, y) {
+ this._autoScrollDir = direction;
+ this._autoScrollX = x;
+ this._autoScrollY = y;
+ if (!this._autoScrollTimerID) {
+ this._autoScrollTimer();
+ }
+ },
+ _endAutoScroll: function () {
+ if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); }
+ this._autoScrollDir = undefined;
+ this._autoScrollTimerID = undefined;
+ },
+ _fixCaret: function() {
+ var clientDiv = this._clientDiv;
+ if (clientDiv) {
+ var hasFocus = this._hasFocus;
+ this._ignoreFocus = true;
+ if (hasFocus) { clientDiv.blur(); }
+ clientDiv.contentEditable = false;
+ clientDiv.contentEditable = true;
+ if (hasFocus) { clientDiv.focus(); }
+ this._ignoreFocus = false;
+ }
+ },
+ _getBaseText: function(start, end) {
+ var model = this._model;
+ /* This is the only case the view access the base model, alternatively the view could use a event to application to customize the text */
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ model = model.getBaseModel();
+ }
+ return model.getText(start, end);
+ },
+ _getBoundsAtOffset: function (offset) {
+ var model = this._model;
+ var document = this._frameDocument;
+ var clientDiv = this._clientDiv;
+ var lineIndex = model.getLineAtOffset(offset);
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var result = null;
+ if (offset < model.getLineEnd(lineIndex)) {
+ var lineOffset = model.getLineStart(lineIndex);
+ var lineChild = child.firstChild;
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ var nodeLength = textNode.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (lineOffset + nodeLength > offset) {
+ var index = offset - lineOffset;
+ var range;
+ if (isRangeRects) {
+ range = document.createRange();
+ range.setStart(textNode, index);
+ range.setEnd(textNode, index + 1);
+ result = range.getBoundingClientRect();
+ } else if (isIE) {
+ range = document.body.createTextRange();
+ range.moveToElementText(lineChild);
+ range.collapse();
+ range.moveEnd("character", index + 1);
+ range.moveStart("character", index);
+ result = range.getBoundingClientRect();
+ } else {
+ var text = textNode.data;
+ lineChild.removeChild(textNode);
+ lineChild.appendChild(document.createTextNode(text.substring(0, index)));
+ var span = document.createElement("SPAN");
+ span.appendChild(document.createTextNode(text.substring(index, index + 1)));
+ lineChild.appendChild(span);
+ lineChild.appendChild(document.createTextNode(text.substring(index + 1)));
+ result = span.getBoundingClientRect();
+ lineChild.innerHTML = "";
+ lineChild.appendChild(textNode);
+ if (!dummy) {
+ /*
+ * Removing the element node that holds the selection start or end
+ * causes the selection to be lost. The fix is to detect this case
+ * and restore the selection.
+ */
+ var s = this._getSelection();
+ if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) {
+ this._updateDOMSelection();
+ }
+ }
+ }
+ if (isIE) {
+ var logicalXDPI = window.screen.logicalXDPI;
+ var deviceXDPI = window.screen.deviceXDPI;
+ result.left = result.left * logicalXDPI / deviceXDPI;
+ result.right = result.right * logicalXDPI / deviceXDPI;
+ }
+ break;
+ }
+ lineOffset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ }
+ if (!result) {
+ var rect = this._getLineBoundingClientRect(child);
+ result = {left: rect.right, right: rect.right};
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return result;
+ },
+ _getBottomIndex: function (fullyVisible) {
+ var child = this._bottomChild;
+ if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
+ var rect = child.getBoundingClientRect();
+ var clientRect = this._clientDiv.getBoundingClientRect();
+ if (rect.bottom > clientRect.bottom) {
+ child = this._getLinePrevious(child) || child;
+ }
+ }
+ return child.lineIndex;
+ },
+ _getFrameHeight: function() {
+ return this._frameDocument.documentElement.clientHeight;
+ },
+ _getFrameWidth: function() {
+ return this._frameDocument.documentElement.clientWidth;
+ },
+ _getClientHeight: function() {
+ var viewPad = this._getViewPadding();
+ return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom);
+ },
+ _getClientWidth: function() {
+ var viewPad = this._getViewPadding();
+ return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right);
+ },
+ _getClipboardText: function (event, handler) {
+ var delimiter = this._model.getLineDelimiter();
+ var clipboadText, text;
+ if (this._frameWindow.clipboardData) {
+ //IE
+ clipboadText = [];
+ text = this._frameWindow.clipboardData.getData("Text");
+ this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ text = clipboadText.join("");
+ if (handler) { handler(text); }
+ return text;
+ }
+ if (isFirefox) {
+ this._ignoreFocus = true;
+ var document = this._frameDocument;
+ var clipboardDiv = this._clipboardDiv;
+ clipboardDiv.innerHTML = "<pre contenteditable=''></pre>";
+ clipboardDiv.firstChild.focus();
+ var self = this;
+ var _getText = function() {
+ var noteText = self._getTextFromElement(clipboardDiv);
+ clipboardDiv.innerHTML = "";
+ clipboadText = [];
+ self._convertDelimiter(noteText, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ return clipboadText.join("");
+ };
+
+ /* Try execCommand first. Works on firefox with clipboard permission. */
+ var result = false;
+ this._ignorePaste = true;
+
+ /* Do not try execCommand if middle-click is used, because if we do, we get the clipboard text, not the primary selection text. */
+ if (!isLinux || this._lastMouseButton !== 2) {
+ try {
+ result = document.execCommand("paste", false, null);
+ } catch (ex) {
+ /* Firefox can throw even when execCommand() works, see bug 362835. */
+ result = clipboardDiv.childNodes.length > 1 || clipboardDiv.firstChild && clipboardDiv.firstChild.childNodes.length > 0;
+ }
+ }
+ this._ignorePaste = false;
+ if (!result) {
+ /* Try native paste in DOM, works for firefox during the paste event. */
+ if (event) {
+ setTimeout(function() {
+ self.focus();
+ text = _getText();
+ if (text && handler) {
+ handler(text);
+ }
+ self._ignoreFocus = false;
+ }, 0);
+ return null;
+ } else {
+ /* no event and no clipboard permission, paste can't be performed */
+ this.focus();
+ this._ignoreFocus = false;
+ return "";
+ }
+ }
+ this.focus();
+ this._ignoreFocus = false;
+ text = _getText();
+ if (text && handler) {
+ handler(text);
+ }
+ return text;
+ }
+ //webkit
+ if (event && event.clipboardData) {
+ /*
+ * Webkit (Chrome/Safari) allows getData during the paste event
+ * Note: setData is not allowed, not even during copy/cut event
+ */
+ clipboadText = [];
+ text = event.clipboardData.getData("text/plain");
+ this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ text = clipboadText.join("");
+ if (text && handler) {
+ handler(text);
+ }
+ return text;
+ } else {
+ //TODO try paste using extension (Chrome only)
+ }
+ return "";
+ },
+ _getDOMText: function(lineIndex) {
+ var child = this._getLineNode(lineIndex);
+ var lineChild = child.firstChild;
+ var text = "";
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ while (textNode) {
+ if (lineChild.ignoreChars) {
+ for (var i = 0; i < textNode.length; i++) {
+ var ch = textNode.data.substring(i, i + 1);
+ if (ch !== " ") {
+ text += ch;
+ }
+ }
+ } else {
+ text += textNode.data;
+ }
+ textNode = textNode.nextSibling;
+ }
+ lineChild = lineChild.nextSibling;
+ }
+ return text;
+ },
+ _getTextFromElement: function(element) {
+ var document = element.ownerDocument;
+ var window = document.defaultView;
+ if (!window.getSelection) {
+ return element.innerText || element.textContent;
+ }
+
+ var newRange = document.createRange();
+ newRange.selectNode(element);
+
+ var selection = window.getSelection();
+ var oldRanges = [], i;
+ for (i = 0; i < selection.rangeCount; i++) {
+ oldRanges.push(selection.getRangeAt(i));
+ }
+
+ this._ignoreSelect = true;
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ var text = selection.toString();
+
+ selection.removeAllRanges();
+ for (i = 0; i < oldRanges.length; i++) {
+ selection.addRange(oldRanges[i]);
+ }
+
+ this._ignoreSelect = false;
+ return text;
+ },
+ _getViewPadding: function() {
+ return this._viewPadding;
+ },
+ _getLineBoundingClientRect: function (child) {
+ var rect = child.getBoundingClientRect();
+ var lastChild = child.lastChild;
+ //Remove any artificial trailing whitespace in the line
+ while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) {
+ lastChild = lastChild.previousSibling;
+ }
+ if (!lastChild) {
+ return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom};
+ }
+ var lastRect = lastChild.getBoundingClientRect();
+ return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom};
+ },
+ _getLineHeight: function() {
+ return this._lineHeight;
+ },
+ _getLineNode: function (lineIndex) {
+ var clientDiv = this._clientDiv;
+ var child = clientDiv.firstChild;
+ while (child) {
+ if (lineIndex === child.lineIndex) {
+ return child;
+ }
+ child = child.nextSibling;
+ }
+ return undefined;
+ },
+ _getLineNext: function (lineNode) {
+ var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild;
+ while (node && node.lineIndex === -1) {
+ node = node.nextSibling;
+ }
+ return node;
+ },
+ _getLinePrevious: function (lineNode) {
+ var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild;
+ while (node && node.lineIndex === -1) {
+ node = node.previousSibling;
+ }
+ return node;
+ },
+ _getOffset: function (offset, unit, direction) {
+ if (unit === "line") {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ if (direction > 0) {
+ return model.getLineEnd(lineIndex);
+ }
+ return model.getLineStart(lineIndex);
+ }
+ if (unit === "wordend") {
+ return this._getOffset_W3C(offset, unit, direction);
+ }
+ return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction);
+ },
+ _getOffset_W3C: function (offset, unit, direction) {
+ function _isPunctuation(c) {
+ return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126);
+ }
+ function _isWhitespace(c) {
+ return c === 32 || c === 9;
+ }
+ if (unit === "word" || unit === "wordend") {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ var lineLength = lineText.length;
+ var offsetInLine = offset - lineStart;
+
+
+ var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit;
+ if (direction > 0) {
+ if (offsetInLine === lineLength) { return lineEnd; }
+ c = lineText.charCodeAt(offsetInLine);
+ previousPunctuation = _isPunctuation(c);
+ previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+ offsetInLine++;
+ while (offsetInLine < lineLength) {
+ c = lineText.charCodeAt(offsetInLine);
+ punctuation = _isPunctuation(c);
+ if (unit === "wordend") {
+ if (!punctuation && previousPunctuation) { break; }
+ } else {
+ if (punctuation && !previousPunctuation) { break; }
+ }
+ letterOrDigit = !punctuation && !_isWhitespace(c);
+ if (unit === "wordend") {
+ if (!letterOrDigit && previousLetterOrDigit) { break; }
+ } else {
+ if (letterOrDigit && !previousLetterOrDigit) { break; }
+ }
+ previousLetterOrDigit = letterOrDigit;
+ previousPunctuation = punctuation;
+ offsetInLine++;
+ }
+ } else {
+ if (offsetInLine === 0) { return lineStart; }
+ offsetInLine--;
+ c = lineText.charCodeAt(offsetInLine);
+ previousPunctuation = _isPunctuation(c);
+ previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+ while (0 < offsetInLine) {
+ c = lineText.charCodeAt(offsetInLine - 1);
+ punctuation = _isPunctuation(c);
+ if (unit === "wordend") {
+ if (punctuation && !previousPunctuation) { break; }
+ } else {
+ if (!punctuation && previousPunctuation) { break; }
+ }
+ letterOrDigit = !punctuation && !_isWhitespace(c);
+ if (unit === "wordend") {
+ if (letterOrDigit && !previousLetterOrDigit) { break; }
+ } else {
+ if (!letterOrDigit && previousLetterOrDigit) { break; }
+ }
+ previousLetterOrDigit = letterOrDigit;
+ previousPunctuation = punctuation;
+ offsetInLine--;
+ }
+ }
+ return lineStart + offsetInLine;
+ }
+ return offset + direction;
+ },
+ _getOffset_IE: function (offset, unit, direction) {
+ var document = this._frameDocument;
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var clientDiv = this._clientDiv;
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var result = 0, range, length;
+ var lineOffset = model.getLineStart(lineIndex);
+ if (offset === model.getLineEnd(lineIndex)) {
+ range = document.body.createTextRange();
+ range.moveToElementText(child.lastChild);
+ length = range.text.length;
+ range.moveEnd(unit, direction);
+ result = offset + range.text.length - length;
+ } else if (offset === lineOffset && direction < 0) {
+ result = lineOffset;
+ } else {
+ var lineChild = child.firstChild;
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ var nodeLength = textNode.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (lineOffset + nodeLength > offset) {
+ range = document.body.createTextRange();
+ if (offset === lineOffset && direction < 0) {
+ range.moveToElementText(lineChild.previousSibling);
+ } else {
+ range.moveToElementText(lineChild);
+ range.collapse();
+ range.moveEnd("character", offset - lineOffset);
+ }
+ length = range.text.length;
+ range.moveEnd(unit, direction);
+ result = offset + range.text.length - length;
+ break;
+ }
+ lineOffset = nodeLength + lineOffset;
+ lineChild = lineChild.nextSibling;
+ }
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return result;
+ },
+ _getOffsetToX: function (offset) {
+ return this._getBoundsAtOffset(offset).left;
+ },
+ _getPadding: function (node) {
+ var left,top,right,bottom;
+ if (node.currentStyle) {
+ left = node.currentStyle.paddingLeft;
+ top = node.currentStyle.paddingTop;
+ right = node.currentStyle.paddingRight;
+ bottom = node.currentStyle.paddingBottom;
+ } else if (this._frameWindow.getComputedStyle) {
+ var style = this._frameWindow.getComputedStyle(node, null);
+ left = style.getPropertyValue("padding-left");
+ top = style.getPropertyValue("padding-top");
+ right = style.getPropertyValue("padding-right");
+ bottom = style.getPropertyValue("padding-bottom");
+ }
+ return {
+ left: parseInt(left, 10),
+ top: parseInt(top, 10),
+ right: parseInt(right, 10),
+ bottom: parseInt(bottom, 10)
+ };
+ },
+ _getScroll: function() {
+ var viewDiv = this._viewDiv;
+ return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop};
+ },
+ _getSelection: function () {
+ return this._selection.clone();
+ },
+ _getTopIndex: function (fullyVisible) {
+ var child = this._topChild;
+ if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
+ var rect = child.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ if (rect.top < viewRect.top + viewPad.top) {
+ child = this._getLineNext(child) || child;
+ }
+ }
+ return child.lineIndex;
+ },
+ _getXToOffset: function (lineIndex, x) {
+ var model = this._model;
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ if (lineStart === lineEnd) {
+ return lineStart;
+ }
+ var document = this._frameDocument;
+ var clientDiv = this._clientDiv;
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var lineRect = this._getLineBoundingClientRect(child);
+ if (x < lineRect.left) { x = lineRect.left; }
+ if (x > lineRect.right) { x = lineRect.right; }
+ /*
+ * Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to
+ * the browser window. The fix is to convert to the frame window before using it.
+ */
+ var deltaX = 0, rects;
+ if (isIE < 9) {
+ rects = child.getClientRects();
+ var minLeft = rects[0].left;
+ for (var i=1; i<rects.length; i++) {
+ minLeft = Math.min(rects[i].left, minLeft);
+ }
+ deltaX = minLeft - lineRect.left;
+ }
+ var scrollX = this._getScroll().x;
+ function _getClientRects(element) {
+ var rects, newRects, i, r;
+ if (!element._rectsCache) {
+ rects = element.getClientRects();
+ newRects = [rects.length];
+ for (i = 0; i<rects.length; i++) {
+ r = rects[i];
+ newRects[i] = {left: r.left - deltaX + scrollX, top: r.top, right: r.right - deltaX + scrollX, bottom: r.bottom};
+ }
+ element._rectsCache = newRects;
+ }
+ rects = element._rectsCache;
+ newRects = [rects.length];
+ for (i = 0; i<rects.length; i++) {
+ r = rects[i];
+ newRects[i] = {left: r.left - scrollX, top: r.top, right: r.right - scrollX, bottom: r.bottom};
+ }
+ return newRects;
+ }
+ var logicalXDPI = isIE ? window.screen.logicalXDPI : 1;
+ var deviceXDPI = isIE ? window.screen.deviceXDPI : 1;
+ var offset = lineStart;
+ var lineChild = child.firstChild;
+ done:
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ var nodeLength = textNode.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ rects = _getClientRects(lineChild);
+ for (var j = 0; j < rects.length; j++) {
+ var rect = rects[j];
+ if (rect.left <= x && x < rect.right) {
+ var range, start, end;
+ if (isIE || isRangeRects) {
+ range = isRangeRects ? document.createRange() : document.body.createTextRange();
+ var high = nodeLength;
+ var low = -1;
+ while ((high - low) > 1) {
+ var mid = Math.floor((high + low) / 2);
+ start = low + 1;
+ end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1;
+ if (isRangeRects) {
+ range.setStart(textNode, start);
+ range.setEnd(textNode, end);
+ } else {
+ range.moveToElementText(lineChild);
+ range.move("character", start);
+ range.moveEnd("character", end - start);
+ }
+ rects = range.getClientRects();
+ var found = false;
+ for (var k = 0; k < rects.length; k++) {
+ rect = rects[k];
+ var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX;
+ var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX;
+ if (rangeLeft <= x && x < rangeRight) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ high = mid;
+ } else {
+ low = mid;
+ }
+ }
+ offset += high;
+ start = high;
+ end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : Math.min(high + 1, textNode.length);
+ if (isRangeRects) {
+ range.setStart(textNode, start);
+ range.setEnd(textNode, end);
+ } else {
+ range.moveToElementText(lineChild);
+ range.move("character", start);
+ range.moveEnd("character", end - start);
+ }
+ rect = range.getClientRects()[0];
+ //TODO test for character trailing (wrong for bidi)
+ if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) {
+ offset++;
+ }
+ } else {
+ var newText = [];
+ for (var q = 0; q < nodeLength; q++) {
+ newText.push("<span>");
+ if (q === nodeLength - 1) {
+ newText.push(textNode.data.substring(q));
+ } else {
+ newText.push(textNode.data.substring(q, q + 1));
+ }
+ newText.push("</span>");
+ }
+ lineChild.innerHTML = newText.join("");
+ var rangeChild = lineChild.firstChild;
+ while (rangeChild) {
+ rect = rangeChild.getBoundingClientRect();
+ if (rect.left <= x && x < rect.right) {
+ //TODO test for character trailing (wrong for bidi)
+ if (x > rect.left + (rect.right - rect.left) / 2) {
+ offset++;
+ }
+ break;
+ }
+ offset++;
+ rangeChild = rangeChild.nextSibling;
+ }
+ if (!dummy) {
+ lineChild.innerHTML = "";
+ lineChild.appendChild(textNode);
+ /*
+ * Removing the element node that holds the selection start or end
+ * causes the selection to be lost. The fix is to detect this case
+ * and restore the selection.
+ */
+ var s = this._getSelection();
+ if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) {
+ this._updateDOMSelection();
+ }
+ }
+ }
+ break done;
+ }
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return Math.min(lineEnd, Math.max(lineStart, offset));
+ },
+ _getYToLine: function (y) {
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ y -= viewRect.top + viewPad.top;
+ var lineHeight = this._getLineHeight();
+ var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight);
+ var lineCount = this._model.getLineCount();
+ return Math.max(0, Math.min(lineCount - 1, lineIndex));
+ },
+ _getOffsetBounds: function(offset) {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineHeight = this._getLineHeight();
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var bounds = this._getBoundsAtOffset(offset);
+ var left = bounds.left;
+ var right = bounds.right;
+ var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top;
+ var bottom = top + lineHeight;
+ return {left: left, top: top, right: right, bottom: bottom};
+ },
+ _getVisible: function() {
+ var temp = this._parent;
+ var parentDocument = temp.ownerDocument;
+ while (temp !== parentDocument) {
+ var hidden;
+ if (isIE < 9) {
+ hidden = temp.currentStyle && temp.currentStyle.display === "none";
+ } else {
+ var tempStyle = parentDocument.defaultView.getComputedStyle(temp, null);
+ hidden = tempStyle && tempStyle.getPropertyValue("display") === "none";
+ }
+ if (hidden) { return "hidden"; }
+ temp = temp.parentNode;
+ if (!temp) { return "disconnected"; }
+ }
+ return "visible";
+ },
+ _hitOffset: function (offset, x, y) {
+ var bounds = this._getOffsetBounds(offset);
+ var left = bounds.left;
+ var right = bounds.right;
+ var top = bounds.top;
+ var bottom = bounds.bottom;
+ var area = 20;
+ left -= area;
+ top -= area;
+ right += area;
+ bottom += area;
+ return (left <= x && x <= right && top <= y && y <= bottom);
+ },
+ _hookEvents: function() {
+ var self = this;
+ this._modelListener = {
+ /** @private */
+ onChanging: function(modelChangingEvent) {
+ self._onModelChanging(modelChangingEvent);
+ },
+ /** @private */
+ onChanged: function(modelChangedEvent) {
+ self._onModelChanged(modelChangedEvent);
+ }
+ };
+ this._model.addEventListener("Changing", this._modelListener.onChanging);
+ this._model.addEventListener("Changed", this._modelListener.onChanged);
+
+ var clientDiv = this._clientDiv;
+ var viewDiv = this._viewDiv;
+ var body = this._frameDocument.body;
+ var handlers = this._handlers = [];
+ var resizeNode = isIE < 9 ? this._frame : this._frameWindow;
+ var focusNode = isPad ? this._textArea : (isIE || isFirefox ? this._clientDiv: this._frameWindow);
+ handlers.push({target: this._frameWindow, type: "unload", handler: function(e) { return self._handleUnload(e);}});
+ handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}});
+ handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}});
+ handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}});
+ handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}});
+ if (isPad) {
+ var touchDiv = this._touchDiv;
+ var textArea = this._textArea;
+ handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+ handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }});
+ handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }});
+ handlers.push({target: textArea, type: "click", handler: function(e) { return self._handleTextAreaClick(e); }});
+ handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }});
+ handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }});
+ handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }});
+ } else {
+ var topNode = this._overlayDiv || this._clientDiv;
+ var grabNode = isIE ? clientDiv : this._frameWindow;
+ handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+ handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}});
+ handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}});
+ handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}});
+ handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}});
+ handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}});
+ handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}});
+ handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}});
+ handlers.push({target: clientDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+ handlers.push({target: clientDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+ handlers.push({target: clientDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+ handlers.push({target: grabNode, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+ handlers.push({target: grabNode, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+ handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}});
+ handlers.push({target: body, type: "mouseup", handler: function(e) { return self._handleBodyMouseUp(e);}});
+ handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}});
+ handlers.push({target: topNode, type: "drag", handler: function(e) { return self._handleDrag(e);}});
+ handlers.push({target: topNode, type: "dragend", handler: function(e) { return self._handleDragEnd(e);}});
+ handlers.push({target: topNode, type: "dragenter", handler: function(e) { return self._handleDragEnter(e);}});
+ handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}});
+ handlers.push({target: topNode, type: "dragleave", handler: function(e) { return self._handleDragLeave(e);}});
+ handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}});
+ if (isChrome) {
+ handlers.push({target: this._parentDocument, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+ handlers.push({target: this._parentDocument, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+ }
+ if (isIE) {
+ handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }});
+ }
+ if (isFirefox) {
+ handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }});
+ }
+ if (!isIE && !isOpera) {
+ var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel";
+ handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }});
+ }
+ if (isFirefox && !isWindows) {
+ handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }});
+ }
+ if (this._overlayDiv) {
+ handlers.push({target: this._overlayDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+ handlers.push({target: this._overlayDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+ handlers.push({target: this._overlayDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+ handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }});
+ }
+ if (!isW3CEvents) {
+ handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }});
+ }
+ }
+ for (var i=0; i<handlers.length; i++) {
+ var h = handlers[i];
+ addHandler(h.target, h.type, h.handler, h.capture);
+ }
+ },
+ _init: function(options) {
+ var parent = options.parent;
+ if (typeof(parent) === "string") {
+ parent = window.document.getElementById(parent);
+ }
+ if (!parent) { throw "no parent"; }
+ options.parent = parent;
+ options.model = options.model || new mTextModel.TextModel();
+ var defaultOptions = this._defaultOptions();
+ for (var option in defaultOptions) {
+ if (defaultOptions.hasOwnProperty(option)) {
+ var value;
+ if (options[option] !== undefined) {
+ value = options[option];
+ } else {
+ value = defaultOptions[option].value;
+ }
+ this["_" + option] = value;
+ }
+ }
+ this._rulers = [];
+ this._selection = new Selection (0, 0, false);
+ this._linksVisible = false;
+ this._redrawCount = 0;
+ this._maxLineWidth = 0;
+ this._maxLineIndex = -1;
+ this._ignoreSelect = true;
+ this._ignoreFocus = false;
+ this._columnX = -1;
+ this._dragOffset = -1;
+
+ /* Auto scroll */
+ this._autoScrollX = null;
+ this._autoScrollY = null;
+ this._autoScrollTimerID = null;
+ this._AUTO_SCROLL_RATE = 50;
+ this._grabControl = null;
+ this._moseMoveClosure = null;
+ this._mouseUpClosure = null;
+
+ /* Double click */
+ this._lastMouseX = 0;
+ this._lastMouseY = 0;
+ this._lastMouseTime = 0;
+ this._clickCount = 0;
+ this._clickTime = 250;
+ this._clickDist = 5;
+ this._isMouseDown = false;
+ this._doubleClickSelection = null;
+
+ /* Scroll */
+ this._hScroll = 0;
+ this._vScroll = 0;
+
+ /* IME */
+ this._imeOffset = -1;
+
+ /* Create elements */
+ this._createActions();
+ this._createFrame();
+ },
+ _isLinkURL: function(string) {
+ return string.toLowerCase().lastIndexOf(".css") === string.length - 4;
+ },
+ _modifyContent: function(e, updateCaret) {
+ if (this._readonly && !e._code) {
+ return;
+ }
+ e.type = "Verify";
+ this.onVerify(e);
+
+ if (e.text === null || e.text === undefined) { return; }
+
+ var model = this._model;
+ try {
+ if (e._ignoreDOMSelection) { this._ignoreDOMSelection = true; }
+ model.setText (e.text, e.start, e.end);
+ } finally {
+ if (e._ignoreDOMSelection) { this._ignoreDOMSelection = false; }
+ }
+
+ if (updateCaret) {
+ var selection = this._getSelection ();
+ selection.setCaret(e.start + e.text.length);
+ this._setSelection(selection, true);
+ }
+ this.onModify({type: "Modify"});
+ },
+ _onModelChanged: function(modelChangedEvent) {
+ modelChangedEvent.type = "ModelChanged";
+ this.onModelChanged(modelChangedEvent);
+ modelChangedEvent.type = "Changed";
+ var start = modelChangedEvent.start;
+ var addedCharCount = modelChangedEvent.addedCharCount;
+ var removedCharCount = modelChangedEvent.removedCharCount;
+ var addedLineCount = modelChangedEvent.addedLineCount;
+ var removedLineCount = modelChangedEvent.removedLineCount;
+ var selection = this._getSelection();
+ if (selection.end > start) {
+ if (selection.end > start && selection.start < start + removedCharCount) {
+ // selection intersects replaced text. set caret behind text change
+ selection.setCaret(start + addedCharCount);
+ } else {
+ // move selection to keep same text selected
+ selection.start += addedCharCount - removedCharCount;
+ selection.end += addedCharCount - removedCharCount;
+ }
+ this._setSelection(selection, false, false);
+ }
+
+ var model = this._model;
+ var startLine = model.getLineAtOffset(start);
+ var child = this._getLineNext();
+ while (child) {
+ var lineIndex = child.lineIndex;
+ if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) {
+ if (startLine === lineIndex && !child.modelChangedEvent && !child.lineRemoved) {
+ child.modelChangedEvent = modelChangedEvent;
+ child.lineChanged = true;
+ } else {
+ child.lineRemoved = true;
+ child.lineChanged = false;
+ child.modelChangedEvent = null;
+ }
+ }
+ if (lineIndex > startLine + removedLineCount) {
+ child.lineIndex = lineIndex + addedLineCount - removedLineCount;
+ }
+ child = this._getLineNext(child);
+ }
+ if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) {
+ this._checkMaxLineIndex = this._maxLineIndex;
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ }
+ this._updatePage();
+ },
+ _onModelChanging: function(modelChangingEvent) {
+ modelChangingEvent.type = "ModelChanging";
+ this.onModelChanging(modelChangingEvent);
+ modelChangingEvent.type = "Changing";
+ },
+ _queueUpdatePage: function() {
+ if (this._updateTimer) { return; }
+ var self = this;
+ this._updateTimer = setTimeout(function() {
+ self._updateTimer = null;
+ self._updatePage();
+ }, 0);
+ },
+ _reset: function() {
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ this._columnX = -1;
+ this._topChild = null;
+ this._bottomChild = null;
+ this._partialY = 0;
+ this._setSelection(new Selection (0, 0, false), false, false);
+ if (this._viewDiv) {
+ this._viewDiv.scrollLeft = 0;
+ this._viewDiv.scrollTop = 0;
+ }
+ var clientDiv = this._clientDiv;
+ if (clientDiv) {
+ var child = clientDiv.firstChild;
+ while (child) {
+ child.lineRemoved = true;
+ child = child.nextSibling;
+ }
+ /*
+ * Bug in Firefox. For some reason, the caret does not show after the
+ * view is refreshed. The fix is to toggle the contentEditable state and
+ * force the clientDiv to loose and receive focus if it is focused.
+ */
+ if (isFirefox) {
+ this._ignoreFocus = false;
+ var hasFocus = this._hasFocus;
+ if (hasFocus) { clientDiv.blur(); }
+ clientDiv.contentEditable = false;
+ clientDiv.contentEditable = true;
+ if (hasFocus) { clientDiv.focus(); }
+ this._ignoreFocus = false;
+ }
+ }
+ },
+ _resizeTouchDiv: function() {
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var parentRect = this._frame.getBoundingClientRect();
+ var temp = this._frame;
+ while (temp) {
+ if (temp.style && temp.style.top) { break; }
+ temp = temp.parentNode;
+ }
+ var parentTop = parentRect.top;
+ if (temp) {
+ parentTop -= temp.getBoundingClientRect().top;
+ } else {
+ parentTop += this._parentDocument.body.scrollTop;
+ }
+ temp = this._frame;
+ while (temp) {
+ if (temp.style && temp.style.left) { break; }
+ temp = temp.parentNode;
+ }
+ var parentLeft = parentRect.left;
+ if (temp) {
+ parentLeft -= temp.getBoundingClientRect().left;
+ } else {
+ parentLeft += this._parentDocument.body.scrollLeft;
+ }
+ var touchDiv = this._touchDiv;
+ touchDiv.style.left = (parentLeft + viewRect.left) + "px";
+ touchDiv.style.top = (parentTop + viewRect.top) + "px";
+ touchDiv.style.width = viewRect.width + "px";
+ touchDiv.style.height = viewRect.height + "px";
+ },
+ _scrollView: function (pixelX, pixelY) {
+ /*
+ * Always set _ensureCaretVisible to false so that the view does not scroll
+ * to show the caret when scrollView is not called from showCaret().
+ */
+ this._ensureCaretVisible = false;
+
+ /*
+ * Scrolling is done only by setting the scrollLeft and scrollTop fields in the
+ * view div. This causes an updatePage from the scroll event. In some browsers
+ * this event is asynchronous and forcing update page to run synchronously
+ * leads to redraw problems.
+ * On Chrome 11, the view redrawing at times when holding PageDown/PageUp key.
+ * On Firefox 4 for Linux, the view redraws the first page when holding
+ * PageDown/PageUp key, but it will not redraw again until the key is released.
+ */
+ var viewDiv = this._viewDiv;
+ if (pixelX) { viewDiv.scrollLeft += pixelX; }
+ if (pixelY) { viewDiv.scrollTop += pixelY; }
+ },
+ _setClipboardText: function (text, event) {
+ var clipboardText;
+ if (this._frameWindow.clipboardData) {
+ //IE
+ clipboardText = [];
+ this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+ return this._frameWindow.clipboardData.setData("Text", clipboardText.join(""));
+ }
+ /* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */
+ if (isChrome || isFirefox || !event) {
+ var window = this._frameWindow;
+ var document = this._frameDocument;
+ var child = document.createElement("PRE");
+ child.style.position = "fixed";
+ child.style.left = "-1000px";
+ this._convertDelimiter(text,
+ function(t) {
+ child.appendChild(document.createTextNode(t));
+ },
+ function() {
+ child.appendChild(document.createElement("BR"));
+ }
+ );
+ child.appendChild(document.createTextNode(" "));
+ this._clientDiv.appendChild(child);
+ var range = document.createRange();
+ range.setStart(child.firstChild, 0);
+ range.setEndBefore(child.lastChild);
+ var sel = window.getSelection();
+ if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+ sel.addRange(range);
+ var self = this;
+ /** @ignore */
+ var cleanup = function() {
+ if (child && child.parentNode === self._clientDiv) {
+ self._clientDiv.removeChild(child);
+ }
+ self._updateDOMSelection();
+ };
+ var result = false;
+ /*
+ * Try execCommand first, it works on firefox with clipboard permission,
+ * chrome 5, safari 4.
+ */
+ this._ignoreCopy = true;
+ try {
+ result = document.execCommand("copy", false, null);
+ } catch (e) {}
+ this._ignoreCopy = false;
+ if (!result) {
+ if (event) {
+ setTimeout(cleanup, 0);
+ return false;
+ }
+ }
+ /* no event and no permission, copy can not be done */
+ cleanup();
+ return true;
+ }
+ if (event && event.clipboardData) {
+ //webkit
+ clipboardText = [];
+ this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+ return event.clipboardData.setData("text/plain", clipboardText.join(""));
+ }
+ },
+ _setDOMSelection: function (startNode, startOffset, endNode, endOffset) {
+ var window = this._frameWindow;
+ var document = this._frameDocument;
+ var startLineNode, startLineOffset, endLineNode, endLineOffset;
+ var offset = 0;
+ var lineChild = startNode.firstChild;
+ var node, nodeLength, model = this._model;
+ var startLineEnd = model.getLine(startNode.lineIndex).length;
+ while (lineChild) {
+ node = lineChild.firstChild;
+ nodeLength = node.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) {
+ startLineNode = node;
+ startLineOffset = startOffset - offset;
+ if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) {
+ startLineOffset += lineChild.ignoreChars;
+ }
+ break;
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ offset = 0;
+ lineChild = endNode.firstChild;
+ var endLineEnd = this._model.getLine(endNode.lineIndex).length;
+ while (lineChild) {
+ node = lineChild.firstChild;
+ nodeLength = node.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) {
+ endLineNode = node;
+ endLineOffset = endOffset - offset;
+ if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) {
+ endLineOffset += lineChild.ignoreChars;
+ }
+ break;
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+
+ this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd);
+ if (isPad) { return; }
+
+ var range;
+ if (window.getSelection) {
+ //W3C
+ range = document.createRange();
+ range.setStart(startLineNode, startLineOffset);
+ range.setEnd(endLineNode, endLineOffset);
+ var sel = window.getSelection();
+ this._ignoreSelect = false;
+ if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+ sel.addRange(range);
+ this._ignoreSelect = true;
+ } else if (document.selection) {
+ //IE < 9
+ var body = document.body;
+
+ /*
+ * Bug in IE. For some reason when text is deselected the overflow
+ * selection at the end of some lines does not get redrawn. The
+ * fix is to create a DOM element in the body to force a redraw.
+ */
+ var child = document.createElement("DIV");
+ body.appendChild(child);
+ body.removeChild(child);
+
+ range = body.createTextRange();
+ range.moveToElementText(startLineNode.parentNode);
+ range.moveStart("character", startLineOffset);
+ var endRange = body.createTextRange();
+ endRange.moveToElementText(endLineNode.parentNode);
+ endRange.moveStart("character", endLineOffset);
+ range.setEndPoint("EndToStart", endRange);
+ this._ignoreSelect = false;
+ range.select();
+ this._ignoreSelect = true;
+ }
+ },
+ _setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) {
+ var model = this._model;
+ if (this._selDiv1) {
+ var startLineBounds, l;
+ startLineBounds = this._getLineBoundingClientRect(startNode);
+ if (startOffset === 0) {
+ l = startLineBounds.left;
+ } else {
+ if (startOffset >= startLineEnd) {
+ l = startLineBounds.right;
+ } else {
+ this._ignoreDOMSelection = true;
+ l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left;
+ this._ignoreDOMSelection = false;
+ }
+ }
+ var textArea = this._textArea;
+ if (textArea && isPad) {
+ textArea.selectionStart = textArea.selectionEnd = 0;
+ var rect = this._frame.getBoundingClientRect();
+ var touchRect = this._touchDiv.getBoundingClientRect();
+ var viewBounds = this._viewDiv.getBoundingClientRect();
+ if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width &&
+ viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) ||
+ !(startNode === endNode && startOffset === endOffset))
+ {
+ textArea.style.left = "-1000px";
+ } else {
+ textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px";
+ }
+ textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px";
+ textArea.style.width = "6px";
+ textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px";
+ }
+
+ var selDiv = this._selDiv1;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ selDiv = this._selDiv2;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ selDiv = this._selDiv3;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ if (!(startNode === endNode && startOffset === endOffset)) {
+ var handleWidth = isPad ? 2 : 0;
+ var handleBorder = handleWidth + "px blue solid";
+ var viewPad = this._getViewPadding();
+ var clientRect = this._clientDiv.getBoundingClientRect();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var left = viewRect.left + viewPad.left;
+ var right = clientRect.right;
+ var top = viewRect.top + viewPad.top;
+ var bottom = clientRect.bottom;
+ var hd = 0, vd = 0;
+ if (this._clipDiv) {
+ var clipRect = this._clipDiv.getBoundingClientRect();
+ hd = clipRect.left - this._clipDiv.scrollLeft;
+ vd = clipRect.top;
+ }
+ var r;
+ var endLineBounds = this._getLineBoundingClientRect(endNode);
+ if (endOffset === 0) {
+ r = endLineBounds.left;
+ } else {
+ if (endOffset >= endLineEnd) {
+ r = endLineBounds.right;
+ } else {
+ this._ignoreDOMSelection = true;
+ r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left;
+ this._ignoreDOMSelection = false;
+ }
+ }
+ var sel1Div = this._selDiv1;
+ var sel1Left = Math.min(right, Math.max(left, l));
+ var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top));
+ var sel1Right = right;
+ var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom));
+ sel1Div.style.left = (sel1Left - hd) + "px";
+ sel1Div.style.top = (sel1Top - vd) + "px";
+ sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px";
+ sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px";
+ if (isPad) {
+ sel1Div.style.borderLeft = handleBorder;
+ sel1Div.style.borderRight = "0px";
+ }
+ if (startNode === endNode) {
+ sel1Right = Math.min(r, right);
+ sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px";
+ if (isPad) {
+ sel1Div.style.borderRight = handleBorder;
+ }
+ } else {
+ var sel3Left = left;
+ var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top));
+ var sel3Right = Math.min(right, Math.max(left, r));
+ var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom));
+ var sel3Div = this._selDiv3;
+ sel3Div.style.left = (sel3Left - hd) + "px";
+ sel3Div.style.top = (sel3Top - vd) + "px";
+ sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px";
+ sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px";
+ if (isPad) {
+ sel3Div.style.borderRight = handleBorder;
+ }
+ if (sel3Top - sel1Bottom > 0) {
+ var sel2Div = this._selDiv2;
+ sel2Div.style.left = (left - hd) + "px";
+ sel2Div.style.top = (sel1Bottom - vd) + "px";
+ sel2Div.style.width = Math.max(0, right - left) + "px";
+ sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px";
+ }
+ }
+ }
+ }
+ },
+ _setGrab: function (target) {
+ if (target === this._grabControl) { return; }
+ if (target) {
+ if (target.setCapture) { target.setCapture(); }
+ this._grabControl = target;
+ } else {
+ if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); }
+ this._grabControl = null;
+ }
+ },
+ _setLinksVisible: function(visible) {
+ if (this._linksVisible === visible) { return; }
+ this._linksVisible = visible;
+ /*
+ * Feature in IE. The client div looses focus and does not regain it back
+ * when the content editable flag is reset. The fix is to remember that it
+ * had focus when the flag is cleared and give focus back to the div when
+ * the flag is set.
+ */
+ if (isIE && visible) {
+ this._hadFocus = this._hasFocus;
+ }
+ var clientDiv = this._clientDiv;
+ clientDiv.contentEditable = !visible;
+ if (this._hadFocus && !visible) {
+ clientDiv.focus();
+ }
+ if (this._overlayDiv) {
+ this._overlayDiv.style.zIndex = visible ? "-1" : "1";
+ }
+ var document = this._frameDocument;
+ var line = this._getLineNext();
+ while (line) {
+ if (line.hasLink) {
+ var lineChild = line.firstChild;
+ while (lineChild) {
+ var next = lineChild.nextSibling;
+ var style = lineChild.viewStyle;
+ if (style && style.tagName === "A") {
+ line.replaceChild(this._createSpan(line, document, lineChild.firstChild.data, style), lineChild);
+ }
+ lineChild = next;
+ }
+ }
+ line = this._getLineNext(line);
+ }
+ },
+ _setSelection: function (selection, scroll, update, pageScroll) {
+ if (selection) {
+ this._columnX = -1;
+ if (update === undefined) { update = true; }
+ var oldSelection = this._selection;
+ if (!oldSelection.equals(selection)) {
+ this._selection = selection;
+ var e = {
+ type: "Selection",
+ oldValue: {start:oldSelection.start, end:oldSelection.end},
+ newValue: {start:selection.start, end:selection.end}
+ };
+ this.onSelection(e);
+ }
+ /*
+ * Always showCaret(), even when the selection is not changing, to ensure the
+ * caret is visible. Note that some views do not scroll to show the caret during
+ * keyboard navigation when the selection does not chanage. For example, line down
+ * when the caret is already at the last line.
+ */
+ if (scroll) { update = !this._showCaret(false, pageScroll); }
+
+ /*
+ * Sometimes the browser changes the selection
+ * as result of method calls or "leaked" events.
+ * The fix is to set the visual selection even
+ * when the logical selection is not changed.
+ */
+ if (update) { this._updateDOMSelection(); }
+ }
+ },
+ _setSelectionTo: function (x, y, extent, drag) {
+ var model = this._model, offset;
+ var selection = this._getSelection();
+ var lineIndex = this._getYToLine(y);
+ if (this._clickCount === 1) {
+ offset = this._getXToOffset(lineIndex, x);
+ if (drag && !extent) {
+ if (selection.start <= offset && offset < selection.end) {
+ this._dragOffset = offset;
+ return false;
+ }
+ }
+ selection.extend(offset);
+ if (!extent) { selection.collapse(); }
+ } else {
+ var word = (this._clickCount & 1) === 0;
+ var start, end;
+ if (word) {
+ offset = this._getXToOffset(lineIndex, x);
+ if (this._doubleClickSelection) {
+ if (offset >= this._doubleClickSelection.start) {
+ start = this._doubleClickSelection.start;
+ end = this._getOffset(offset, "wordend", +1);
+ } else {
+ start = this._getOffset(offset, "word", -1);
+ end = this._doubleClickSelection.end;
+ }
+ } else {
+ start = this._getOffset(offset, "word", -1);
+ end = this._getOffset(start, "wordend", +1);
+ }
+ } else {
+ if (this._doubleClickSelection) {
+ var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start);
+ if (lineIndex >= doubleClickLine) {
+ start = model.getLineStart(doubleClickLine);
+ end = model.getLineEnd(lineIndex);
+ } else {
+ start = model.getLineStart(lineIndex);
+ end = model.getLineEnd(doubleClickLine);
+ }
+ } else {
+ start = model.getLineStart(lineIndex);
+ end = model.getLineEnd(lineIndex);
+ }
+ }
+ selection.setCaret(start);
+ selection.extend(end);
+ }
+ this._setSelection(selection, true, true);
+ return true;
+ },
+ _setStyleSheet: function(stylesheet) {
+ var oldstylesheet = this._stylesheet;
+ if (!(oldstylesheet instanceof Array)) {
+ oldstylesheet = [oldstylesheet];
+ }
+ this._stylesheet = stylesheet;
+ if (!(stylesheet instanceof Array)) {
+ stylesheet = [stylesheet];
+ }
+ var document = this._frameDocument;
+ var documentStylesheet = document.styleSheets;
+ var head = document.getElementsByTagName("head")[0];
+ var changed = false;
+ var i = 0, sheet, oldsheet, documentSheet, ownerNode, styleNode, textNode;
+ while (i < stylesheet.length) {
+ if (i >= oldstylesheet.length) { break; }
+ sheet = stylesheet[i];
+ oldsheet = oldstylesheet[i];
+ if (sheet !== oldsheet) {
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ documentSheet = documentStylesheet[i+1];
+ ownerNode = documentSheet.ownerNode;
+ styleNode = document.createElement('STYLE');
+ textNode = document.createTextNode(sheet);
+ styleNode.appendChild(textNode);
+ head.replaceChild(styleNode, ownerNode);
+ changed = true;
+ }
+ }
+ i++;
+ }
+ if (i < oldstylesheet.length) {
+ while (i < oldstylesheet.length) {
+ sheet = oldstylesheet[i];
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ documentSheet = documentStylesheet[i+1];
+ ownerNode = documentSheet.ownerNode;
+ head.removeChild(ownerNode);
+ changed = true;
+ }
+ i++;
+ }
+ } else {
+ while (i < stylesheet.length) {
+ sheet = stylesheet[i];
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ styleNode = document.createElement('STYLE');
+ textNode = document.createTextNode(sheet);
+ styleNode.appendChild(textNode);
+ head.appendChild(styleNode);
+ changed = true;
+ }
+ i++;
+ }
+ }
+ if (changed) {
+ this._updateStyle();
+ }
+ return false;
+ },
+ _setFullSelection: function(fullSelection, init) {
+ this._fullSelection = fullSelection;
+
+ /*
+ * Bug in IE 8. For some reason, during scrolling IE does not reflow the elements
+ * that are used to compute the location for the selection divs. This causes the
+ * divs to be placed at the wrong location. The fix is to disabled full selection for IE8.
+ */
+ if (isIE < 9) {
+ this._fullSelection = false;
+ }
+ if (isWebkit) {
+ this._fullSelection = true;
+ }
+ var parent = this._clipDiv || this._scrollDiv;
+ if (!parent) {
+ return;
+ }
+ if (!isPad && !this._fullSelection) {
+ if (this._selDiv1) {
+ parent.removeChild(this._selDiv1);
+ this._selDiv1 = null;
+ }
+ if (this._selDiv2) {
+ parent.removeChild(this._selDiv2);
+ this._selDiv2 = null;
+ }
+ if (this._selDiv3) {
+ parent.removeChild(this._selDiv3);
+ this._selDiv3 = null;
+ }
+ return;
+ }
+
+ if (!this._selDiv1 && (isPad || (this._fullSelection && !isWebkit))) {
+ var frameDocument = this._frameDocument;
+ this._hightlightRGB = "Highlight";
+ var selDiv1 = frameDocument.createElement("DIV");
+ this._selDiv1 = selDiv1;
+ selDiv1.id = "selDiv1";
+ selDiv1.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv1.style.borderWidth = "0px";
+ selDiv1.style.margin = "0px";
+ selDiv1.style.padding = "0px";
+ selDiv1.style.outline = "none";
+ selDiv1.style.background = this._hightlightRGB;
+ selDiv1.style.width = "0px";
+ selDiv1.style.height = "0px";
+ selDiv1.style.zIndex = "0";
+ parent.appendChild(selDiv1);
+ var selDiv2 = frameDocument.createElement("DIV");
+ this._selDiv2 = selDiv2;
+ selDiv2.id = "selDiv2";
+ selDiv2.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv2.style.borderWidth = "0px";
+ selDiv2.style.margin = "0px";
+ selDiv2.style.padding = "0px";
+ selDiv2.style.outline = "none";
+ selDiv2.style.background = this._hightlightRGB;
+ selDiv2.style.width = "0px";
+ selDiv2.style.height = "0px";
+ selDiv2.style.zIndex = "0";
+ parent.appendChild(selDiv2);
+ var selDiv3 = frameDocument.createElement("DIV");
+ this._selDiv3 = selDiv3;
+ selDiv3.id = "selDiv3";
+ selDiv3.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv3.style.borderWidth = "0px";
+ selDiv3.style.margin = "0px";
+ selDiv3.style.padding = "0px";
+ selDiv3.style.outline = "none";
+ selDiv3.style.background = this._hightlightRGB;
+ selDiv3.style.width = "0px";
+ selDiv3.style.height = "0px";
+ selDiv3.style.zIndex = "0";
+ parent.appendChild(selDiv3);
+
+ /*
+ * Bug in Firefox. The Highlight color is mapped to list selection
+ * background instead of the text selection background. The fix
+ * is to map known colors using a table or fallback to light blue.
+ */
+ if (isFirefox && isMac) {
+ var style = this._frameWindow.getComputedStyle(selDiv3, null);
+ var rgb = style.getPropertyValue("background-color");
+ switch (rgb) {
+ case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break;
+ case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break;
+ case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break;
+ case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break;
+ case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break;
+ case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break;
+ case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break;
+ default: rgb = "rgb(180, 213, 255)"; break;
+ }
+ this._hightlightRGB = rgb;
+ selDiv1.style.background = rgb;
+ selDiv2.style.background = rgb;
+ selDiv3.style.background = rgb;
+ if (!this._insertedSelRule) {
+ var styleSheet = frameDocument.styleSheets[0];
+ styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0);
+ this._insertedSelRule = true;
+ }
+ }
+ if (!init) {
+ this._updateDOMSelection();
+ }
+ }
+ },
+ _setTabSize: function (tabSize, init) {
+ this._tabSize = tabSize;
+ this._customTabSize = undefined;
+ var clientDiv = this._clientDiv;
+ if (isOpera) {
+ if (clientDiv) { clientDiv.style.OTabSize = this._tabSize+""; }
+ } else if (isFirefox >= 4) {
+ if (clientDiv) { clientDiv.style.MozTabSize = this._tabSize+""; }
+ } else if (this._tabSize !== 8) {
+ this._customTabSize = this._tabSize;
+ if (!init) {
+ this.redrawLines();
+ }
+ }
+ },
+ _setThemeClass: function (themeClass, init) {
+ this._themeClass = themeClass;
+ var document = this._frameDocument;
+ if (document) {
+ var viewContainerClass = "viewContainer";
+ if (this._themeClass) { viewContainerClass += " " + this._themeClass; }
+ document.body.className = viewContainerClass;
+ if (!init) {
+ this._updateStyle();
+ }
+ }
+ },
+ _showCaret: function (allSelection, pageScroll) {
+ if (!this._clientDiv) { return; }
+ var model = this._model;
+ var selection = this._getSelection();
+ var scroll = this._getScroll();
+ var caret = selection.getCaret();
+ var start = selection.start;
+ var end = selection.end;
+ var startLine = model.getLineAtOffset(start);
+ var endLine = model.getLineAtOffset(end);
+ var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1);
+ var viewPad = this._getViewPadding();
+
+ var clientWidth = this._getClientWidth();
+ var leftEdge = viewPad.left;
+ var rightEdge = viewPad.left + clientWidth;
+ var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive);
+ var left = bounds.left;
+ var right = bounds.right;
+ var minScroll = clientWidth / 4;
+ if (allSelection && !selection.isEmpty() && startLine === endLine) {
+ bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive);
+ var selectionWidth = caret === start ? bounds.right - left : right - bounds.left;
+ if ((clientWidth - minScroll) > selectionWidth) {
+ if (left > bounds.left) { left = bounds.left; }
+ if (right < bounds.right) { right = bounds.right; }
+ }
+ }
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ left -= viewRect.left;
+ right -= viewRect.left;
+ var pixelX = 0;
+ if (left < leftEdge) {
+ pixelX = Math.min(left - leftEdge, -minScroll);
+ }
+ if (right > rightEdge) {
+ var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth;
+ pixelX = Math.min(maxScroll, Math.max(right - rightEdge, minScroll));
+ }
+
+ var pixelY = 0;
+ var topIndex = this._getTopIndex(true);
+ var bottomIndex = this._getBottomIndex(true);
+ var caretLine = model.getLineAtOffset(caret);
+ var clientHeight = this._getClientHeight();
+ if (!(topIndex <= caretLine && caretLine <= bottomIndex)) {
+ var lineHeight = this._getLineHeight();
+ var selectionHeight = allSelection ? (endLine - startLine) * lineHeight : 0;
+ pixelY = caretLine * lineHeight;
+ pixelY -= scroll.y;
+ if (pixelY + lineHeight > clientHeight) {
+ pixelY -= clientHeight - lineHeight;
+ if (caret === start && start !== end) {
+ pixelY += Math.min(clientHeight - lineHeight, selectionHeight);
+ }
+ } else {
+ if (caret === end) {
+ pixelY -= Math.min (clientHeight - lineHeight, selectionHeight);
+ }
+ }
+ if (pageScroll) {
+ if (pageScroll > 0) {
+ if (pixelY > 0) {
+ pixelY = Math.max(pixelY, pageScroll);
+ }
+ } else {
+ if (pixelY < 0) {
+ pixelY = Math.min(pixelY, pageScroll);
+ }
+ }
+ }
+ }
+
+ if (pixelX !== 0 || pixelY !== 0) {
+ this._scrollView (pixelX, pixelY);
+ /*
+ * When the view scrolls it is possible that one of the scrollbars can show over the caret.
+ * Depending on the browser scrolling can be synchronous (Safari), in which case the change
+ * can be detected before showCaret() returns. When scrolling is asynchronous (most browsers),
+ * the detection is done during the next update page.
+ */
+ if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) {
+ this._showCaret();
+ } else {
+ this._ensureCaretVisible = true;
+ }
+ return true;
+ }
+ return false;
+ },
+ _startIME: function () {
+ if (this._imeOffset !== -1) { return; }
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ }
+ this._imeOffset = selection.start;
+ },
+ _unhookEvents: function() {
+ this._model.removeEventListener("Changing", this._modelListener.onChanging);
+ this._model.removeEventListener("Changed", this._modelListener.onChanged);
+ this._modelListener = null;
+ for (var i=0; i<this._handlers.length; i++) {
+ var h = this._handlers[i];
+ removeHandler(h.target, h.type, h.handler);
+ }
+ this._handlers = null;
+ },
+ _updateDOMSelection: function () {
+ if (this._ignoreDOMSelection) { return; }
+ if (!this._clientDiv) { return; }
+ var selection = this._getSelection();
+ var model = this._model;
+ var startLine = model.getLineAtOffset(selection.start);
+ var endLine = model.getLineAtOffset(selection.end);
+ var firstNode = this._getLineNext();
+ /*
+ * Bug in Firefox. For some reason, after a update page sometimes the
+ * firstChild returns null incorrectly. The fix is to ignore show selection.
+ */
+ if (!firstNode) { return; }
+ var lastNode = this._getLinePrevious();
+
+ var topNode, bottomNode, topOffset, bottomOffset;
+ if (startLine < firstNode.lineIndex) {
+ topNode = firstNode;
+ topOffset = 0;
+ } else if (startLine > lastNode.lineIndex) {
+ topNode = lastNode;
+ topOffset = 0;
+ } else {
+ topNode = this._getLineNode(startLine);
+ topOffset = selection.start - model.getLineStart(startLine);
+ }
+
+ if (endLine < firstNode.lineIndex) {
+ bottomNode = firstNode;
+ bottomOffset = 0;
+ } else if (endLine > lastNode.lineIndex) {
+ bottomNode = lastNode;
+ bottomOffset = 0;
+ } else {
+ bottomNode = this._getLineNode(endLine);
+ bottomOffset = selection.end - model.getLineStart(endLine);
+ }
+ this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset);
+ },
+ _updatePage: function(hScrollOnly) {
+ if (this._redrawCount > 0) { return; }
+ if (this._updateTimer) {
+ clearTimeout(this._updateTimer);
+ this._updateTimer = null;
+ hScrollOnly = false;
+ }
+ var clientDiv = this._clientDiv;
+ if (!clientDiv) { return; }
+ var model = this._model;
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var lineCount = model.getLineCount();
+ var lineHeight = this._getLineHeight();
+ var firstLine = Math.max(0, scroll.y) / lineHeight;
+ var topIndex = Math.floor(firstLine);
+ var lineStart = Math.max(0, topIndex - 1);
+ var top = Math.round((firstLine - lineStart) * lineHeight);
+ var partialY = this._partialY = Math.round((firstLine - topIndex) * lineHeight);
+ var scrollWidth, scrollHeight = lineCount * lineHeight;
+ var leftWidth, clientWidth, clientHeight;
+ if (hScrollOnly) {
+ clientWidth = this._getClientWidth();
+ clientHeight = this._getClientHeight();
+ leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+ scrollWidth = Math.max(this._maxLineWidth, clientWidth);
+ } else {
+ var document = this._frameDocument;
+ var frameWidth = this._getFrameWidth();
+ var frameHeight = this._getFrameHeight();
+ document.body.style.width = frameWidth + "px";
+ document.body.style.height = frameHeight + "px";
+
+ /* Update view height in order to have client height computed */
+ var viewDiv = this._viewDiv;
+ viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px";
+ clientHeight = this._getClientHeight();
+ var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight);
+ var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1);
+ var lineEnd = Math.min(bottomIndex + 1, lineCount - 1);
+
+ var lineIndex, lineWidth;
+ var child = clientDiv.firstChild;
+ while (child) {
+ lineIndex = child.lineIndex;
+ var nextChild = child.nextSibling;
+ if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineRemoved || child.lineIndex === -1) {
+ if (this._mouseWheelLine === child) {
+ child.style.display = "none";
+ child.lineIndex = -1;
+ } else {
+ clientDiv.removeChild(child);
+ }
+ }
+ child = nextChild;
+ }
+
+ child = this._getLineNext();
+ var frag = document.createDocumentFragment();
+ for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) {
+ if (!child || child.lineIndex > lineIndex) {
+ this._createLine(frag, null, document, lineIndex, model);
+ } else {
+ if (frag.firstChild) {
+ clientDiv.insertBefore(frag, child);
+ frag = document.createDocumentFragment();
+ }
+ if (child && child.lineChanged) {
+ child = this._createLine(frag, child, document, lineIndex, model);
+ child.lineChanged = false;
+ }
+ child = this._getLineNext(child);
+ }
+ }
+ if (frag.firstChild) { clientDiv.insertBefore(frag, child); }
+
+ /*
+ * Feature in WekKit. Webkit limits the width of the lines
+ * computed below to the width of the client div. This causes
+ * the lines to be wrapped even though "pre" is set. The fix
+ * is to set the width of the client div to a larger number
+ * before computing the lines width. Note that this value is
+ * reset to the appropriate value further down.
+ */
+ if (isWebkit) {
+ clientDiv.style.width = (0x7FFFF).toString() + "px";
+ }
+
+ var rect;
+ child = this._getLineNext();
+ while (child) {
+ lineWidth = child.lineWidth;
+ if (lineWidth === undefined) {
+ rect = this._getLineBoundingClientRect(child);
+ lineWidth = child.lineWidth = rect.right - rect.left;
+ }
+ if (lineWidth >= this._maxLineWidth) {
+ this._maxLineWidth = lineWidth;
+ this._maxLineIndex = child.lineIndex;
+ }
+ if (child.lineIndex === topIndex) { this._topChild = child; }
+ if (child.lineIndex === bottomIndex) { this._bottomChild = child; }
+ if (this._checkMaxLineIndex === child.lineIndex) { this._checkMaxLineIndex = -1; }
+ child = this._getLineNext(child);
+ }
+ if (this._checkMaxLineIndex !== -1) {
+ lineIndex = this._checkMaxLineIndex;
+ this._checkMaxLineIndex = -1;
+ if (0 <= lineIndex && lineIndex < lineCount) {
+ var dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ rect = this._getLineBoundingClientRect(dummy);
+ lineWidth = rect.right - rect.left;
+ if (lineWidth >= this._maxLineWidth) {
+ this._maxLineWidth = lineWidth;
+ this._maxLineIndex = lineIndex;
+ }
+ clientDiv.removeChild(dummy);
+ }
+ }
+
+ // Update rulers
+ this._updateRuler(this._leftDiv, topIndex, bottomIndex);
+ this._updateRuler(this._rightDiv, topIndex, bottomIndex);
+
+ leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+ var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0;
+ viewDiv.style.left = leftWidth + "px";
+ viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px";
+ if (this._rightDiv) {
+ this._rightDiv.style.left = (frameWidth - rightWidth) + "px";
+ }
+ /* Need to set the height first in order for the width to consider the vertical scrollbar */
+ var scrollDiv = this._scrollDiv;
+ scrollDiv.style.height = scrollHeight + "px";
+ /*
+ * TODO if frameHeightWithoutHScrollbar < scrollHeight < frameHeightWithHScrollbar and the horizontal bar is visible,
+ * then the clientWidth is wrong because the vertical scrollbar is showing. To correct code should hide both scrollbars
+ * at this point.
+ */
+ clientWidth = this._getClientWidth();
+ var width = Math.max(this._maxLineWidth, clientWidth);
+ /*
+ * Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding
+ * in the scrollbar. It is possible this a bug since all other paddings are considered.
+ */
+ scrollWidth = width;
+ if (!isIE || isIE >= 9) { width += viewPad.right; }
+ scrollDiv.style.width = width + "px";
+ if (this._clipScrollDiv) {
+ this._clipScrollDiv.style.width = width + "px";
+ }
+ /* Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset. */
+ scroll = this._getScroll();
+ var rulerHeight = clientHeight + viewPad.top + viewPad.bottom;
+ this._updateRulerSize(this._leftDiv, rulerHeight);
+ this._updateRulerSize(this._rightDiv, rulerHeight);
+ }
+ var left = scroll.x;
+ var clipDiv = this._clipDiv;
+ var overlayDiv = this._overlayDiv;
+ var clipLeft, clipTop;
+ if (clipDiv) {
+ clipDiv.scrollLeft = left;
+ clipLeft = leftWidth + viewPad.left;
+ clipTop = viewPad.top;
+ var clipWidth = clientWidth;
+ var clipHeight = clientHeight;
+ var clientLeft = 0, clientTop = -top;
+ if (scroll.x === 0) {
+ clipLeft -= viewPad.left;
+ clipWidth += viewPad.left;
+ clientLeft = viewPad.left;
+ }
+ if (scroll.x + clientWidth === scrollWidth) {
+ clipWidth += viewPad.right;
+ }
+ if (scroll.y === 0) {
+ clipTop -= viewPad.top;
+ clipHeight += viewPad.top;
+ clientTop += viewPad.top;
+ }
+ if (scroll.y + clientHeight === scrollHeight) {
+ clipHeight += viewPad.bottom;
+ }
+ clipDiv.style.left = clipLeft + "px";
+ clipDiv.style.top = clipTop + "px";
+ clipDiv.style.width = clipWidth + "px";
+ clipDiv.style.height = clipHeight + "px";
+ clientDiv.style.left = clientLeft + "px";
+ clientDiv.style.top = clientTop + "px";
+ clientDiv.style.width = scrollWidth + "px";
+ clientDiv.style.height = (clientHeight + top) + "px";
+ if (overlayDiv) {
+ overlayDiv.style.left = clientDiv.style.left;
+ overlayDiv.style.top = clientDiv.style.top;
+ overlayDiv.style.width = clientDiv.style.width;
+ overlayDiv.style.height = clientDiv.style.height;
+ }
+ } else {
+ clipLeft = left;
+ clipTop = top;
+ var clipRight = left + clientWidth;
+ var clipBottom = top + clientHeight;
+ if (clipLeft === 0) { clipLeft -= viewPad.left; }
+ if (clipTop === 0) { clipTop -= viewPad.top; }
+ if (clipRight === scrollWidth) { clipRight += viewPad.right; }
+ if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; }
+ clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)";
+ clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px";
+ clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px";
+ if (!hScrollOnly) {
+ clientDiv.style.top = (-top + viewPad.top) + "px";
+ clientDiv.style.height = (clientHeight + top) + "px";
+ }
+ if (overlayDiv) {
+ overlayDiv.style.clip = clientDiv.style.clip;
+ overlayDiv.style.left = clientDiv.style.left;
+ overlayDiv.style.width = clientDiv.style.width;
+ if (!hScrollOnly) {
+ overlayDiv.style.top = clientDiv.style.top;
+ overlayDiv.style.height = clientDiv.style.height;
+ }
+ }
+ }
+ this._updateDOMSelection();
+
+ /*
+ * If the client height changed during the update page it means that scrollbar has either been shown or hidden.
+ * When this happens update page has to run again to ensure that the top and bottom lines div are correct.
+ *
+ * Note: On IE, updateDOMSelection() has to be called before getting the new client height because it
+ * forces the client area to be recomputed.
+ */
+ var ensureCaretVisible = this._ensureCaretVisible;
+ this._ensureCaretVisible = false;
+ if (clientHeight !== this._getClientHeight()) {
+ this._updatePage();
+ if (ensureCaretVisible) {
+ this._showCaret();
+ }
+ }
+ if (isPad) {
+ var self = this;
+ setTimeout(function() {self._resizeTouchDiv();}, 0);
+ }
+ },
+ _updateRulerSize: function (divRuler, rulerHeight) {
+ if (!divRuler) { return; }
+ var partialY = this._partialY;
+ var lineHeight = this._getLineHeight();
+ var cells = divRuler.firstChild.rows[0].cells;
+ for (var i = 0; i < cells.length; i++) {
+ var div = cells[i].firstChild;
+ var offset = lineHeight;
+ if (div._ruler.getOverview() === "page") { offset += partialY; }
+ div.style.top = -offset + "px";
+ div.style.height = (rulerHeight + offset) + "px";
+ div = div.nextSibling;
+ }
+ divRuler.style.height = rulerHeight + "px";
+ },
+ _updateRuler: function (divRuler, topIndex, bottomIndex) {
+ if (!divRuler) { return; }
+ var cells = divRuler.firstChild.rows[0].cells;
+ var lineHeight = this._getLineHeight();
+ var parentDocument = this._frameDocument;
+ var viewPad = this._getViewPadding();
+ for (var i = 0; i < cells.length; i++) {
+ var div = cells[i].firstChild;
+ var ruler = div._ruler;
+ if (div.rulerChanged) {
+ this._applyStyle(ruler.getRulerStyle(), div);
+ }
+
+ var widthDiv;
+ var child = div.firstChild;
+ if (child) {
+ widthDiv = child;
+ child = child.nextSibling;
+ } else {
+ widthDiv = parentDocument.createElement("DIV");
+ widthDiv.style.visibility = "hidden";
+ div.appendChild(widthDiv);
+ }
+ var lineIndex, annotation;
+ if (div.rulerChanged) {
+ if (widthDiv) {
+ lineIndex = -1;
+ annotation = ruler.getWidestAnnotation();
+ if (annotation) {
+ this._applyStyle(annotation.style, widthDiv);
+ if (annotation.html) {
+ widthDiv.innerHTML = annotation.html;
+ }
+ }
+ widthDiv.lineIndex = lineIndex;
+ widthDiv.style.height = (lineHeight + viewPad.top) + "px";
+ }
+ }
+
+ var overview = ruler.getOverview(), lineDiv, frag, annotations;
+ if (overview === "page") {
+ annotations = ruler.getAnnotations(topIndex, bottomIndex + 1);
+ while (child) {
+ lineIndex = child.lineIndex;
+ var nextChild = child.nextSibling;
+ if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) {
+ div.removeChild(child);
+ }
+ child = nextChild;
+ }
+ child = div.firstChild.nextSibling;
+ frag = parentDocument.createDocumentFragment();
+ for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) {
+ if (!child || child.lineIndex > lineIndex) {
+ lineDiv = parentDocument.createElement("DIV");
+ annotation = annotations[lineIndex];
+ if (annotation) {
+ this._applyStyle(annotation.style, lineDiv);
+ if (annotation.html) {
+ lineDiv.innerHTML = annotation.html;
+ }
+ lineDiv.annotation = annotation;
+ }
+ lineDiv.lineIndex = lineIndex;
+ lineDiv.style.height = lineHeight + "px";
+ frag.appendChild(lineDiv);
+ } else {
+ if (frag.firstChild) {
+ div.insertBefore(frag, child);
+ frag = parentDocument.createDocumentFragment();
+ }
+ if (child) {
+ child = child.nextSibling;
+ }
+ }
+ }
+ if (frag.firstChild) { div.insertBefore(frag, child); }
+ } else {
+ var buttonHeight = isPad ? 0 : 17;
+ var clientHeight = this._getClientHeight ();
+ var lineCount = this._model.getLineCount ();
+ var contentHeight = lineHeight * lineCount;
+ var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
+ var divHeight;
+ if (contentHeight < trackHeight) {
+ divHeight = lineHeight;
+ } else {
+ divHeight = trackHeight / lineCount;
+ }
+ if (div.rulerChanged) {
+ var count = div.childNodes.length;
+ while (count > 1) {
+ div.removeChild(div.lastChild);
+ count--;
+ }
+ annotations = ruler.getAnnotations(0, lineCount);
+ frag = parentDocument.createDocumentFragment();
+ for (var prop in annotations) {
+ lineIndex = prop >>> 0;
+ if (lineIndex < 0) { continue; }
+ lineDiv = parentDocument.createElement("DIV");
+ annotation = annotations[prop];
+ this._applyStyle(annotation.style, lineDiv);
+ lineDiv.style.position = "absolute";
+ lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px";
+ if (annotation.html) {
+ lineDiv.innerHTML = annotation.html;
+ }
+ lineDiv.annotation = annotation;
+ lineDiv.lineIndex = lineIndex;
+ frag.appendChild(lineDiv);
+ }
+ div.appendChild(frag);
+ } else if (div._oldTrackHeight !== trackHeight) {
+ lineDiv = div.firstChild ? div.firstChild.nextSibling : null;
+ while (lineDiv) {
+ lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px";
+ lineDiv = lineDiv.nextSibling;
+ }
+ }
+ div._oldTrackHeight = trackHeight;
+ }
+ div.rulerChanged = false;
+ div = div.nextSibling;
+ }
+ },
+ _updateStyle: function () {
+ var document = this._frameDocument;
+ if (isIE) {
+ document.body.style.lineHeight = "normal";
+ }
+ this._lineHeight = this._calculateLineHeight();
+ this._viewPadding = this._calculatePadding();
+ if (isIE) {
+ document.body.style.lineHeight = this._lineHeight + "px";
+ }
+ this.redraw();
+ }
+ };//end prototype
+ mEventTarget.EventTarget.addMixin(TextView.prototype);
+
+ return {TextView: TextView};
+});
+
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/textDND", [], function() {
+
+ function TextDND(view, undoStack) {
+ this._view = view;
+ this._undoStack = undoStack;
+ this._dragSelection = null;
+ this._dropOffset = -1;
+ this._dropText = null;
+ var self = this;
+ this._listener = {
+ onDragStart: function (evt) {
+ self._onDragStart(evt);
+ },
+ onDragEnd: function (evt) {
+ self._onDragEnd(evt);
+ },
+ onDragEnter: function (evt) {
+ self._onDragEnter(evt);
+ },
+ onDragOver: function (evt) {
+ self._onDragOver(evt);
+ },
+ onDrop: function (evt) {
+ self._onDrop(evt);
+ },
+ onDestroy: function (evt) {
+ self._onDestroy(evt);
+ }
+ };
+ view.addEventListener("DragStart", this._listener.onDragStart);
+ view.addEventListener("DragEnd", this._listener.onDragEnd);
+ view.addEventListener("DragEnter", this._listener.onDragEnter);
+ view.addEventListener("DragOver", this._listener.onDragOver);
+ view.addEventListener("Drop", this._listener.onDrop);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ }
+ TextDND.prototype = {
+ destroy: function() {
+ var view = this._view;
+ if (!view) { return; }
+ view.removeEventListener("DragStart", this._listener.onDragStart);
+ view.removeEventListener("DragEnd", this._listener.onDragEnd);
+ view.removeEventListener("DragEnter", this._listener.onDragEnter);
+ view.removeEventListener("DragOver", this._listener.onDragOver);
+ view.removeEventListener("Drop", this._listener.onDrop);
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ this._view = null;
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onDragStart: function(e) {
+ var view = this._view;
+ var selection = view.getSelection();
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ selection.start = model.mapOffset(selection.start);
+ selection.end = model.mapOffset(selection.end);
+ model = model.getBaseModel();
+ }
+ var text = model.getText(selection.start, selection.end);
+ if (text) {
+ this._dragSelection = selection;
+ e.event.dataTransfer.effectAllowed = "copyMove";
+ e.event.dataTransfer.setData("Text", text);
+ }
+ },
+ _onDragEnd: function(e) {
+ var view = this._view;
+ if (this._dragSelection) {
+ if (this._undoStack) { this._undoStack.startCompoundChange(); }
+ var move = e.event.dataTransfer.dropEffect === "move";
+ if (move) {
+ view.setText("", this._dragSelection.start, this._dragSelection.end);
+ }
+ if (this._dropText) {
+ var text = this._dropText;
+ var offset = this._dropOffset;
+ if (move) {
+ if (offset >= this._dragSelection.end) {
+ offset -= this._dragSelection.end - this._dragSelection.start;
+ } else if (offset >= this._dragSelection.start) {
+ offset = this._dragSelection.start;
+ }
+ }
+ view.setText(text, offset, offset);
+ view.setSelection(offset, offset + text.length);
+ this._dropText = null;
+ this._dropOffset = -1;
+ }
+ if (this._undoStack) { this._undoStack.endCompoundChange(); }
+ }
+ this._dragSelection = null;
+ },
+ _onDragEnter: function(e) {
+ this._onDragOver(e);
+ },
+ _onDragOver: function(e) {
+ var types = e.event.dataTransfer.types;
+ if (types) {
+ var allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain") !== -1;
+ if (!allowed) {
+ e.event.dataTransfer.dropEffect = "none";
+ }
+ }
+ },
+ _onDrop: function(e) {
+ var view = this._view;
+ var text = e.event.dataTransfer.getData("Text");
+ if (text) {
+ var offset = view.getOffsetAtLocation(e.x, e.y);
+ if (this._dragSelection) {
+ this._dropOffset = offset;
+ this._dropText = text;
+ } else {
+ view.setText(text, offset, offset);
+ view.setSelection(offset, offset + text.length);
+ }
+ }
+ }
+ };
+
+ return {TextDND: TextDND};
+});/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*jslint */
+/*global define */
+
+define("orion/editor/htmlGrammar", [], function() {
+
+ /**
+ * Provides a grammar that can do some very rough syntax highlighting for HTML.
+ * @class orion.syntax.HtmlGrammar
+ */
+ function HtmlGrammar() {
+ /**
+ * Object containing the grammar rules.
+ * @public
+ * @type Object
+ */
+ return {
+ "name": "HTML",
+ "scopeName": "source.html",
+ "uuid": "3B5C76FB-EBB5-D930-F40C-047D082CE99B",
+ "patterns": [
+ // TODO unicode?
+ {
+ "match": "<!(doctype|DOCTYPE)[^>]+>",
+ "name": "entity.name.tag.doctype.html"
+ },
+ {
+ "begin": "<!--",
+ "end": "-->",
+ "beginCaptures": {
+ "0": { "name": "punctuation.definition.comment.html" }
+ },
+ "endCaptures": {
+ "0": { "name": "punctuation.definition.comment.html" }
+ },
+ "patterns": [
+ {
+ "match": "--",
+ "name": "invalid.illegal.badcomment.html"
+ }
+ ],
+ "contentName": "comment.block.html"
+ },
+ { // startDelimiter + tagName
+ "match": "<[A-Za-z0-9_\\-:]+(?= ?)",
+ "name": "entity.name.tag.html"
+ },
+ { "include": "#attrName" },
+ { "include": "#qString" },
+ { "include": "#qqString" },
+ // TODO attrName, qString, qqString should be applied first while inside a tag
+ { // startDelimiter + slash + tagName + endDelimiter
+ "match": "</[A-Za-z0-9_\\-:]+>",
+ "name": "entity.name.tag.html"
+ },
+ { // end delimiter of open tag
+ "match": ">",
+ "name": "entity.name.tag.html"
+ } ],
+ "repository": {
+ "attrName": { // attribute name
+ "match": "[A-Za-z\\-:]+(?=\\s*=\\s*['\"])",
+ "name": "entity.other.attribute.name.html"
+ },
+ "qqString": { // double quoted string
+ "match": "(\")[^\"]+(\")",
+ "name": "string.quoted.double.html"
+ },
+ "qString": { // single quoted string
+ "match": "(')[^']+(\')",
+ "name": "string.quoted.single.html"
+ }
+ }
+ };
+ }
+
+ return {HtmlGrammar: HtmlGrammar};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*jslint regexp:false laxbreak:true*/
+/*global define */
+
+define("orion/editor/textMateStyler", ['orion/editor/regex'], function(mRegex) {
+
+var RegexUtil = {
+ // Rules to detect some unsupported Oniguruma features
+ unsupported: [
+ {regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }},
+ {regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }},
+ {regex: /\(\?>/, func: function(match) { return "atomic group"; }}
+ ],
+
+ /**
+ * @param {String} str String giving a regular expression pattern from a TextMate grammar.
+ * @param {String} [flags] [ismg]+
+ * @returns {RegExp}
+ */
+ toRegExp: function(str) {
+ function fail(feature, match) {
+ throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: "
+ + match.index + " in " + match.input);
+ }
+ // Turns an extended regex pattern into a normal one
+ function normalize(/**String*/ str) {
+ var result = "";
+ var insideCharacterClass = false;
+ var len = str.length;
+ for (var i=0; i < len; ) {
+ var chr = str[i];
+ if (!insideCharacterClass && chr === "#") {
+ // skip to eol
+ while (i < len && chr !== "\r" && chr !== "\n") {
+ chr = str[++i];
+ }
+ } else if (!insideCharacterClass && /\s/.test(chr)) {
+ // skip whitespace
+ while (i < len && /\s/.test(chr)) {
+ chr = str[++i];
+ }
+ } else if (chr === "\\") {
+ result += chr;
+ if (!/\s/.test(str[i+1])) {
+ result += str[i+1];
+ i += 1;
+ }
+ i += 1;
+ } else if (chr === "[") {
+ insideCharacterClass = true;
+ result += chr;
+ i += 1;
+ } else if (chr === "]") {
+ insideCharacterClass = false;
+ result += chr;
+ i += 1;
+ } else {
+ result += chr;
+ i += 1;
+ }
+ }
+ return result;
+ }
+
+ var flags = "";
+ var i;
+
+ // Handle global "x" flag (whitespace/comments)
+ str = RegexUtil.processGlobalFlag("x", str, function(subexp) {
+ return normalize(subexp);
+ });
+
+ // Handle global "i" flag (case-insensitive)
+ str = RegexUtil.processGlobalFlag("i", str, function(subexp) {
+ flags += "i";
+ return subexp;
+ });
+
+ // Check for remaining unsupported syntax
+ for (i=0; i < this.unsupported.length; i++) {
+ var match;
+ if ((match = this.unsupported[i].regex.exec(str))) {
+ fail(this.unsupported[i].func(match), match);
+ }
+ }
+
+ return new RegExp(str, flags);
+ },
+
+ /**
+ * Checks if flag applies to entire pattern. If so, obtains replacement string by calling processor
+ * on the unwrapped pattern. Handles 2 possible syntaxes: (?f)pat and (?f:pat)
+ */
+ processGlobalFlag: function(/**String*/ flag, /**String*/ str, /**Function*/ processor) {
+ function getMatchingCloseParen(/*String*/pat, /*Number*/start) {
+ var depth = 0,
+ len = pat.length,
+ flagStop = -1;
+ for (var i=start; i < len && flagStop === -1; i++) {
+ switch (pat[i]) {
+ case "\\":
+ i++; // escape: skip next char
+ break;
+ case "(":
+ depth++;
+ break;
+ case ")":
+ depth--;
+ if (depth === 0) {
+ flagStop = i;
+ }
+ break;
+ }
+ }
+ return flagStop;
+ }
+ var flag1 = "(?" + flag + ")",
+ flag2 = "(?" + flag + ":";
+ if (str.substring(0, flag1.length) === flag1) {
+ return processor(str.substring(flag1.length));
+ } else if (str.substring(0, flag2.length) === flag2) {
+ var flagStop = getMatchingCloseParen(str, 0);
+ if (flagStop < str.length-1) {
+ throw new Error("Only a " + flag2 + ") group that encloses the entire regex is supported in: " + str);
+ }
+ return processor(str.substring(flag2.length, flagStop));
+ }
+ return str;
+ },
+
+ hasBackReference: function(/**RegExp*/ regex) {
+ return (/\\\d+/).test(regex.source);
+ },
+
+ /** @returns {RegExp} A regex made by substituting any backreferences in <code>regex</code> for the value of the property
+ * in <code>sub</code> with the same name as the backreferenced group number. */
+ getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) {
+ escape = (typeof escape === "undefined") ? true : false;
+ var exploded = regex.source.split(/(\\\d+)/g);
+ var array = [];
+ for (var i=0; i < exploded.length; i++) {
+ var term = exploded[i];
+ var backrefMatch = /\\(\d+)/.exec(term);
+ if (backrefMatch) {
+ var text = sub[backrefMatch[1]] || "";
+ array.push(escape ? mRegex.escape(text) : text);
+ } else {
+ array.push(term);
+ }
+ }
+ return new RegExp(array.join(""));
+ },
+
+ /**
+ * Builds a version of <code>regex</code> with every non-capturing term converted into a capturing group. This is a workaround
+ * for JavaScript's lack of API to get the index at which a matched group begins in the input string.<p>
+ * Using the "groupified" regex, we can sum the lengths of matches from <i>consuming groups</i> 1..n-1 to obtain the
+ * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).</p>
+ * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/<br />
+ * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/
+ * @param {RegExp} regex The regex to groupify.
+ * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the
+ * properties of this object rather than the new group numbers of regex itself.
+ * <ul><li>[0] {RegExp} The groupified version of the input regex.</li>
+ * <li>[1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of <code>regex</code>
+ * and its value is the corresponding capturing group number of [0].</li>
+ * <li>[2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found
+ * as a property in this object, then it's a consuming group.</li></ul>
+ */
+ groupify: function(regex, backRefOld2NewMap) {
+ var NON_CAPTURING = 1,
+ CAPTURING = 2,
+ LOOKAHEAD = 3,
+ NEW_CAPTURING = 4;
+ var src = regex.source,
+ len = src.length;
+ var groups = [],
+ lookaheadDepth = 0,
+ newGroups = [],
+ oldGroupNumber = 1,
+ newGroupNumber = 1;
+ var result = [],
+ old2New = {},
+ consuming = {};
+ for (var i=0; i < len; i++) {
+ var curGroup = groups[groups.length-1];
+ var chr = src[i];
+ switch (chr) {
+ case "(":
+ // If we're in new capturing group, close it since ( signals end-of-term
+ if (curGroup === NEW_CAPTURING) {
+ groups.pop();
+ result.push(")");
+ newGroups[newGroups.length-1].end = i;
+ }
+ var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null;
+ if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") {
+ // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups
+ // as such, but any term inside them will become a new capturing group (unless it happens to
+ // also be inside a lookahead).
+ var groupType;
+ if (peek2 === "?:") {
+ groupType = NON_CAPTURING;
+ } else {
+ groupType = LOOKAHEAD;
+ lookaheadDepth++;
+ }
+ groups.push(groupType);
+ newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ });
+ result.push(chr);
+ result.push(peek2);
+ i += peek2.length;
+ } else {
+ groups.push(CAPTURING);
+ newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber });
+ result.push(chr);
+ if (lookaheadDepth === 0) {
+ consuming[newGroupNumber] = null;
+ }
+ old2New[oldGroupNumber] = newGroupNumber;
+ oldGroupNumber++;
+ newGroupNumber++;
+ }
+ break;
+ case ")":
+ var group = groups.pop();
+ if (group === LOOKAHEAD) { lookaheadDepth--; }
+ newGroups[newGroups.length-1].end = i;
+ result.push(chr);
+ break;
+ case "*":
+ case "+":
+ case "?":
+ case "}":
+ // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group
+ // enclosing the pair
+ var op = chr;
+ var prev = src[i-1],
+ prevIndex = i-1;
+ if (chr === "}") {
+ for (var j=i-1; src[j] !== "{" && j >= 0; j--) {}
+ prev = src[j-1];
+ prevIndex = j-1;
+ op = src.substring(j, i+1);
+ }
+ var lastGroup = newGroups[newGroups.length-1];
+ if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) {
+ // Shove in the new group's (, increment num/start in from [lastGroup.start .. end]
+ result.splice(lastGroup.start, 0, "(");
+ result.push(op);
+ result.push(")");
+ var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num };
+ for (var k=0; k < newGroups.length; k++) {
+ group = newGroups[k];
+ if (group.type === CAPTURING || group.type === NEW_CAPTURING) {
+ if (group.start >= lastGroup.start && group.end <= prevIndex) {
+ group.start += 1;
+ group.end += 1;
+ group.num = group.num + 1;
+ if (group.type === CAPTURING) {
+ old2New[group.oldNum] = group.num;
+ }
+ }
+ }
+ }
+ newGroups.push(newGroup);
+ newGroupNumber++;
+ break;
+ } else {
+ // Fallthrough to default
+ }
+ default:
+ if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) {
+ // Not in a capturing group, so make a new one to hold this term.
+ // Perf improvement: don't create the new group if we're inside a lookahead, since we don't
+ // care about them (nothing inside a lookahead actually consumes input so we don't need it)
+ if (lookaheadDepth === 0) {
+ groups.push(NEW_CAPTURING);
+ newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber });
+ result.push("(");
+ consuming[newGroupNumber] = null;
+ newGroupNumber++;
+ }
+ }
+ result.push(chr);
+ if (chr === "\\") {
+ var peek = src[i+1];
+ // Eat next so following iteration doesn't think it's a real special character
+ result.push(peek);
+ i += 1;
+ }
+ break;
+ }
+ }
+ while (groups.length) {
+ // Close any remaining new capturing groups
+ groups.pop();
+ result.push(")");
+ }
+ var newRegex = new RegExp(result.join(""));
+
+ // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided
+ var subst = {};
+ backRefOld2NewMap = backRefOld2NewMap || old2New;
+ for (var prop in backRefOld2NewMap) {
+ if (backRefOld2NewMap.hasOwnProperty(prop)) {
+ subst[prop] = "\\" + backRefOld2NewMap[prop];
+ }
+ }
+ newRegex = this.getSubstitutedRegex(newRegex, subst, false);
+
+ return [newRegex, old2New, consuming];
+ },
+
+ /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */
+ complexCaptures: function(capturesObj) {
+ if (!capturesObj) { return false; }
+ for (var prop in capturesObj) {
+ if (capturesObj.hasOwnProperty(prop)) {
+ if (prop !== "0") {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+};
+
+ /**
+ * @name orion.editor.TextMateStyler
+ * @class A styler that knows how to apply a subset of the TextMate grammar format to style a line.
+ *
+ * <h4>Styling from a grammar:</h4>
+ * <p>Each scope name given in the grammar is converted to an array of CSS class names. For example
+ * a region of text with scope <code>keyword.control.php</code> will be assigned the CSS classes<br />
+ * <code>keyword, keyword-control, keyword-control-php</code></p>
+ *
+ * <p>A CSS file can give rules matching any of these class names to provide generic or more specific styling.
+ * For example,</p>
+ * <p><code>.keyword { font-color: blue; }</code></p>
+ * <p>colors all keywords blue, while</p>
+ * <p><code>.keyword-control-php { font-weight: bold; }</code></p>
+ * <p>bolds only PHP control keywords.</p>
+ *
+ * <p>This is useful when using grammars that adhere to TextMate's
+ * <a href="http://manual.macromates.com/en/language_grammars.html#naming_conventions">scope name conventions</a>,
+ * as a single CSS rule can provide consistent styling to similar constructs across different languages.</p>
+ *
+ * <h4>Top-level grammar constructs:</h4>
+ * <ul><li><code>patterns, repository</code> (with limitations, see "Other Features") are supported.</li>
+ * <li><code>scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker</code> are <b>not</b> supported.</li>
+ * <li><code>fileTypes</code> is <b>not</b> supported. When using the Orion service registry, the "orion.edit.highlighter"
+ * service serves a similar purpose.</li>
+ * </ul>
+ *
+ * <h4>Regular expression constructs:</h4>
+ * <ul>
+ * <li><code>match</code> patterns are supported.</li>
+ * <li><code>begin .. end</code> patterns are supported.</li>
+ * <li>The "extended" regex forms <code>(?x)</code> and <code>(?x:...)</code> are supported, but <b>only</b> when they
+ * apply to the entire regex pattern.</li>
+ * <li>Matching is done using native JavaScript <code>RegExp</code>s. As a result, many features of the Oniguruma regex
+ * engine used by TextMate are <b>not</b> supported.
+ * Unsupported features include:
+ * <ul><li>Named captures</li>
+ * <li>Setting flags inside subgroups (eg. <code>(?i:a)b</code>)</li>
+ * <li>Lookbehind and negative lookbehind</li>
+ * <li>Subexpression call</li>
+ * <li>etc.</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * <h4>Scope-assignment constructs:</h4>
+ * <ul>
+ * <li><code>captures, beginCaptures, endCaptures</code> are supported.</li>
+ * <li><code>name</code> and <code>contentName</code> are supported.</li>
+ * </ul>
+ *
+ * <h4>Other features:</h4>
+ * <ul>
+ * <li><code>applyEndPatternLast</code> is supported.</li>
+ * <li><code>include</code> is supported, but only when it references a rule in the current grammar's <code>repository</code>.
+ * Including <code>$self</code>, <code>$base</code>, or <code>rule.from.another.grammar</code> is <b>not</b> supported.</li>
+ * </ul>
+ *
+ * @description Creates a new TextMateStyler.
+ * @extends orion.editor.AbstractStyler
+ * @param {orion.textview.TextView} textView The <code>TextView</code> to provide styling for.
+ * @param {Object} grammar The TextMate grammar to use for styling the <code>TextView</code>, as a JavaScript object. You can
+ * produce this object by running a PList-to-JavaScript conversion tool on a TextMate <code>.tmLanguage</code> file.
+ * @param {Object[]} [externalGrammars] Additional grammar objects that will be used to resolve named rule references.
+ */
+ function TextMateStyler(textView, grammar, externalGrammars) {
+ this.initialize(textView);
+ // Copy grammar object(s) since we will mutate them
+ this.grammar = this.copy(grammar);
+ this.externalGrammars = externalGrammars ? this.copy(externalGrammars) : [];
+
+ this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */
+ this._tree = null;
+ this._allGrammars = {}; /* key: {String} scopeName of grammar, value: {Object} grammar */
+ this.preprocess(this.grammar);
+ }
+ TextMateStyler.prototype = /** @lends orion.editor.TextMateStyler.prototype */ {
+ initialize: function(textView) {
+ this.textView = textView;
+ var self = this;
+ this._listener = {
+ onModelChanged: function(e) {
+ self.onModelChanged(e);
+ },
+ onDestroy: function(e) {
+ self.onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self.onLineStyle(e);
+ }
+ };
+ textView.addEventListener("ModelChanged", this._listener.onModelChanged);
+ textView.addEventListener("Destroy", this._listener.onDestroy);
+ textView.addEventListener("LineStyle", this._listener.onLineStyle);
+ textView.redrawLines();
+ },
+ onDestroy: function(/**eclipse.DestroyEvent*/ e) {
+ this.destroy();
+ },
+ destroy: function() {
+ if (this.textView) {
+ this.textView.removeEventListener("ModelChanged", this._listener.onModelChanged);
+ this.textView.removeEventListener("Destroy", this._listener.onDestroy);
+ this.textView.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.textView = null;
+ }
+ this.grammar = null;
+ this._styles = null;
+ this._tree = null;
+ this._listener = null;
+ },
+ /** @private */
+ copy: function(obj) {
+ return JSON.parse(JSON.stringify(obj));
+ },
+ /** @private */
+ preprocess: function(grammar) {
+ var stack = [grammar];
+ for (; stack.length !== 0; ) {
+ var rule = stack.pop();
+ if (rule._resolvedRule && rule._typedRule) {
+ continue;
+ }
+// console.debug("Process " + (rule.include || rule.name));
+
+ // Look up include'd rule, create typed *Rule instance
+ rule._resolvedRule = this._resolve(rule);
+ rule._typedRule = this._createTypedRule(rule);
+
+ // Convert the scope names to styles and cache them for later
+ this.addStyles(rule.name);
+ this.addStyles(rule.contentName);
+ this.addStylesForCaptures(rule.captures);
+ this.addStylesForCaptures(rule.beginCaptures);
+ this.addStylesForCaptures(rule.endCaptures);
+
+ if (rule._resolvedRule !== rule) {
+ // Add include target
+ stack.push(rule._resolvedRule);
+ }
+ if (rule.patterns) {
+ // Add subrules
+ for (var i=0; i < rule.patterns.length; i++) {
+ stack.push(rule.patterns[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Adds eclipse.Style objects for scope to our _styles cache.
+ * @param {String} scope A scope name, like "constant.character.php".
+ */
+ addStyles: function(scope) {
+ if (scope && !this._styles[scope]) {
+ this._styles[scope] = [];
+ var scopeArray = scope.split(".");
+ for (var i = 0; i < scopeArray.length; i++) {
+ this._styles[scope].push(scopeArray.slice(0, i + 1).join("-"));
+ }
+ }
+ },
+ /** @private */
+ addStylesForCaptures: function(/**Object*/ captures) {
+ for (var prop in captures) {
+ if (captures.hasOwnProperty(prop)) {
+ var scope = captures[prop].name;
+ this.addStyles(scope);
+ }
+ }
+ },
+ /**
+ * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end".
+ * Also handles top level of grammar.
+ * @private
+ */
+ ContainerRule: (function() {
+ function ContainerRule(/**Object*/ rule) {
+ this.rule = rule;
+ this.subrules = rule.patterns;
+ }
+ ContainerRule.prototype.valueOf = function() { return "aa"; };
+ return ContainerRule;
+ }()),
+ /**
+ * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of
+ * lines. This type of rule may contain subrules, which apply only inside the begin .. end region.
+ * @private
+ */
+ BeginEndRule: (function() {
+ function BeginEndRule(/**Object*/ rule) {
+ this.rule = rule;
+ // TODO: the TextMate blog claims that "end" is optional.
+ this.beginRegex = RegexUtil.toRegExp(rule.begin);
+ this.endRegex = RegexUtil.toRegExp(rule.end);
+ this.subrules = rule.patterns || [];
+
+ this.endRegexHasBackRef = RegexUtil.hasBackReference(this.endRegex);
+
+ // Deal with non-0 captures
+ var complexCaptures = RegexUtil.complexCaptures(rule.captures);
+ var complexBeginEnd = RegexUtil.complexCaptures(rule.beginCaptures) || RegexUtil.complexCaptures(rule.endCaptures);
+ this.isComplex = complexCaptures || complexBeginEnd;
+ if (this.isComplex) {
+ var bg = RegexUtil.groupify(this.beginRegex);
+ this.beginRegex = bg[0];
+ this.beginOld2New = bg[1];
+ this.beginConsuming = bg[2];
+
+ var eg = RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/);
+ this.endRegex = eg[0];
+ this.endOld2New = eg[1];
+ this.endConsuming = eg[2];
+ }
+ }
+ BeginEndRule.prototype.valueOf = function() { return this.beginRegex; };
+ return BeginEndRule;
+ }()),
+ /**
+ * A rule with a "match" pattern.
+ * @private
+ */
+ MatchRule: (function() {
+ function MatchRule(/**Object*/ rule) {
+ this.rule = rule;
+ this.matchRegex = RegexUtil.toRegExp(rule.match);
+ this.isComplex = RegexUtil.complexCaptures(rule.captures);
+ if (this.isComplex) {
+ var mg = RegexUtil.groupify(this.matchRegex);
+ this.matchRegex = mg[0];
+ this.matchOld2New = mg[1];
+ this.matchConsuming = mg[2];
+ }
+ }
+ MatchRule.prototype.valueOf = function() { return this.matchRegex; };
+ return MatchRule;
+ }()),
+ /**
+ * @param {Object} rule A rule from the grammar.
+ * @returns {MatchRule|BeginEndRule|ContainerRule}
+ * @private
+ */
+ _createTypedRule: function(rule) {
+ if (rule.match) {
+ return new this.MatchRule(rule);
+ } else if (rule.begin) {
+ return new this.BeginEndRule(rule);
+ } else {
+ return new this.ContainerRule(rule);
+ }
+ },
+ /**
+ * Resolves a rule from the grammar (which may be an include) into the real rule that it points to.
+ * @private
+ */
+ _resolve: function(rule) {
+ var resolved = rule;
+ if (rule.include) {
+ if (rule.begin || rule.end || rule.match) {
+ throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include);
+ }
+ var name = rule.include;
+ if (name[0] === "#") {
+ resolved = this.grammar.repository && this.grammar.repository[name.substring(1)];
+ if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); }
+ } else if (name === "$self") {
+ resolved = this.grammar;
+ } else if (name === "$base") {
+ // $base is only relevant when including rules from foreign grammars
+ throw new Error("Include \"$base\" is not supported");
+ } else {
+ resolved = this._allGrammars[name];
+ if (!resolved) {
+ for (var i=0; i < this.externalGrammars.length; i++) {
+ var grammar = this.externalGrammars[i];
+ if (grammar.scopeName === name) {
+ this.preprocess(grammar);
+ this._allGrammars[name] = grammar;
+ resolved = grammar;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return resolved;
+ },
+ /** @private */
+ ContainerNode: (function() {
+ function ContainerNode(parent, rule) {
+ this.parent = parent;
+ this.rule = rule;
+ this.children = [];
+
+ this.start = null;
+ this.end = null;
+ }
+ ContainerNode.prototype.addChild = function(child) {
+ this.children.push(child);
+ };
+ ContainerNode.prototype.valueOf = function() {
+ var r = this.rule;
+ return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}";
+ };
+ return ContainerNode;
+ }()),
+ /** @private */
+ BeginEndNode: (function() {
+ function BeginEndNode(parent, rule, beginMatch) {
+ this.parent = parent;
+ this.rule = rule;
+ this.children = [];
+
+ this.setStart(beginMatch);
+ this.end = null; // will be set eventually during parsing (may be EOF)
+ this.endMatch = null; // may remain null if we never match our "end" pattern
+
+ // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch
+ if (rule.endRegexHasBackRef) {
+ this.endRegexSubstituted = RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch);
+ } else {
+ this.endRegexSubstituted = null;
+ }
+ }
+ BeginEndNode.prototype.addChild = function(child) {
+ this.children.push(child);
+ };
+ /** @return {Number} This node's index in its parent's "children" list */
+ BeginEndNode.prototype.getIndexInParent = function(node) {
+ return this.parent ? this.parent.children.indexOf(this) : -1;
+ };
+ /** @param {RegExp.match} beginMatch */
+ BeginEndNode.prototype.setStart = function(beginMatch) {
+ this.start = beginMatch.index;
+ this.beginMatch = beginMatch;
+ };
+ /** @param {RegExp.match|Number} endMatchOrLastChar */
+ BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) {
+ if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") {
+ var endMatch = endMatchOrLastChar;
+ this.endMatch = endMatch;
+ this.end = endMatch.index + endMatch[0].length;
+ } else {
+ var lastChar = endMatchOrLastChar;
+ this.endMatch = null;
+ this.end = lastChar;
+ }
+ };
+ BeginEndNode.prototype.shiftStart = function(amount) {
+ this.start += amount;
+ this.beginMatch.index += amount;
+ };
+ BeginEndNode.prototype.shiftEnd = function(amount) {
+ this.end += amount;
+ if (this.endMatch) { this.endMatch.index += amount; }
+ };
+ BeginEndNode.prototype.valueOf = function() {
+ return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}";
+ };
+ return BeginEndNode;
+ }()),
+ /** Pushes rules onto stack such that rules[startFrom] is on top
+ * @private
+ */
+ push: function(/**Array*/ stack, /**Array*/ rules) {
+ if (!rules) { return; }
+ for (var i = rules.length; i > 0; ) {
+ stack.push(rules[--i]);
+ }
+ },
+ /** Executes <code>regex</code> on <code>text</code>, and returns the match object with its index
+ * offset by the given amount.
+ * @returns {RegExp.match}
+ * @private
+ */
+ exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) {
+ var match = regex.exec(text);
+ if (match) { match.index += offset; }
+ regex.lastIndex = 0; // Just in case
+ return match;
+ },
+ /** @returns {Number} The position immediately following the match.
+ * @private
+ */
+ afterMatch: function(/**RegExp.match*/ match) {
+ return match.index + match[0].length;
+ },
+ /**
+ * @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text.
+ * @private
+ */
+ getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) {
+ if (node instanceof this.BeginEndNode) {
+ var rule = node.rule;
+ var endRegex = node.endRegexSubstituted || rule.endRegex;
+ if (!endRegex) { return null; }
+ return this.exec(endRegex, text, offset);
+ }
+ return null;
+ },
+ /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter
+ * as buffer is modified.
+ * @private
+ */
+ initialParse: function() {
+ var last = this.textView.getModel().getCharCount();
+ // First time; make parse tree for whole buffer
+ var root = new this.ContainerNode(null, this.grammar._typedRule);
+ this._tree = root;
+ this.parse(this._tree, false, 0);
+ },
+ onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {
+ var addedCharCount = e.addedCharCount,
+ addedLineCount = e.addedLineCount,
+ removedCharCount = e.removedCharCount,
+ removedLineCount = e.removedLineCount,
+ start = e.start;
+ if (!this._tree) {
+ this.initialParse();
+ } else {
+ var model = this.textView.getModel();
+ var charCount = model.getCharCount();
+
+ // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's
+ // line since it may've been changed in a way that would cause a new beginMatch at its lineStart.
+ var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0
+ var fd = this.getFirstDamaged(rs, rs);
+ rs = rs === -1 ? 0 : rs;
+ var stoppedAt;
+ if (fd) {
+ // [rs, re] is the region we need to verify. If we find the structure of the tree
+ // has changed in that area, then we may need to reparse the rest of the file.
+ stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount);
+ } else {
+ // FIXME: fd == null ?
+ stoppedAt = charCount;
+ }
+ this.textView.redrawRange(rs, stoppedAt);
+ }
+ },
+ /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value)
+ * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any).
+ * @private
+ */
+ getFirstDamaged: function(start, end) {
+ // If start === 0 we actually have to start from the root because there is no position
+ // we can rely on. (First index is damaged)
+ if (start < 0) {
+ return this._tree;
+ }
+
+ var nodes = [this._tree];
+ var result = null;
+ while (nodes.length) {
+ var n = nodes.pop();
+ if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) {
+ // n is damaged by the edit, so go into its children
+ // Note: If a node is damaged, then some of its descendents MAY be damaged
+ // If a node is undamaged, then ALL of its descendents are undamaged
+ if (n instanceof this.BeginEndNode) {
+ result = n;
+ }
+ // Examine children[0] last
+ for (var i=0; i < n.children.length; i++) {
+ nodes.push(n.children[i]);
+ }
+ }
+ }
+ return result || this._tree;
+ },
+ /** @returns true If <code>n</code> overlaps the interval [start,end].
+ * @private
+ */
+ isDamaged: function(/**BeginEndNode*/ n, start, end) {
+ // Note strict > since [2,5] doesn't overlap [5,7]
+ return (n.start <= end && n.end > start);
+ },
+ /**
+ * Builds tree from some of the buffer content
+ *
+ * TODO cleanup params
+ * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root.
+ * @param {Boolean} repairing
+ * @param {Number} rs See _onModelChanged()
+ * @param {Number} [editStart] Only used for repairing === true
+ * @param {Number} [addedCharCount] Only used for repairing === true
+ * @param {Number} [removedCharCount] Only used for repairing === true
+ * @returns {Number} The end position that redrawRange should be called for.
+ * @private
+ */
+ parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) {
+ var model = this.textView.getModel();
+ var lastLineStart = model.getLineStart(model.getLineCount() - 1);
+ var eof = model.getCharCount();
+ var initialExpected = this.getInitialExpected(origNode, rs);
+
+ // re is best-case stopping point; if we detect change to tree, we must continue past it
+ var re = -1;
+ if (repairing) {
+ origNode.repaired = true;
+ origNode.endNeedsUpdate = true;
+ var lastChild = origNode.children[origNode.children.length-1];
+ var delta = addedCharCount - removedCharCount;
+ var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1;
+ var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount));
+ re = Math.max(lastChildLineEnd, editLineEnd);
+ }
+ re = (re === -1) ? eof : re;
+
+ var expected = initialExpected;
+ var node = origNode;
+ var matchedChildOrEnd = false;
+ var pos = rs;
+ var redrawEnd = -1;
+ while (node && (!repairing || (pos < re))) {
+ var matchInfo = this.getNextMatch(model, node, pos);
+ if (!matchInfo) {
+ // Go to next line, if any
+ pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1);
+ }
+ var match = matchInfo && matchInfo.match,
+ rule = matchInfo && matchInfo.rule,
+ isSub = matchInfo && matchInfo.isSub,
+ isEnd = matchInfo && matchInfo.isEnd;
+ if (isSub) {
+ pos = this.afterMatch(match);
+ if (rule instanceof this.BeginEndRule) {
+ matchedChildOrEnd = true;
+ // Matched a child. Did we expect that?
+ if (repairing && rule === expected.rule && node === expected.parent) {
+ // Yes: matched expected child
+ var foundChild = expected;
+ foundChild.setStart(match);
+ // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop
+ foundChild.repaired = true;
+ foundChild.endNeedsUpdate = true;
+ node = foundChild; // descend
+ expected = this.getNextExpected(expected, "begin");
+ } else {
+ if (repairing) {
+ // No: matched unexpected child.
+ this.prune(node, expected);
+ repairing = false;
+ }
+
+ // Add the new child (will replace 'expected' in node's children list)
+ var subNode = new this.BeginEndNode(node, rule, match);
+ node.addChild(subNode);
+ node = subNode; // descend
+ }
+ } else {
+ // Matched a MatchRule; no changes to tree required
+ }
+ } else if (isEnd || pos === eof) {
+ if (node instanceof this.BeginEndNode) {
+ if (match) {
+ matchedChildOrEnd = true;
+ redrawEnd = Math.max(redrawEnd, node.end); // if end moved up, must still redraw to its old value
+ node.setEnd(match);
+ pos = this.afterMatch(match);
+ // Matched node's end. Did we expect that?
+ if (repairing && node === expected && node.parent === expected.parent) {
+ // Yes: found the expected end of node
+ node.repaired = true;
+ delete node.endNeedsUpdate;
+ expected = this.getNextExpected(expected, "end");
+ } else {
+ if (repairing) {
+ // No: found an unexpected end
+ this.prune(node, expected);
+ repairing = false;
+ }
+ }
+ } else {
+ // Force-ending a BeginEndNode that runs until eof
+ node.setEnd(eof);
+ delete node.endNeedsUpdate;
+ }
+ }
+ node = node.parent; // ascend
+ }
+
+ if (repairing && pos >= re && !matchedChildOrEnd) {
+ // Reached re without matching any begin/end => initialExpected itself was removed => repair fail
+ this.prune(origNode, initialExpected);
+ repairing = false;
+ }
+ } // end loop
+ // TODO: do this for every node we end?
+ this.removeUnrepairedChildren(origNode, repairing, rs);
+
+ //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf");
+ this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount);
+ if (repairing) {
+ return Math.max(redrawEnd, pos);
+ } else {
+ return pos; // where we stopped reparsing
+ }
+ },
+ /** Helper for parse() in the repair case. To be called when ending a node, as any children that
+ * lie in [rs,node.end] and were not repaired must've been deleted.
+ * @private
+ */
+ removeUnrepairedChildren: function(node, repairing, start) {
+ if (repairing) {
+ var children = node.children;
+ var removeFrom = -1;
+ for (var i=0; i < children.length; i++) {
+ var child = children[i];
+ if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) {
+ removeFrom = i;
+ break;
+ }
+ }
+ if (removeFrom !== -1) {
+ node.children.length = removeFrom;
+ }
+ }
+ },
+ /** Helper for parse() in the repair case
+ * @private
+ */
+ cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) {
+ var i, node, maybeRepairedNodes;
+ if (repairing) {
+ // The repair succeeded, so update stale begin/end indices by simple translation.
+ var delta = addedCharCount - removedCharCount;
+ // A repaired node's end can't exceed re, but it may exceed re-delta+1.
+ // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag
+ var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof);
+ maybeRepairedNodes = this.getIntersecting(rs, re);
+ // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag
+ for (i=0; i < maybeUnrepairedNodes.length; i++) {
+ node = maybeUnrepairedNodes[i];
+ if (!node.repaired && node instanceof this.BeginEndNode) {
+ node.shiftEnd(delta);
+ node.shiftStart(delta);
+ }
+ }
+ // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re)
+ for (i=0; i < maybeRepairedNodes.length; i++) {
+ node = maybeRepairedNodes[i];
+ if (node.repaired && node.endNeedsUpdate) {
+ node.shiftEnd(delta);
+ }
+ delete node.endNeedsUpdate;
+ delete node.repaired;
+ }
+ } else {
+ // Clean up after ourself
+ maybeRepairedNodes = this.getIntersecting(rs, re);
+ for (i=0; i < maybeRepairedNodes.length; i++) {
+ delete maybeRepairedNodes[i].repaired;
+ }
+ }
+ },
+ /**
+ * @param model {orion.textview.TextModel}
+ * @param node {Node}
+ * @param pos {Number}
+ * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered.
+ * @returns {Object} A match info object with properties:
+ * {Boolean} isEnd
+ * {Boolean} isSub
+ * {RegExp.match} match
+ * {(Match|BeginEnd)Rule} rule
+ * @private
+ */
+ getNextMatch: function(model, node, pos, matchRulesOnly) {
+ var lineIndex = model.getLineAtOffset(pos);
+ var lineEnd = model.getLineEnd(lineIndex);
+ var line = model.getText(pos, lineEnd);
+
+ var stack = [],
+ expandedContainers = [],
+ subMatches = [],
+ subrules = [];
+ this.push(stack, node.rule.subrules);
+ while (stack.length) {
+ var next = stack.length ? stack.pop() : null;
+ var subrule = next && next._resolvedRule._typedRule;
+ if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) {
+ // Expand ContainerRule by pushing its subrules on
+ expandedContainers.push(subrule);
+ this.push(stack, subrule.subrules);
+ continue;
+ }
+ if (subrule && matchRulesOnly && !(subrule.matchRegex)) {
+ continue;
+ }
+ var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos);
+ if (subMatch) {
+ subMatches.push(subMatch);
+ subrules.push(subrule);
+ }
+ }
+
+ var bestSub = Number.MAX_VALUE,
+ bestSubIndex = -1;
+ for (var i=0; i < subMatches.length; i++) {
+ var match = subMatches[i];
+ if (match.index < bestSub) {
+ bestSub = match.index;
+ bestSubIndex = i;
+ }
+ }
+
+ if (!matchRulesOnly) {
+ // See if the "end" pattern of the active begin/end node matches.
+ // TODO: The active begin/end node may not be the same as the node that holds the subrules
+ var activeBENode = node;
+ var endMatch = this.getEndMatch(node, line, pos);
+ if (endMatch) {
+ var doEndLast = activeBENode.rule.applyEndPatternLast;
+ var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub);
+ if (endWins) {
+ return {isEnd: true, rule: activeBENode.rule, match: endMatch};
+ }
+ }
+ }
+ return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]};
+ },
+ /**
+ * Gets the node corresponding to the first match we expect to see in the repair.
+ * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root.
+ * @param {Number} rs See _onModelChanged()
+ * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or
+ * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines).
+ * This is the only time we rely on the start/end values from the pre-change tree. After this
+ * we only look at node ordering, never use the old indices.
+ * @returns {Node}
+ * @private
+ */
+ getInitialExpected: function(node, rs) {
+ // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes
+ var i, child;
+ if (node === this._tree) {
+ // get whichever of our children comes after rs
+ for (i=0; i < node.children.length; i++) {
+ child = node.children[i]; // BeginEndNode
+ if (child.start >= rs) {
+ return child;
+ }
+ }
+ } else if (node instanceof this.BeginEndNode) {
+ if (node.endMatch) {
+ // Which comes next after rs: our nodeEnd or one of our children?
+ var nodeEnd = node.endMatch.index;
+ for (i=0; i < node.children.length; i++) {
+ child = node.children[i]; // BeginEndNode
+ if (child.start >= rs) {
+ break;
+ }
+ }
+ if (child && child.start < nodeEnd) {
+ return child; // Expect child as the next match
+ }
+ } else {
+ // No endMatch => node goes until eof => it end should be the next match
+ }
+ }
+ return node; // We expect node to end, so it should be the next match
+ },
+ /**
+ * Helper for repair() to tell us what kind of event we expect next.
+ * @param {Node} expected Last value returned by this method.
+ * @param {String} event "begin" if the last value of expected was matched as "begin",
+ * or "end" if it was matched as an end.
+ * @returns {Node} The next expected node to match, or null.
+ * @private
+ */
+ getNextExpected: function(/**Node*/ expected, event) {
+ var node = expected;
+ if (event === "begin") {
+ var child = node.children[0];
+ if (child) {
+ return child;
+ } else {
+ return node;
+ }
+ } else if (event === "end") {
+ var parent = node.parent;
+ if (parent) {
+ var nextSibling = parent.children[parent.children.indexOf(node) + 1];
+ if (nextSibling) {
+ return nextSibling;
+ } else {
+ return parent;
+ }
+ }
+ }
+ return null;
+ },
+ /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing.
+ * @private
+ */
+ prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) {
+ var expectedAChild = expected.parent === node;
+ if (expectedAChild) {
+ // Expected child wasn't matched; prune it and all siblings after it
+ node.children.length = expected.getIndexInParent();
+ } else if (node instanceof this.BeginEndNode) {
+ // Expected node to end but it didn't; set its end unknown and we'll match it eventually
+ node.endMatch = null;
+ node.end = null;
+ }
+ // Reparsing from node, so prune the successors outside of node's subtree
+ if (node.parent) {
+ node.parent.children.length = node.getIndexInParent() + 1;
+ }
+ },
+ onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {
+ function byStart(r1, r2) {
+ return r1.start - r2.start;
+ }
+
+ if (!this._tree) {
+ // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here
+ this.initialParse();
+ }
+ var lineStart = e.lineStart,
+ model = this.textView.getModel(),
+ lineEnd = model.getLineEnd(e.lineIndex);
+
+ var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0
+ var node = this.getFirstDamaged(rs, rs);
+
+ var scopes = this.getLineScope(model, node, lineStart, lineEnd);
+ e.ranges = this.toStyleRanges(scopes);
+ // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored
+ e.ranges.sort(byStart);
+ },
+ /** Runs parse algorithm on [start, end] in the context of node, assigning scope as we find matches.
+ * @private
+ */
+ getLineScope: function(model, node, start, end) {
+ var pos = start;
+ var expected = this.getInitialExpected(node, start);
+ var scopes = [],
+ gaps = [];
+ while (node && (pos < end)) {
+ var matchInfo = this.getNextMatch(model, node, pos);
+ if (!matchInfo) {
+ break; // line is over
+ }
+ var match = matchInfo && matchInfo.match,
+ rule = matchInfo && matchInfo.rule,
+ isSub = matchInfo && matchInfo.isSub,
+ isEnd = matchInfo && matchInfo.isEnd;
+ if (match.index !== pos) {
+ // gap [pos..match.index]
+ gaps.push({ start: pos, end: match.index, node: node});
+ }
+ if (isSub) {
+ pos = this.afterMatch(match);
+ if (rule instanceof this.BeginEndRule) {
+ // Matched a "begin", assign its scope and descend into it
+ this.addBeginScope(scopes, match, rule);
+ node = expected; // descend
+ expected = this.getNextExpected(expected, "begin");
+ } else {
+ // Matched a child MatchRule;
+ this.addMatchScope(scopes, match, rule);
+ }
+ } else if (isEnd) {
+ pos = this.afterMatch(match);
+ // Matched and "end", assign its end scope and go up
+ this.addEndScope(scopes, match, rule);
+ expected = this.getNextExpected(expected, "end");
+ node = node.parent; // ascend
+ }
+ }
+ if (pos < end) {
+ gaps.push({ start: pos, end: end, node: node });
+ }
+ var inherited = this.getInheritedLineScope(gaps, start, end);
+ return scopes.concat(inherited);
+ },
+ /** @private */
+ getInheritedLineScope: function(gaps, start, end) {
+ var scopes = [];
+ for (var i=0; i < gaps.length; i++) {
+ var gap = gaps[i];
+ var node = gap.node;
+ while (node) {
+ // if node defines a contentName or name, apply it
+ var rule = node.rule.rule;
+ var name = rule.name,
+ contentName = rule.contentName;
+ // TODO: if both are given, we don't resolve the conflict. contentName always wins
+ var scope = contentName || name;
+ if (scope) {
+ this.addScopeRange(scopes, gap.start, gap.end, scope);
+ break;
+ }
+ node = node.parent;
+ }
+ }
+ return scopes;
+ },
+ /** @private */
+ addBeginScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule;
+ this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming);
+ },
+ /** @private */
+ addEndScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule;
+ this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming);
+ },
+ /** @private */
+ addMatchScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule,
+ name = rule.name,
+ captures = rule.captures;
+ if (captures) {
+ // captures takes priority over name
+ this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming);
+ } else {
+ this.addScope(scopes, match, name);
+ }
+ },
+ /** @private */
+ addScope: function(scopes, match, name) {
+ if (!name) { return; }
+ scopes.push({start: match.index, end: this.afterMatch(match), scope: name });
+ },
+ /** @private */
+ addScopeRange: function(scopes, start, end, name) {
+ if (!name) { return; }
+ scopes.push({start: start, end: end, scope: name });
+ },
+ /** @private */
+ addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) {
+ if (!captures) { return; }
+ if (!isComplex) {
+ this.addScope(scopes, match, captures[0] && captures[0].name);
+ } else {
+ // apply scopes captures[1..n] to matching groups [1]..[n] of match
+
+ // Sum up the lengths of preceding consuming groups to get the start offset for each matched group.
+ var newGroupStarts = {1: 0};
+ var sum = 0;
+ for (var num = 1; match[num] !== undefined; num++) {
+ if (consuming[num] !== undefined) {
+ sum += match[num].length;
+ }
+ if (match[num+1] !== undefined) {
+ newGroupStarts[num + 1] = sum;
+ }
+ }
+ // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range.
+ var start = match.index;
+ for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) {
+ var scope = captures[oldGroupNum].name;
+ var newGroupNum = old2New[oldGroupNum];
+ var groupStart = start + newGroupStarts[newGroupNum];
+ // Not every capturing group defined in regex need match every time the regex is run.
+ // eg. (a)|b matches "b" but group 1 is undefined
+ if (typeof match[newGroupNum] !== "undefined") {
+ var groupEnd = groupStart + match[newGroupNum].length;
+ this.addScopeRange(scopes, groupStart, groupEnd, scope);
+ }
+ }
+ }
+ },
+ /** @returns {Node[]} In depth-first order
+ * @private
+ */
+ getIntersecting: function(start, end) {
+ var result = [];
+ var nodes = this._tree ? [this._tree] : [];
+ while (nodes.length) {
+ var n = nodes.pop();
+ var visitChildren = false;
+ if (n instanceof this.ContainerNode) {
+ visitChildren = true;
+ } else if (this.isDamaged(n, start, end)) {
+ visitChildren = true;
+ result.push(n);
+ }
+ if (visitChildren) {
+ var len = n.children.length;
+// for (var i=len-1; i >= 0; i--) {
+// nodes.push(n.children[i]);
+// }
+ for (var i=0; i < len; i++) {
+ nodes.push(n.children[i]);
+ }
+ }
+ }
+ return result.reverse();
+ },
+ /**
+ * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line.
+ * @returns eclipse.StyleRange[]
+ * @private
+ */
+ toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) {
+ var styleRanges = [];
+ for (var i=0; i < scopeRanges.length; i++) {
+ var scopeRange = scopeRanges[i];
+ var classNames = this._styles[scopeRange.scope];
+ if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); }
+ var classNamesString = classNames.join(" ");
+ styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}});
+// console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}");
+ }
+ return styleRanges;
+ }
+ };
+
+ return {
+ RegexUtil: RegexUtil,
+ TextMateStyler: TextMateStyler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ * Alex Lakatos - fix for bug#369781
+ ******************************************************************************/
+
+/*global document window navigator define */
+
+define("examples/textview/textStyler", ['orion/textview/annotations'], function(mAnnotations) {
+
+ var JS_KEYWORDS =
+ ["break",
+ "case", "class", "catch", "continue", "const",
+ "debugger", "default", "delete", "do",
+ "else", "enum", "export", "extends",
+ "false", "finally", "for", "function",
+ "if", "implements", "import", "in", "instanceof", "interface",
+ "let",
+ "new", "null",
+ "package", "private", "protected", "public",
+ "return",
+ "static", "super", "switch",
+ "this", "throw", "true", "try", "typeof",
+ "undefined",
+ "var", "void",
+ "while", "with",
+ "yield"];
+
+ var JAVA_KEYWORDS =
+ ["abstract",
+ "boolean", "break", "byte",
+ "case", "catch", "char", "class", "continue",
+ "default", "do", "double",
+ "else", "extends",
+ "false", "final", "finally", "float", "for",
+ "if", "implements", "import", "instanceof", "int", "interface",
+ "long",
+ "native", "new", "null",
+ "package", "private", "protected", "public",
+ "return",
+ "short", "static", "super", "switch", "synchronized",
+ "this", "throw", "throws", "transient", "true", "try",
+ "void", "volatile",
+ "while"];
+
+ var CSS_KEYWORDS =
+ ["alignment-adjust", "alignment-baseline", "animation", "animation-delay", "animation-direction", "animation-duration",
+ "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance",
+ "azimuth", "backface-visibility", "background", "background-attachment", "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-align", "box-decoration-break", "box-direction", "box-flex", "box-flex-group", "box-lines",
+ "box-ordinal-group", "box-orient", "box-pack", "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-align", "flex-flow", "flex-inline-pack", "flex-order",
+ "flex-pack", "float", "float-offset", "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style",
+ "font-variant", "font-weight", "grid-columns", "grid-rows", "hanging-punctuation", "height", "hyphenate-after",
+ "hyphenate-before", "hyphenate-character", "hyphenate-lines", "hyphenate-resource", "hyphens", "icon", "image-orientation",
+ "image-rendering", "image-resolution", "inline-box-align", "left", "letter-spacing", "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", "mark", "mark-after", "mark-before",
+ "marker-offset", "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", "opacity", "orphans",
+ "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-style", "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", "phonemes", "pitch",
+ "pitch-range", "play-during", "position", "presentation-level", "punctuation-trim", "quotes", "rendering-intent", "resize",
+ "rest", "rest-after", "rest-before", "richness", "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", "ruby-position",
+ "ruby-span", "size", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", "table-layout",
+ "target", "target-name", "target-new", "target-position", "text-align", "text-align-last", "text-decoration", "text-emphasis",
+ "text-height", "text-indent", "text-justify", "text-outline", "text-shadow", "text-transform", "text-wrap", "top", "transform",
+ "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property",
+ "transition-timing-function", "unicode-bidi", "vector-effect", "vertical-align", "visibility", "voice-balance", "voice-duration", "voice-family",
+ "voice-pitch", "voice-pitch-range", "voice-rate", "voice-stress", "voice-volume", "volume", "white-space", "white-space-collapse",
+ "widows", "width", "word-break", "word-spacing", "word-wrap", "z-index"
+ ];
+
+ // Scanner constants
+ var UNKOWN = 1;
+ var KEYWORD = 2;
+ var STRING = 3;
+ var SINGLELINE_COMMENT = 4;
+ var MULTILINE_COMMENT = 5;
+ var DOC_COMMENT = 6;
+ var WHITE = 7;
+ var WHITE_TAB = 8;
+ var WHITE_SPACE = 9;
+ var HTML_MARKUP = 10;
+ var DOC_TAG = 11;
+ var TASK_TAG = 12;
+
+ // Styles
+ var singleCommentStyle = {styleClass: "token_singleline_comment"};
+ var multiCommentStyle = {styleClass: "token_multiline_comment"};
+ var docCommentStyle = {styleClass: "token_doc_comment"};
+ var htmlMarkupStyle = {styleClass: "token_doc_html_markup"};
+ var tasktagStyle = {styleClass: "token_task_tag"};
+ var doctagStyle = {styleClass: "token_doc_tag"};
+ var stringStyle = {styleClass: "token_string"};
+ var keywordStyle = {styleClass: "token_keyword"};
+ var spaceStyle = {styleClass: "token_space"};
+ var tabStyle = {styleClass: "token_tab"};
+ var caretLineStyle = {styleClass: "line_caret"};
+
+ function Scanner (keywords, whitespacesVisible) {
+ this.keywords = keywords;
+ this.whitespacesVisible = whitespacesVisible;
+ this.setText("");
+ }
+
+ Scanner.prototype = {
+ getOffset: function() {
+ return this.offset;
+ },
+ getStartOffset: function() {
+ return this.startOffset;
+ },
+ getData: function() {
+ return this.text.substring(this.startOffset, this.offset);
+ },
+ getDataLength: function() {
+ return this.offset - this.startOffset;
+ },
+ _default: function(c) {
+ var keywords = this.keywords;
+ switch (c) {
+ case 32: // SPACE
+ case 9: // TAB
+ if (this.whitespacesVisible) {
+ return c === 32 ? WHITE_SPACE : WHITE_TAB;
+ }
+ do {
+ c = this._read();
+ } while(c === 32 || c === 9);
+ this._unread(c);
+ return WHITE;
+ case 123: // {
+ case 125: // }
+ case 40: // (
+ case 41: // )
+ case 91: // [
+ case 93: // ]
+ case 60: // <
+ case 62: // >
+ // BRACKETS
+ return c;
+ default:
+ var isCSS = this.isCSS;
+ if ((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)) { //LETTER OR UNDERSCORE OR NUMBER
+ var off = this.offset - 1;
+ do {
+ c = this._read();
+ } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)); //LETTER OR UNDERSCORE OR NUMBER
+ this._unread(c);
+ if (keywords.length > 0) {
+ var word = this.text.substring(off, this.offset);
+ //TODO slow
+ for (var i=0; i<keywords.length; i++) {
+ if (this.keywords[i] === word) { return KEYWORD; }
+ }
+ }
+ }
+ return UNKOWN;
+ }
+ },
+ _read: function() {
+ if (this.offset < this.text.length) {
+ return this.text.charCodeAt(this.offset++);
+ }
+ return -1;
+ },
+ _unread: function(c) {
+ if (c !== -1) { this.offset--; }
+ },
+ nextToken: function() {
+ this.startOffset = this.offset;
+ while (true) {
+ var c = this._read();
+ switch (c) {
+ case -1: return null;
+ case 47: // SLASH -> comment
+ c = this._read();
+ if (!this.isCSS) {
+ if (c === 47) { // SLASH -> single line
+ while (true) {
+ c = this._read();
+ if ((c === -1) || (c === 10) || (c === 13)) {
+ this._unread(c);
+ return SINGLELINE_COMMENT;
+ }
+ }
+ }
+ }
+ if (c === 42) { // STAR -> multi line
+ c = this._read();
+ var token = MULTILINE_COMMENT;
+ if (c === 42) {
+ token = DOC_COMMENT;
+ }
+ while (true) {
+ while (c === 42) {
+ c = this._read();
+ if (c === 47) {
+ return token;
+ }
+ }
+ if (c === -1) {
+ this._unread(c);
+ return token;
+ }
+ c = this._read();
+ }
+ }
+ this._unread(c);
+ return UNKOWN;
+ case 39: // SINGLE QUOTE -> char const
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 39:
+ return STRING;
+ case 13:
+ case 10:
+ case -1:
+ this._unread(c);
+ return STRING;
+ case 92: // BACKSLASH
+ c = this._read();
+ break;
+ }
+ }
+ break;
+ case 34: // DOUBLE QUOTE -> string
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 34: // DOUBLE QUOTE
+ return STRING;
+ case 13:
+ case 10:
+ case -1:
+ this._unread(c);
+ return STRING;
+ case 92: // BACKSLASH
+ c = this._read();
+ break;
+ }
+ }
+ break;
+ default:
+ return this._default(c);
+ }
+ }
+ },
+ setText: function(text) {
+ this.text = text;
+ this.offset = 0;
+ this.startOffset = 0;
+ }
+ };
+
+ function WhitespaceScanner () {
+ Scanner.call(this, null, true);
+ }
+ WhitespaceScanner.prototype = new Scanner(null);
+ WhitespaceScanner.prototype.nextToken = function() {
+ this.startOffset = this.offset;
+ while (true) {
+ var c = this._read();
+ switch (c) {
+ case -1: return null;
+ case 32: // SPACE
+ return WHITE_SPACE;
+ case 9: // TAB
+ return WHITE_TAB;
+ default:
+ do {
+ c = this._read();
+ } while(!(c === 32 || c === 9 || c === -1));
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function CommentScanner (whitespacesVisible) {
+ Scanner.call(this, null, whitespacesVisible);
+ }
+ CommentScanner.prototype = new Scanner(null);
+ CommentScanner.prototype.setType = function(type) {
+ this._type = type;
+ };
+ CommentScanner.prototype.nextToken = function() {
+ this.startOffset = this.offset;
+ while (true) {
+ var c = this._read();
+ switch (c) {
+ case -1: return null;
+ case 32: // SPACE
+ case 9: // TAB
+ if (this.whitespacesVisible) {
+ return c === 32 ? WHITE_SPACE : WHITE_TAB;
+ }
+ do {
+ c = this._read();
+ } while(c === 32 || c === 9);
+ this._unread(c);
+ return WHITE;
+ case 60: // <
+ if (this._type === DOC_COMMENT) {
+ do {
+ c = this._read();
+ } while(!(c === 62 || c === -1)); // >
+ if (c === 62) {
+ return HTML_MARKUP;
+ }
+ }
+ return UNKOWN;
+ case 64: // @
+ if (this._type === DOC_COMMENT) {
+ do {
+ c = this._read();
+ } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57)); //LETTER OR UNDERSCORE OR NUMBER
+ this._unread(c);
+ return DOC_TAG;
+ }
+ return UNKOWN;
+ case 84: // T
+ if ((c = this._read()) === 79) { // O
+ if ((c = this._read()) === 68) { // D
+ if ((c = this._read()) === 79) { // O
+ c = this._read();
+ if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57))) {
+ this._unread(c);
+ return TASK_TAG;
+ }
+ this._unread(c);
+ } else {
+ this._unread(c);
+ }
+ } else {
+ this._unread(c);
+ }
+ } else {
+ this._unread(c);
+ }
+ //FALL THROUGH
+ default:
+ do {
+ c = this._read();
+ } while(!(c === 32 || c === 9 || c === -1 || c === 60 || c === 64 || c === 84));
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function FirstScanner () {
+ Scanner.call(this, null, false);
+ }
+ FirstScanner.prototype = new Scanner(null);
+ FirstScanner.prototype._default = function(c) {
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 47: // SLASH
+ case 34: // DOUBLE QUOTE
+ case 39: // SINGLE QUOTE
+ case -1:
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function TextStyler (view, lang, annotationModel) {
+ this.commentStart = "/*";
+ this.commentEnd = "*/";
+ var keywords = [];
+ switch (lang) {
+ case "java": keywords = JAVA_KEYWORDS; break;
+ case "js": keywords = JS_KEYWORDS; break;
+ case "css": keywords = CSS_KEYWORDS; break;
+ }
+ this.whitespacesVisible = false;
+ this.detectHyperlinks = true;
+ this.highlightCaretLine = false;
+ this.foldingEnabled = true;
+ this.detectTasks = true;
+ this._scanner = new Scanner(keywords, this.whitespacesVisible);
+ this._firstScanner = new FirstScanner();
+ this._commentScanner = new CommentScanner(this.whitespacesVisible);
+ this._whitespaceScanner = new WhitespaceScanner();
+ //TODO these scanners are not the best/correct way to parse CSS
+ if (lang === "css") {
+ this._scanner.isCSS = true;
+ this._firstScanner.isCSS = true;
+ }
+ this.view = view;
+ this.annotationModel = annotationModel;
+ this._bracketAnnotations = undefined;
+
+ var self = this;
+ this._listener = {
+ onChanged: function(e) {
+ self._onModelChanged(e);
+ },
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self._onLineStyle(e);
+ },
+ onSelection: function(e) {
+ self._onSelection(e);
+ }
+ };
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model.getBaseModel().addEventListener("Changed", this._listener.onChanged);
+ } else {
+ //TODO still needed to keep the event order correct (styler before view)
+ view.addEventListener("ModelChanged", this._listener.onChanged);
+ }
+ view.addEventListener("Selection", this._listener.onSelection);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ view.addEventListener("LineStyle", this._listener.onLineStyle);
+ this._computeComments ();
+ this._computeFolding();
+ view.redrawLines();
+ }
+
+ TextStyler.prototype = {
+ getClassNameForToken: function(token) {
+ switch (token) {
+ case "singleLineComment": return singleCommentStyle.styleClass;
+ case "multiLineComment": return multiCommentStyle.styleClass;
+ case "docComment": return docCommentStyle.styleClass;
+ case "docHtmlComment": return htmlMarkupStyle.styleClass;
+ case "tasktag": return tasktagStyle.styleClass;
+ case "doctag": return doctagStyle.styleClass;
+ case "string": return stringStyle.styleClass;
+ case "keyword": return keywordStyle.styleClass;
+ case "space": return spaceStyle.styleClass;
+ case "tab": return tabStyle.styleClass;
+ case "caretLine": return caretLineStyle.styleClass;
+ }
+ return null;
+ },
+ destroy: function() {
+ var view = this.view;
+ if (view) {
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model.getBaseModel().removeEventListener("Changed", this._listener.onChanged);
+ } else {
+ view.removeEventListener("ModelChanged", this._listener.onChanged);
+ }
+ view.removeEventListener("Selection", this._listener.onSelection);
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ view.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.view = null;
+ }
+ },
+ setHighlightCaretLine: function(highlight) {
+ this.highlightCaretLine = highlight;
+ },
+ setWhitespacesVisible: function(visible) {
+ this.whitespacesVisible = visible;
+ this._scanner.whitespacesVisible = visible;
+ this._commentScanner.whitespacesVisible = visible;
+ },
+ setDetectHyperlinks: function(enabled) {
+ this.detectHyperlinks = enabled;
+ },
+ setFoldingEnabled: function(enabled) {
+ this.foldingEnabled = enabled;
+ },
+ setDetectTasks: function(enabled) {
+ this.detectTasks = enabled;
+ },
+ _binarySearch: function (array, offset, inclusive, low, high) {
+ var index;
+ if (low === undefined) { low = -1; }
+ if (high === undefined) { high = array.length; }
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else if (inclusive && offset < array[index].end) {
+ high = index;
+ break;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ _computeComments: function() {
+ var model = this.view.getModel();
+ if (model.getBaseModel) { model = model.getBaseModel(); }
+ this.comments = this._findComments(model.getText());
+ },
+ _computeFolding: function() {
+ if (!this.foldingEnabled) { return; }
+ var view = this.view;
+ var viewModel = view.getModel();
+ if (!viewModel.getBaseModel) { return; }
+ var annotationModel = this.annotationModel;
+ if (!annotationModel) { return; }
+ annotationModel.removeAnnotations("orion.annotation.folding");
+ var add = [];
+ var baseModel = viewModel.getBaseModel();
+ var comments = this.comments;
+ for (var i=0; i<comments.length; i++) {
+ var comment = comments[i];
+ var annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end);
+ if (annotation) {
+ add.push(annotation);
+ }
+ }
+ annotationModel.replaceAnnotations(null, add);
+ },
+ _createFoldingAnnotation: function(viewModel, baseModel, start, end) {
+ var startLine = baseModel.getLineAtOffset(start);
+ var endLine = baseModel.getLineAtOffset(end);
+ if (startLine === endLine) {
+ return null;
+ }
+ return new mAnnotations.FoldingAnnotation(viewModel, "orion.annotation.folding", start, end,
+ "<div class='annotationHTML expanded'></div>", {styleClass: "annotation expanded"},
+ "<div class='annotationHTML collapsed'></div>", {styleClass: "annotation collapsed"});
+ },
+ _computeTasks: function(type, commentStart, commentEnd) {
+ if (!this.detectTasks) { return; }
+ var annotationModel = this.annotationModel;
+ if (!annotationModel) { return; }
+ var view = this.view;
+ var viewModel = view.getModel(), baseModel = viewModel;
+ if (viewModel.getBaseModel) { baseModel = viewModel.getBaseModel(); }
+ var annotations = annotationModel.getAnnotations(commentStart, commentEnd);
+ var remove = [];
+ var annotationType = "orion.annotation.task";
+ while (annotations.hasNext()) {
+ var annotation = annotations.next();
+ if (annotation.type === annotationType) {
+ remove.push(annotation);
+ }
+ }
+ var add = [];
+ var scanner = this._commentScanner;
+ scanner.setText(baseModel.getText(commentStart, commentEnd));
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + commentStart;
+ if (token === TASK_TAG) {
+ var end = baseModel.getLineEnd(baseModel.getLineAtOffset(tokenStart));
+ if (type !== SINGLELINE_COMMENT) {
+ end = Math.min(end, commentEnd - this.commentEnd.length);
+ }
+ add.push({
+ start: tokenStart,
+ end: end,
+ type: annotationType,
+ title: baseModel.getText(tokenStart, end),
+ style: {styleClass: "annotation task"},
+ html: "<div class='annotationHTML task'></div>",
+ overviewStyle: {styleClass: "annotationOverview task"},
+ rangeStyle: {styleClass: "annotationRange task"}
+ });
+ }
+ }
+ annotationModel.replaceAnnotations(remove, add);
+ },
+ _getLineStyle: function(lineIndex) {
+ if (this.highlightCaretLine) {
+ var view = this.view;
+ var model = view.getModel();
+ var selection = view.getSelection();
+ if (selection.start === selection.end && model.getLineAtOffset(selection.start) === lineIndex) {
+ return caretLineStyle;
+ }
+ }
+ return null;
+ },
+ _getStyles: function(model, text, start) {
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ }
+ var end = start + text.length;
+
+ var styles = [];
+
+ // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
+ var offset = start, comments = this.comments;
+ var startIndex = this._binarySearch(comments, start, true);
+ for (var i = startIndex; i < comments.length; i++) {
+ if (comments[i].start >= end) { break; }
+ var commentStart = comments[i].start;
+ var commentEnd = comments[i].end;
+ if (offset < commentStart) {
+ this._parse(text.substring(offset - start, commentStart - start), offset, styles);
+ }
+ var style = comments[i].type === DOC_COMMENT ? docCommentStyle : multiCommentStyle;
+ if (this.whitespacesVisible || this.detectHyperlinks) {
+ var s = Math.max(offset, commentStart);
+ var e = Math.min(end, commentEnd);
+ this._parseComment(text.substring(s - start, e - start), s, styles, style, comments[i].type);
+ } else {
+ styles.push({start: commentStart, end: commentEnd, style: style});
+ }
+ offset = commentEnd;
+ }
+ if (offset < end) {
+ this._parse(text.substring(offset - start, end - start), offset, styles);
+ }
+ if (model.getBaseModel) {
+ for (var j = 0; j < styles.length; j++) {
+ var length = styles[j].end - styles[j].start;
+ styles[j].start = model.mapOffset(styles[j].start, true);
+ styles[j].end = styles[j].start + length;
+ }
+ }
+ return styles;
+ },
+ _parse: function(text, offset, styles) {
+ var scanner = this._scanner;
+ scanner.setText(text);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = null;
+ switch (token) {
+ case KEYWORD: style = keywordStyle; break;
+ case STRING:
+ if (this.whitespacesVisible) {
+ this._parseString(scanner.getData(), tokenStart, styles, stringStyle);
+ continue;
+ } else {
+ style = stringStyle;
+ }
+ break;
+ case DOC_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, docCommentStyle, token);
+ continue;
+ case SINGLELINE_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, singleCommentStyle, token);
+ continue;
+ case MULTILINE_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, multiCommentStyle, token);
+ continue;
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ }
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ },
+ _parseComment: function(text, offset, styles, s, type) {
+ var scanner = this._commentScanner;
+ scanner.setText(text);
+ scanner.setType(type);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = s;
+ switch (token) {
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ case HTML_MARKUP:
+ style = htmlMarkupStyle;
+ break;
+ case DOC_TAG:
+ style = doctagStyle;
+ break;
+ case TASK_TAG:
+ style = tasktagStyle;
+ break;
+ default:
+ if (this.detectHyperlinks) {
+ style = this._detectHyperlinks(scanner.getData(), tokenStart, styles, style);
+ }
+ }
+ if (style) {
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ }
+ },
+ _parseString: function(text, offset, styles, s) {
+ var scanner = this._whitespaceScanner;
+ scanner.setText(text);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = s;
+ switch (token) {
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ }
+ if (style) {
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ }
+ },
+ _detectHyperlinks: function(text, offset, styles, s) {
+ var href = null, index, linkStyle;
+ if ((index = text.indexOf("://")) > 0) {
+ href = text;
+ var start = index;
+ while (start > 0) {
+ var c = href.charCodeAt(start - 1);
+ if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || 0x2d === c || (48 <= c && c <= 57))) { //LETTER OR DASH OR NUMBER
+ break;
+ }
+ start--;
+ }
+ if (start > 0) {
+ var brackets = "\"\"''(){}[]<>";
+ index = brackets.indexOf(href.substring(start - 1, start));
+ if (index !== -1 && (index & 1) === 0 && (index = href.lastIndexOf(brackets.substring(index + 1, index + 2))) !== -1) {
+ var end = index;
+ linkStyle = this._clone(s);
+ linkStyle.tagName = "A";
+ linkStyle.attributes = {href: href.substring(start, end)};
+ styles.push({start: offset, end: offset + start, style: s});
+ styles.push({start: offset + start, end: offset + end, style: linkStyle});
+ styles.push({start: offset + end, end: offset + text.length, style: s});
+ return null;
+ }
+ }
+ } else if (text.toLowerCase().indexOf("bug#") === 0) {
+ href = "https://bugs.eclipse.org/bugs/show_bug.cgi?id=" + parseInt(text.substring(4), 10);
+ }
+ if (href) {
+ linkStyle = this._clone(s);
+ linkStyle.tagName = "A";
+ linkStyle.attributes = {href: href};
+ return linkStyle;
+ }
+ return s;
+ },
+ _clone: function(obj) {
+ if (!obj) { return obj; }
+ var newObj = {};
+ for (var p in obj) {
+ if (obj.hasOwnProperty(p)) {
+ var value = obj[p];
+ newObj[p] = value;
+ }
+ }
+ return newObj;
+ },
+ _findComments: function(text, offset) {
+ offset = offset || 0;
+ var scanner = this._firstScanner, token;
+ scanner.setText(text);
+ var result = [];
+ while ((token = scanner.nextToken())) {
+ if (token === MULTILINE_COMMENT || token === DOC_COMMENT) {
+ var comment = {
+ start: scanner.getStartOffset() + offset,
+ end: scanner.getOffset() + offset,
+ type: token
+ };
+ result.push(comment);
+ //TODO can we avoid this work if edition does not overlap comment?
+ this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
+ }
+ if (token === SINGLELINE_COMMENT) {
+ //TODO can we avoid this work if edition does not overlap comment?
+ this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
+ }
+ }
+ return result;
+ },
+ _findMatchingBracket: function(model, offset) {
+ var brackets = "{}()[]<>";
+ var bracket = model.getText(offset, offset + 1);
+ var bracketIndex = brackets.indexOf(bracket, 0);
+ if (bracketIndex === -1) { return -1; }
+ var closingBracket;
+ if (bracketIndex & 1) {
+ closingBracket = brackets.substring(bracketIndex - 1, bracketIndex);
+ } else {
+ closingBracket = brackets.substring(bracketIndex + 1, bracketIndex + 2);
+ }
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var i=0; i<brackets.length; i++) {
+ var sign = brackets[i] >= 0 ? 1 : -1;
+ if (brackets[i] * sign === offset) {
+ var level = 1;
+ if (bracketIndex & 1) {
+ i--;
+ for (; i>=0; i--) {
+ sign = brackets[i] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[i] * sign;
+ }
+ }
+ lineIndex -= 1;
+ while (lineIndex >= 0) {
+ lineText = model.getLine(lineIndex);
+ lineStart = model.getLineStart(lineIndex);
+ lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var j=brackets.length - 1; j>=0; j--) {
+ sign = brackets[j] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[j] * sign;
+ }
+ }
+ lineIndex--;
+ }
+ } else {
+ i++;
+ for (; i<brackets.length; i++) {
+ sign = brackets[i] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[i] * sign;
+ }
+ }
+ lineIndex += 1;
+ var lineCount = model.getLineCount ();
+ while (lineIndex < lineCount) {
+ lineText = model.getLine(lineIndex);
+ lineStart = model.getLineStart(lineIndex);
+ lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var k=0; k<brackets.length; k++) {
+ sign = brackets[k] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[k] * sign;
+ }
+ }
+ lineIndex++;
+ }
+ }
+ break;
+ }
+ }
+ return -1;
+ },
+ _findBrackets: function(bracket, closingBracket, text, textOffset, start, end) {
+ var result = [];
+ var bracketToken = bracket.charCodeAt(0);
+ var closingBracketToken = closingBracket.charCodeAt(0);
+ // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
+ var offset = start, scanner = this._scanner, token, comments = this.comments;
+ var startIndex = this._binarySearch(comments, start, true);
+ for (var i = startIndex; i < comments.length; i++) {
+ if (comments[i].start >= end) { break; }
+ var commentStart = comments[i].start;
+ var commentEnd = comments[i].end;
+ if (offset < commentStart) {
+ scanner.setText(text.substring(offset - start, commentStart - start));
+ while ((token = scanner.nextToken())) {
+ if (token === bracketToken) {
+ result.push(scanner.getStartOffset() + offset - start + textOffset);
+ } else if (token === closingBracketToken) {
+ result.push(-(scanner.getStartOffset() + offset - start + textOffset));
+ }
+ }
+ }
+ offset = commentEnd;
+ }
+ if (offset < end) {
+ scanner.setText(text.substring(offset - start, end - start));
+ while ((token = scanner.nextToken())) {
+ if (token === bracketToken) {
+ result.push(scanner.getStartOffset() + offset - start + textOffset);
+ } else if (token === closingBracketToken) {
+ result.push(-(scanner.getStartOffset() + offset - start + textOffset));
+ }
+ }
+ }
+ return result;
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onLineStyle: function (e) {
+ if (e.textView === this.view) {
+ e.style = this._getLineStyle(e.lineIndex);
+ }
+ e.ranges = this._getStyles(e.textView.getModel(), e.lineText, e.lineStart);
+ },
+ _onSelection: function(e) {
+ var oldSelection = e.oldValue;
+ var newSelection = e.newValue;
+ var view = this.view;
+ var model = view.getModel();
+ var lineIndex;
+ if (this.highlightCaretLine) {
+ var oldLineIndex = model.getLineAtOffset(oldSelection.start);
+ lineIndex = model.getLineAtOffset(newSelection.start);
+ var newEmpty = newSelection.start === newSelection.end;
+ var oldEmpty = oldSelection.start === oldSelection.end;
+ if (!(oldLineIndex === lineIndex && oldEmpty && newEmpty)) {
+ if (oldEmpty) {
+ view.redrawLines(oldLineIndex, oldLineIndex + 1);
+ }
+ if ((oldLineIndex !== lineIndex || !oldEmpty) && newEmpty) {
+ view.redrawLines(lineIndex, lineIndex + 1);
+ }
+ }
+ }
+ if (!this.annotationModel) { return; }
+ var remove = this._bracketAnnotations, add, caret;
+ if (newSelection.start === newSelection.end && (caret = view.getCaretOffset()) > 0) {
+ var mapCaret = caret - 1;
+ if (model.getBaseModel) {
+ mapCaret = model.mapOffset(mapCaret);
+ model = model.getBaseModel();
+ }
+ var bracket = this._findMatchingBracket(model, mapCaret);
+ if (bracket !== -1) {
+ add = [{
+ start: bracket,
+ end: bracket + 1,
+ type: "orion.annotation.matchingBracket",
+ title: "Matching Bracket",
+ html: "<div class='annotationHTML matchingBracket'></div>",
+ overviewStyle: {styleClass: "annotationOverview matchingBracket"},
+ rangeStyle: {styleClass: "annotationRange matchingBracket"}
+ },
+ {
+ start: mapCaret,
+ end: mapCaret + 1,
+ type: "orion.annotation.currentBracket",
+ title: "Current Bracket",
+ html: "<div class='annotationHTML currentBracket'></div>",
+ overviewStyle: {styleClass: "annotationOverview currentBracket"},
+ rangeStyle: {styleClass: "annotationRange currentBracket"}
+ }];
+ }
+ }
+ this._bracketAnnotations = add;
+ this.annotationModel.replaceAnnotations(remove, add);
+ },
+ _onModelChanged: function(e) {
+ var start = e.start;
+ var removedCharCount = e.removedCharCount;
+ var addedCharCount = e.addedCharCount;
+ var changeCount = addedCharCount - removedCharCount;
+ var view = this.view;
+ var viewModel = view.getModel();
+ var baseModel = viewModel.getBaseModel ? viewModel.getBaseModel() : viewModel;
+ var end = start + removedCharCount;
+ var charCount = baseModel.getCharCount();
+ var commentCount = this.comments.length;
+ var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
+ var commentStart = this._binarySearch(this.comments, lineStart, true);
+ var commentEnd = this._binarySearch(this.comments, end, false, commentStart - 1, commentCount);
+
+ var ts;
+ if (commentStart < commentCount && this.comments[commentStart].start <= lineStart && lineStart < this.comments[commentStart].end) {
+ ts = this.comments[commentStart].start;
+ if (ts > start) { ts += changeCount; }
+ } else {
+ if (commentStart === commentCount && commentCount > 0 && charCount - changeCount === this.comments[commentCount - 1].end) {
+ ts = this.comments[commentCount - 1].start;
+ } else {
+ ts = lineStart;
+ }
+ }
+ var te;
+ if (commentEnd < commentCount) {
+ te = this.comments[commentEnd].end;
+ if (te > start) { te += changeCount; }
+ commentEnd += 1;
+ } else {
+ commentEnd = commentCount;
+ te = charCount;//TODO could it be smaller?
+ }
+ var text = baseModel.getText(ts, te), comment;
+ var newComments = this._findComments(text, ts), i;
+ for (i = commentStart; i < this.comments.length; i++) {
+ comment = this.comments[i];
+ if (comment.start > start) { comment.start += changeCount; }
+ if (comment.start > start) { comment.end += changeCount; }
+ }
+ var redraw = (commentEnd - commentStart) !== newComments.length;
+ if (!redraw) {
+ for (i=0; i<newComments.length; i++) {
+ comment = this.comments[commentStart + i];
+ var newComment = newComments[i];
+ if (comment.start !== newComment.start || comment.end !== newComment.end || comment.type !== newComment.type) {
+ redraw = true;
+ break;
+ }
+ }
+ }
+ var args = [commentStart, commentEnd - commentStart].concat(newComments);
+ Array.prototype.splice.apply(this.comments, args);
+ if (redraw) {
+ var redrawStart = ts;
+ var redrawEnd = te;
+ if (viewModel !== baseModel) {
+ redrawStart = viewModel.mapOffset(redrawStart, true);
+ redrawEnd = viewModel.mapOffset(redrawEnd, true);
+ }
+ view.redrawRange(redrawStart, redrawEnd);
+ }
+
+ if (this.foldingEnabled && baseModel !== viewModel && this.annotationModel) {
+ var annotationModel = this.annotationModel;
+ var iter = annotationModel.getAnnotations(ts, te);
+ var remove = [], all = [];
+ var annotation;
+ while (iter.hasNext()) {
+ annotation = iter.next();
+ if (annotation.type === "orion.annotation.folding") {
+ all.push(annotation);
+ for (i = 0; i < newComments.length; i++) {
+ if (annotation.start === newComments[i].start && annotation.end === newComments[i].end) {
+ break;
+ }
+ }
+ if (i === newComments.length) {
+ remove.push(annotation);
+ annotation.expand();
+ } else {
+ var annotationStart = annotation.start;
+ var annotationEnd = annotation.end;
+ if (annotationStart > start) {
+ annotationStart -= changeCount;
+ }
+ if (annotationEnd > start) {
+ annotationEnd -= changeCount;
+ }
+ if (annotationStart <= start && start < annotationEnd && annotationStart <= end && end < annotationEnd) {
+ var startLine = baseModel.getLineAtOffset(annotation.start);
+ var endLine = baseModel.getLineAtOffset(annotation.end);
+ if (startLine !== endLine) {
+ if (!annotation.expanded) {
+ annotation.expand();
+ annotationModel.modifyAnnotation(annotation);
+ }
+ } else {
+ annotationModel.removeAnnotation(annotation);
+ }
+ }
+ }
+ }
+ }
+ var add = [];
+ for (i = 0; i < newComments.length; i++) {
+ comment = newComments[i];
+ for (var j = 0; j < all.length; j++) {
+ if (all[j].start === comment.start && all[j].end === comment.end) {
+ break;
+ }
+ }
+ if (j === all.length) {
+ annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end);
+ if (annotation) {
+ add.push(annotation);
+ }
+ }
+ }
+ annotationModel.replaceAnnotations(remove, add);
+ }
+ }
+ };
+
+ return {TextStyler: TextStyler};
+});
diff --git a/browser/devtools/sourceeditor/source-editor-orion.jsm b/browser/devtools/sourceeditor/source-editor-orion.jsm
new file mode 100644
index 000000000..55a9a4424
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor-orion.jsm
@@ -0,0 +1,2129 @@
+/* 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 Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/source-editor-ui.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+const ORION_SCRIPT = "chrome://browser/content/devtools/orion.js";
+const ORION_IFRAME = "data:text/html;charset=utf8,<!DOCTYPE html>" +
+ "<html style='height:100%' dir='ltr'>" +
+ "<head><link rel='stylesheet'" +
+ " href='chrome://browser/skin/devtools/orion-container.css'></head>" +
+ "<body style='height:100%;margin:0;overflow:hidden'>" +
+ "<div id='editor' style='height:100%'></div>" +
+ "</body></html>";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Maximum allowed vertical offset for the line index when you call
+ * SourceEditor.setCaretPosition().
+ *
+ * @type number
+ */
+const VERTICAL_OFFSET = 3;
+
+/**
+ * The primary selection update delay. On Linux, the X11 primary selection is
+ * updated to hold the currently selected text.
+ *
+ * @type number
+ */
+const PRIMARY_SELECTION_DELAY = 100;
+
+/**
+ * Predefined themes for syntax highlighting. This objects maps
+ * SourceEditor.THEMES to Orion CSS files.
+ */
+const ORION_THEMES = {
+ mozilla: ["chrome://browser/skin/devtools/orion.css"],
+};
+
+/**
+ * Known Orion editor events you can listen for. This object maps several of the
+ * SourceEditor.EVENTS to Orion events.
+ */
+const ORION_EVENTS = {
+ ContextMenu: "ContextMenu",
+ TextChanged: "ModelChanged",
+ Selection: "Selection",
+ Focus: "Focus",
+ Blur: "Blur",
+ MouseOver: "MouseOver",
+ MouseOut: "MouseOut",
+ MouseMove: "MouseMove",
+};
+
+/**
+ * Known Orion annotation types.
+ */
+const ORION_ANNOTATION_TYPES = {
+ currentBracket: "orion.annotation.currentBracket",
+ matchingBracket: "orion.annotation.matchingBracket",
+ breakpoint: "orion.annotation.breakpoint",
+ task: "orion.annotation.task",
+ currentLine: "orion.annotation.currentLine",
+ debugLocation: "mozilla.annotation.debugLocation",
+};
+
+/**
+ * Default key bindings in the Orion editor.
+ */
+const DEFAULT_KEYBINDINGS = [
+ {
+ action: "enter",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_ENTER,
+ },
+ {
+ action: "undo",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_Z,
+ accel: true,
+ },
+ {
+ action: "redo",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_Z,
+ accel: true,
+ shift: true,
+ },
+ {
+ action: "Unindent Lines",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_TAB,
+ shift: true,
+ },
+ {
+ action: "Move Lines Up",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_UP,
+ ctrl: Services.appinfo.OS == "Darwin",
+ alt: true,
+ },
+ {
+ action: "Move Lines Down",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_DOWN,
+ ctrl: Services.appinfo.OS == "Darwin",
+ alt: true,
+ },
+ {
+ action: "Comment/Uncomment",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_SLASH,
+ accel: true,
+ },
+ {
+ action: "Move to Bracket Opening",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET,
+ accel: true,
+ },
+ {
+ action: "Move to Bracket Closing",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET,
+ accel: true,
+ },
+];
+
+if (Services.appinfo.OS == "WINNT" ||
+ Services.appinfo.OS == "Linux") {
+ DEFAULT_KEYBINDINGS.push({
+ action: "redo",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_Y,
+ accel: true,
+ });
+}
+
+this.EXPORTED_SYMBOLS = ["SourceEditor"];
+
+/**
+ * The SourceEditor object constructor. The SourceEditor component allows you to
+ * provide users with an editor tailored to the specific needs of editing source
+ * code, aimed primarily at web developers.
+ *
+ * The editor used here is Eclipse Orion (see http://www.eclipse.org/orion).
+ *
+ * @constructor
+ */
+this.SourceEditor = function SourceEditor() {
+ // Update the SourceEditor defaults from user preferences.
+
+ SourceEditor.DEFAULTS.tabSize =
+ Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
+ SourceEditor.DEFAULTS.expandTab =
+ Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
+
+ this._onOrionSelection = this._onOrionSelection.bind(this);
+ this._onTextChanged = this._onTextChanged.bind(this);
+ this._onOrionContextMenu = this._onOrionContextMenu.bind(this);
+
+ this._eventTarget = {};
+ this._eventListenersQueue = [];
+ this.ui = new SourceEditorUI(this);
+}
+
+SourceEditor.prototype = {
+ _view: null,
+ _iframe: null,
+ _model: null,
+ _undoStack: null,
+ _linesRuler: null,
+ _annotationRuler: null,
+ _overviewRuler: null,
+ _styler: null,
+ _annotationStyler: null,
+ _annotationModel: null,
+ _dragAndDrop: null,
+ _currentLineAnnotation: null,
+ _primarySelectionTimeout: null,
+ _mode: null,
+ _expandTab: null,
+ _tabSize: null,
+ _iframeWindow: null,
+ _eventTarget: null,
+ _eventListenersQueue: null,
+ _contextMenu: null,
+ _dirty: false,
+
+ /**
+ * The Source Editor user interface manager.
+ * @type object
+ * An instance of the SourceEditorUI.
+ */
+ ui: null,
+
+ /**
+ * The editor container element.
+ * @type nsIDOMElement
+ */
+ parentElement: null,
+
+ /**
+ * Initialize the editor.
+ *
+ * @param nsIDOMElement aElement
+ * The DOM element where you want the editor to show.
+ * @param object aConfig
+ * Editor configuration object. See SourceEditor.DEFAULTS for the
+ * available configuration options.
+ * @param function [aCallback]
+ * Function you want to execute once the editor is loaded and
+ * initialized.
+ * @see SourceEditor.DEFAULTS
+ */
+ init: function SE_init(aElement, aConfig, aCallback)
+ {
+ if (this._iframe) {
+ throw new Error("SourceEditor is already initialized!");
+ }
+
+ let doc = aElement.ownerDocument;
+
+ this._iframe = doc.createElementNS(XUL_NS, "iframe");
+ this._iframe.flex = 1;
+
+ let onIframeLoad = (function() {
+ this._iframe.removeEventListener("load", onIframeLoad, true);
+ this._onIframeLoad();
+ }).bind(this);
+
+ this._iframe.addEventListener("load", onIframeLoad, true);
+
+ this._iframe.setAttribute("src", ORION_IFRAME);
+
+ aElement.appendChild(this._iframe);
+ this.parentElement = aElement;
+
+ this._config = {};
+ for (let key in SourceEditor.DEFAULTS) {
+ this._config[key] = key in aConfig ?
+ aConfig[key] :
+ SourceEditor.DEFAULTS[key];
+ }
+
+ // TODO: Bug 725677 - Remove the deprecated placeholderText option from the
+ // Source Editor initialization.
+ if (aConfig.placeholderText) {
+ this._config.initialText = aConfig.placeholderText;
+ Services.console.logStringMessage("SourceEditor.init() was called with the placeholderText option which is deprecated, please use initialText.");
+ }
+
+ this._onReadyCallback = aCallback;
+ this.ui.init();
+ },
+
+ /**
+ * The editor iframe load event handler.
+ * @private
+ */
+ _onIframeLoad: function SE__onIframeLoad()
+ {
+ this._iframeWindow = this._iframe.contentWindow.wrappedJSObject;
+ let window = this._iframeWindow;
+ let config = this._config;
+
+ Services.scriptloader.loadSubScript(ORION_SCRIPT, window, "utf8");
+
+ let TextModel = window.require("orion/textview/textModel").TextModel;
+ let TextView = window.require("orion/textview/textView").TextView;
+
+ this._expandTab = config.expandTab;
+ this._tabSize = config.tabSize;
+
+ let theme = config.theme;
+ let stylesheet = theme in ORION_THEMES ? ORION_THEMES[theme] : theme;
+
+ this._model = new TextModel(config.initialText);
+ this._view = new TextView({
+ model: this._model,
+ parent: "editor",
+ stylesheet: stylesheet,
+ tabSize: this._tabSize,
+ expandTab: this._expandTab,
+ readonly: config.readOnly,
+ themeClass: "mozilla" + (config.readOnly ? " readonly" : ""),
+ });
+
+ let onOrionLoad = function() {
+ this._view.removeEventListener("Load", onOrionLoad);
+ this._onOrionLoad();
+ }.bind(this);
+
+ this._view.addEventListener("Load", onOrionLoad);
+ if (config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
+ this.addEventListener(SourceEditor.EVENTS.SELECTION,
+ this._onOrionSelection);
+ }
+ this.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
+ this._onTextChanged);
+
+ if (typeof config.contextMenu == "string") {
+ let chromeDocument = this.parentElement.ownerDocument;
+ this._contextMenu = chromeDocument.getElementById(config.contextMenu);
+ } else if (typeof config.contextMenu == "object" ) {
+ this._contextMenu = config._contextMenu;
+ }
+ if (this._contextMenu) {
+ this.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
+ this._onOrionContextMenu);
+ }
+
+ let KeyBinding = window.require("orion/textview/keyBinding").KeyBinding;
+ let TextDND = window.require("orion/textview/textDND").TextDND;
+ let Rulers = window.require("orion/textview/rulers");
+ let LineNumberRuler = Rulers.LineNumberRuler;
+ let AnnotationRuler = Rulers.AnnotationRuler;
+ let OverviewRuler = Rulers.OverviewRuler;
+ let UndoStack = window.require("orion/textview/undoStack").UndoStack;
+ let AnnotationModel = window.require("orion/textview/annotations").AnnotationModel;
+
+ this._annotationModel = new AnnotationModel(this._model);
+
+ if (config.showAnnotationRuler) {
+ this._annotationRuler = new AnnotationRuler(this._annotationModel, "left",
+ {styleClass: "ruler annotations"});
+ this._annotationRuler.onClick = this._annotationRulerClick.bind(this);
+ this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint);
+ this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+ this._view.addRuler(this._annotationRuler);
+ }
+
+ if (config.showLineNumbers) {
+ let rulerClass = this._annotationRuler ?
+ "ruler lines linesWithAnnotations" :
+ "ruler lines";
+
+ this._linesRuler = new LineNumberRuler(this._annotationModel, "left",
+ {styleClass: rulerClass}, {styleClass: "rulerLines odd"},
+ {styleClass: "rulerLines even"});
+
+ this._linesRuler.onClick = this._linesRulerClick.bind(this);
+ this._linesRuler.onDblClick = this._linesRulerDblClick.bind(this);
+ this._view.addRuler(this._linesRuler);
+ }
+
+ if (config.showOverviewRuler) {
+ this._overviewRuler = new OverviewRuler(this._annotationModel, "right",
+ {styleClass: "ruler overview"});
+ this._overviewRuler.onClick = this._overviewRulerClick.bind(this);
+
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.task);
+ this._view.addRuler(this._overviewRuler);
+ }
+
+ this.setMode(config.mode);
+
+ this._undoStack = new UndoStack(this._view, config.undoLimit);
+
+ this._dragAndDrop = new TextDND(this._view, this._undoStack);
+
+ let actions = {
+ "undo": [this.undo, this],
+ "redo": [this.redo, this],
+ "tab": [this._doTab, this],
+ "Unindent Lines": [this._doUnindentLines, this],
+ "enter": [this._doEnter, this],
+ "Find...": [this.ui.find, this.ui],
+ "Find Next Occurrence": [this.ui.findNext, this.ui],
+ "Find Previous Occurrence": [this.ui.findPrevious, this.ui],
+ "Goto Line...": [this.ui.gotoLine, this.ui],
+ "Move Lines Down": [this._moveLines, this],
+ "Comment/Uncomment": [this._doCommentUncomment, this],
+ "Move to Bracket Opening": [this._moveToBracketOpening, this],
+ "Move to Bracket Closing": [this._moveToBracketClosing, this],
+ };
+
+ for (let name in actions) {
+ let action = actions[name];
+ this._view.setAction(name, action[0].bind(action[1]));
+ }
+
+ this._view.setAction("Move Lines Up", this._moveLines.bind(this, true));
+
+ let keys = (config.keys || []).concat(DEFAULT_KEYBINDINGS);
+ keys.forEach(function(aKey) {
+ // In Orion mod1 refers to Cmd on Macs and Ctrl on Windows and Linux.
+ // So, if ctrl is in aKey we use it on Windows and Linux, otherwise
+ // we use aKey.accel for mod1.
+ let mod1 = Services.appinfo.OS != "Darwin" &&
+ "ctrl" in aKey ? aKey.ctrl : aKey.accel;
+ let binding = new KeyBinding(aKey.code, mod1, aKey.shift, aKey.alt,
+ aKey.ctrl);
+ this._view.setKeyBinding(binding, aKey.action);
+
+ if (aKey.callback) {
+ this._view.setAction(aKey.action, aKey.callback);
+ }
+ }, this);
+
+ this._initEventTarget();
+ },
+
+ /**
+ * Initialize the private Orion EventTarget object. This is used for tracking
+ * our own event listeners for events outside of Orion's scope.
+ * @private
+ */
+ _initEventTarget: function SE__initEventTarget()
+ {
+ let EventTarget =
+ this._iframeWindow.require("orion/textview/eventTarget").EventTarget;
+ EventTarget.addMixin(this._eventTarget);
+
+ this._eventListenersQueue.forEach(function(aRequest) {
+ if (aRequest[0] == "add") {
+ this.addEventListener(aRequest[1], aRequest[2]);
+ } else {
+ this.removeEventListener(aRequest[1], aRequest[2]);
+ }
+ }, this);
+
+ this._eventListenersQueue = [];
+ },
+
+ /**
+ * Dispatch an event to the SourceEditor event listeners. This covers only the
+ * SourceEditor-specific events.
+ *
+ * @private
+ * @param object aEvent
+ * The event object to dispatch to all listeners.
+ */
+ _dispatchEvent: function SE__dispatchEvent(aEvent)
+ {
+ this._eventTarget.dispatchEvent(aEvent);
+ },
+
+ /**
+ * The Orion "Load" event handler. This is called when the Orion editor
+ * completes the initialization.
+ * @private
+ */
+ _onOrionLoad: function SE__onOrionLoad()
+ {
+ this.ui.onReady();
+ if (this._onReadyCallback) {
+ this._onReadyCallback(this);
+ this._onReadyCallback = null;
+ }
+ },
+
+ /**
+ * The "tab" editor action implementation. This adds support for expanded tabs
+ * to spaces, and support for the indentation of multiple lines at once.
+ * @private
+ */
+ _doTab: function SE__doTab()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let indent = "\t";
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineOffset = selection.end > selection.start ?
+ selection.end - 1 : selection.end;
+ let lastLine = model.getLineAtOffset(lastLineOffset);
+
+ if (this._expandTab) {
+ let offsetFromLineStart = firstLine == lastLine ?
+ selection.start - firstLineStart : 0;
+ let spaces = this._tabSize - (offsetFromLineStart % this._tabSize);
+ indent = (new Array(spaces + 1)).join(" ");
+ }
+
+ // Do selection indentation.
+ if (firstLine != lastLine) {
+ let lines = [""];
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+ let selectedLines = lastLine - firstLine + 1;
+
+ for (let i = firstLine; i <= lastLine; i++) {
+ lines.push(model.getLine(i, true));
+ }
+
+ this.startCompoundChange();
+
+ this.setText(lines.join(indent), firstLineStart, lastLineEnd);
+
+ let newSelectionStart = firstLineStart == selection.start ?
+ selection.start : selection.start + indent.length;
+ let newSelectionEnd = selection.end + (selectedLines * indent.length);
+
+ this._view.setSelection(newSelectionStart, newSelectionEnd);
+
+ this.endCompoundChange();
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * The "Unindent lines" editor action implementation. This method is invoked
+ * when the user presses Shift-Tab.
+ * @private
+ */
+ _doUnindentLines: function SE__doUnindentLines()
+ {
+ if (this.readOnly) {
+ return true;
+ }
+
+ let indent = "\t";
+
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let lastLineOffset = selection.end > selection.start ?
+ selection.end - 1 : selection.end;
+ let lastLine = model.getLineAtOffset(lastLineOffset);
+
+ if (this._expandTab) {
+ indent = (new Array(this._tabSize + 1)).join(" ");
+ }
+
+ let lines = [];
+ for (let line, i = firstLine; i <= lastLine; i++) {
+ line = model.getLine(i, true);
+ if (line.indexOf(indent) != 0) {
+ return true;
+ }
+ lines.push(line.substring(indent.length));
+ }
+
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineStart = this.getLineStart(lastLine);
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+
+ this.startCompoundChange();
+
+ this.setText(lines.join(""), firstLineStart, lastLineEnd);
+
+ let selectedLines = lastLine - firstLine + 1;
+ let newSelectionStart = firstLineStart == selection.start ?
+ selection.start :
+ Math.max(firstLineStart,
+ selection.start - indent.length);
+ let newSelectionEnd = selection.end - (selectedLines * indent.length) +
+ (selection.end == lastLineStart + 1 ? 1 : 0);
+ if (firstLine == lastLine) {
+ newSelectionEnd = Math.max(lastLineStart, newSelectionEnd);
+ }
+ this._view.setSelection(newSelectionStart, newSelectionEnd);
+
+ this.endCompoundChange();
+
+ return true;
+ },
+
+ /**
+ * The editor Enter action implementation, which adds simple automatic
+ * indentation based on the previous line when the user presses the Enter key.
+ * @private
+ */
+ _doEnter: function SE__doEnter()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ if (selection.start != selection.end) {
+ return false;
+ }
+
+ let model = this._model;
+ let lineIndex = model.getLineAtOffset(selection.start);
+ let lineText = model.getLine(lineIndex, true);
+ let lineStart = this.getLineStart(lineIndex);
+ let index = 0;
+ let lineOffset = selection.start - lineStart;
+ while (index < lineOffset && /[ \t]/.test(lineText.charAt(index))) {
+ index++;
+ }
+
+ if (!index) {
+ return false;
+ }
+
+ let prefix = lineText.substring(0, index);
+ index = lineOffset;
+ while (index < lineText.length &&
+ /[ \t]/.test(lineText.charAt(index++))) {
+ selection.end++;
+ }
+
+ this.setText(this.getLineDelimiter() + prefix, selection.start,
+ selection.end);
+ return true;
+ },
+
+ /**
+ * Move lines upwards or downwards, relative to the current caret location.
+ *
+ * @private
+ * @param boolean aLineAbove
+ * True if moving lines up, false to move lines down.
+ */
+ _moveLines: function SE__moveLines(aLineAbove)
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let model = this._model;
+ let selection = this.getSelection();
+ let firstLine = model.getLineAtOffset(selection.start);
+ if (firstLine == 0 && aLineAbove) {
+ return true;
+ }
+
+ let lastLine = model.getLineAtOffset(selection.end);
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineStart = this.getLineStart(lastLine);
+ if (selection.start != selection.end && lastLineStart == selection.end) {
+ lastLine--;
+ }
+ if (!aLineAbove && (lastLine + 1) == this.getLineCount()) {
+ return true;
+ }
+
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+ let text = this.getText(firstLineStart, lastLineEnd);
+
+ if (aLineAbove) {
+ let aboveLine = firstLine - 1;
+ let aboveLineStart = this.getLineStart(aboveLine);
+
+ this.startCompoundChange();
+ if (lastLine == (this.getLineCount() - 1)) {
+ let delimiterStart = this.getLineEnd(aboveLine);
+ let delimiterEnd = this.getLineEnd(aboveLine, true);
+ let lineDelimiter = this.getText(delimiterStart, delimiterEnd);
+ text += lineDelimiter;
+ this.setText("", firstLineStart - lineDelimiter.length, lastLineEnd);
+ } else {
+ this.setText("", firstLineStart, lastLineEnd);
+ }
+ this.setText(text, aboveLineStart, aboveLineStart);
+ this.endCompoundChange();
+ this.setSelection(aboveLineStart, aboveLineStart + text.length);
+ } else {
+ let belowLine = lastLine + 1;
+ let belowLineEnd = this.getLineEnd(belowLine, true);
+
+ let insertAt = belowLineEnd - lastLineEnd + firstLineStart;
+ let lineDelimiter = "";
+ if (belowLine == this.getLineCount() - 1) {
+ let delimiterStart = this.getLineEnd(lastLine);
+ lineDelimiter = this.getText(delimiterStart, lastLineEnd);
+ text = lineDelimiter + text.substr(0, text.length -
+ lineDelimiter.length);
+ }
+ this.startCompoundChange();
+ this.setText("", firstLineStart, lastLineEnd);
+ this.setText(text, insertAt, insertAt);
+ this.endCompoundChange();
+ this.setSelection(insertAt + lineDelimiter.length,
+ insertAt + text.length);
+ }
+ return true;
+ },
+
+ /**
+ * The Orion Selection event handler. The current caret line is
+ * highlighted and for Linux users the selected text is copied into the X11
+ * PRIMARY buffer.
+ *
+ * @private
+ * @param object aEvent
+ * The Orion Selection event object.
+ */
+ _onOrionSelection: function SE__onOrionSelection(aEvent)
+ {
+ if (this._config.highlightCurrentLine) {
+ this._highlightCurrentLine(aEvent);
+ }
+
+ if (Services.appinfo.OS == "Linux") {
+ let window = this.parentElement.ownerDocument.defaultView;
+
+ if (this._primarySelectionTimeout) {
+ window.clearTimeout(this._primarySelectionTimeout);
+ }
+ this._primarySelectionTimeout =
+ window.setTimeout(this._updatePrimarySelection.bind(this),
+ PRIMARY_SELECTION_DELAY);
+ }
+ },
+
+ /**
+ * The TextChanged event handler which tracks the dirty state of the editor.
+ *
+ * @see SourceEditor.EVENTS.TEXT_CHANGED
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @see SourceEditor.dirty
+ * @private
+ */
+ _onTextChanged: function SE__onTextChanged()
+ {
+ this._updateDirty();
+ },
+
+ /**
+ * The Orion contextmenu event handler. This method opens the default or
+ * the custom context menu popup at the pointer location.
+ *
+ * @param object aEvent
+ * The contextmenu event object coming from Orion. This object should
+ * hold the screenX and screenY properties.
+ */
+ _onOrionContextMenu: function SE__onOrionContextMenu(aEvent)
+ {
+ if (this._contextMenu.state == "closed") {
+ this._contextMenu.openPopupAtScreen(aEvent.screenX || 0,
+ aEvent.screenY || 0, true);
+ }
+ },
+
+ /**
+ * Update the dirty state of the editor based on the undo stack.
+ * @private
+ */
+ _updateDirty: function SE__updateDirty()
+ {
+ this.dirty = !this._undoStack.isClean();
+ },
+
+ /**
+ * Update the X11 PRIMARY buffer to hold the current selection.
+ * @private
+ */
+ _updatePrimarySelection: function SE__updatePrimarySelection()
+ {
+ this._primarySelectionTimeout = null;
+
+ let text = this.getSelectedText();
+ if (!text) {
+ return;
+ }
+
+ clipboardHelper.copyStringToClipboard(text,
+ Ci.nsIClipboard.kSelectionClipboard,
+ this.parentElement.ownerDocument);
+ },
+
+ /**
+ * Highlight the current line using the Orion annotation model.
+ *
+ * @private
+ * @param object aEvent
+ * The Selection event object.
+ */
+ _highlightCurrentLine: function SE__highlightCurrentLine(aEvent)
+ {
+ let annotationModel = this._annotationModel;
+ let model = this._model;
+ let oldAnnotation = this._currentLineAnnotation;
+ let newSelection = aEvent.newValue;
+
+ let collapsed = newSelection.start == newSelection.end;
+ if (!collapsed) {
+ if (oldAnnotation) {
+ annotationModel.removeAnnotation(oldAnnotation);
+ this._currentLineAnnotation = null;
+ }
+ return;
+ }
+
+ let line = model.getLineAtOffset(newSelection.start);
+ let lineStart = this.getLineStart(line);
+ let lineEnd = this.getLineEnd(line);
+
+ let title = oldAnnotation ? oldAnnotation.title :
+ SourceEditorUI.strings.GetStringFromName("annotation.currentLine");
+
+ this._currentLineAnnotation = {
+ start: lineStart,
+ end: lineEnd,
+ type: ORION_ANNOTATION_TYPES.currentLine,
+ title: title,
+ html: "<div class='annotationHTML currentLine'></div>",
+ overviewStyle: {styleClass: "annotationOverview currentLine"},
+ lineStyle: {styleClass: "annotationLine currentLine"},
+ };
+
+ annotationModel.replaceAnnotations(oldAnnotation ? [oldAnnotation] : null,
+ [this._currentLineAnnotation]);
+ },
+
+ /**
+ * The click event handler for the lines gutter. This function allows the user
+ * to jump to a line or to perform line selection while holding the Shift key
+ * down.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _linesRulerClick: function SE__linesRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined || aLineIndex == -1) {
+ return;
+ }
+
+ if (aEvent.shiftKey) {
+ let model = this._model;
+ let selection = this.getSelection();
+ let selectionLineStart = model.getLineAtOffset(selection.start);
+ let selectionLineEnd = model.getLineAtOffset(selection.end);
+ let newStart = aLineIndex <= selectionLineStart ?
+ this.getLineStart(aLineIndex) : selection.start;
+ let newEnd = aLineIndex <= selectionLineStart ?
+ selection.end : this.getLineEnd(aLineIndex);
+ this.setSelection(newStart, newEnd);
+ } else {
+ if (this._annotationRuler) {
+ this._annotationRulerClick(aLineIndex, aEvent);
+ } else {
+ this.setCaretPosition(aLineIndex);
+ }
+ }
+ },
+
+ /**
+ * The dblclick event handler for the lines gutter. This function selects the
+ * whole line where the event occurred.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the double click event occurred.
+ * @param object aEvent
+ * The DOM dblclick event object.
+ */
+ _linesRulerDblClick: function SE__linesRulerDblClick(aLineIndex)
+ {
+ if (aLineIndex === undefined) {
+ return;
+ }
+
+ let newStart = this.getLineStart(aLineIndex);
+ let newEnd = this.getLineEnd(aLineIndex);
+ this.setSelection(newStart, newEnd);
+ },
+
+ /**
+ * Highlight the Orion annotations. This updates the annotation styler as
+ * needed.
+ * @private
+ */
+ _highlightAnnotations: function SE__highlightAnnotations()
+ {
+ if (this._annotationStyler) {
+ this._annotationStyler.destroy();
+ this._annotationStyler = null;
+ }
+
+ let AnnotationStyler =
+ this._iframeWindow.require("orion/textview/annotations").AnnotationStyler;
+
+ let styler = new AnnotationStyler(this._view, this._annotationModel);
+ this._annotationStyler = styler;
+
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.task);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+
+ if (this._config.highlightCurrentLine) {
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentLine);
+ }
+ },
+
+ /**
+ * Retrieve the list of Orion Annotations filtered by type for the given text range.
+ *
+ * @private
+ * @param string aType
+ * The annotation type to filter annotations for. Use one of the keys
+ * in ORION_ANNOTATION_TYPES.
+ * @param number aStart
+ * Offset from where to start finding the annotations.
+ * @param number aEnd
+ * End offset for retrieving the annotations.
+ * @return array
+ * The array of annotations, filtered by type, within the given text
+ * range.
+ */
+ _getAnnotationsByType: function SE__getAnnotationsByType(aType, aStart, aEnd)
+ {
+ let annotations = this._annotationModel.getAnnotations(aStart, aEnd);
+ let annotation, result = [];
+ while (annotation = annotations.next()) {
+ if (annotation.type == ORION_ANNOTATION_TYPES[aType]) {
+ result.push(annotation);
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * The click event handler for the annotation ruler.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _annotationRulerClick: function SE__annotationRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined || aLineIndex == -1) {
+ return;
+ }
+
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+ if (annotations.length > 0) {
+ this.removeBreakpoint(aLineIndex);
+ } else {
+ this.addBreakpoint(aLineIndex);
+ }
+ },
+
+ /**
+ * The click event handler for the overview ruler. When the user clicks on an
+ * annotation the editor jumps to the associated line.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _overviewRulerClick: function SE__overviewRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined || aLineIndex == -1) {
+ return;
+ }
+
+ let model = this._model;
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+ let annotations = this._annotationModel.getAnnotations(lineStart, lineEnd);
+ let annotation = annotations.next();
+
+ // Jump to the line where annotation is. If the annotation is specific to
+ // a substring part of the line, then select the substring.
+ if (!annotation || lineStart == annotation.start && lineEnd == annotation.end) {
+ this.setSelection(lineStart, lineStart);
+ } else {
+ this.setSelection(annotation.start, annotation.end);
+ }
+ },
+
+ /**
+ * Get the editor element.
+ *
+ * @return nsIDOMElement
+ * In this implementation a xul:iframe holds the editor.
+ */
+ get editorElement() {
+ return this._iframe;
+ },
+
+ /**
+ * Helper function to retrieve the strings used for comments in the current
+ * editor mode.
+ *
+ * @private
+ * @return object
+ * An object that holds the following properties:
+ * - line: the comment string used for the start of a single line
+ * comment.
+ * - blockStart: the comment string used for the start of a comment
+ * block.
+ * - blockEnd: the comment string used for the end of a block comment.
+ * Null is returned for unsupported editor modes.
+ */
+ _getCommentStrings: function SE__getCommentStrings()
+ {
+ let line = "";
+ let blockCommentStart = "";
+ let blockCommentEnd = "";
+
+ switch (this.getMode()) {
+ case SourceEditor.MODES.JAVASCRIPT:
+ line = "//";
+ blockCommentStart = "/*";
+ blockCommentEnd = "*/";
+ break;
+ case SourceEditor.MODES.CSS:
+ blockCommentStart = "/*";
+ blockCommentEnd = "*/";
+ break;
+ case SourceEditor.MODES.HTML:
+ case SourceEditor.MODES.XML:
+ blockCommentStart = "<!--";
+ blockCommentEnd = "-->";
+ break;
+ default:
+ return null;
+ }
+ return {line: line, blockStart: blockCommentStart, blockEnd: blockCommentEnd};
+ },
+
+ /**
+ * Decide whether to comment the selection/current line or to uncomment it.
+ *
+ * @private
+ */
+ _doCommentUncomment: function SE__doCommentUncomment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let lastLine = model.getLineAtOffset(selection.end);
+
+ // Checks for block comment.
+ let firstLineText = model.getLine(firstLine);
+ let lastLineText = model.getLine(lastLine);
+ let openIndex = firstLineText.indexOf(commentObject.blockStart);
+ let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd);
+ if (openIndex != -1 && closeIndex != -1 &&
+ (firstLine != lastLine ||
+ (closeIndex - openIndex) >= commentObject.blockStart.length)) {
+ return this._doUncomment();
+ }
+
+ if (!commentObject.line) {
+ return this._doComment();
+ }
+
+ // If the selection is not a block comment, check for the first and the last
+ // lines to be line commented.
+ let firstLastCommented = [firstLineText,
+ lastLineText].every(function(aLineText) {
+ let openIndex = aLineText.indexOf(commentObject.line);
+ if (openIndex != -1) {
+ let textUntilComment = aLineText.slice(0, openIndex);
+ if (!textUntilComment || /^\s+$/.test(textUntilComment)) {
+ return true;
+ }
+ }
+ return false;
+ });
+ if (firstLastCommented) {
+ return this._doUncomment();
+ }
+
+ // If we reach here, then we have to comment the selection/line.
+ return this._doComment();
+ },
+
+ /**
+ * Wrap the selected text in comments. If nothing is selected the current
+ * caret line is commented out. Single line and block comments depend on the
+ * current editor mode.
+ *
+ * @private
+ */
+ _doComment: function SE__doComment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+
+ if (selection.start == selection.end) {
+ let selectionLine = this._model.getLineAtOffset(selection.start);
+ let lineStartOffset = this.getLineStart(selectionLine);
+ if (commentObject.line) {
+ this.setText(commentObject.line, lineStartOffset, lineStartOffset);
+ } else {
+ let lineEndOffset = this.getLineEnd(selectionLine);
+ this.startCompoundChange();
+ this.setText(commentObject.blockStart, lineStartOffset, lineStartOffset);
+ this.setText(commentObject.blockEnd,
+ lineEndOffset + commentObject.blockStart.length,
+ lineEndOffset + commentObject.blockStart.length);
+ this.endCompoundChange();
+ }
+ } else {
+ this.startCompoundChange();
+ this.setText(commentObject.blockStart, selection.start, selection.start);
+ this.setText(commentObject.blockEnd,
+ selection.end + commentObject.blockStart.length,
+ selection.end + commentObject.blockStart.length);
+ this.endCompoundChange();
+ }
+
+ return true;
+ },
+
+ /**
+ * Uncomment the selected text. If nothing is selected the current caret line
+ * is umcommented. Single line and block comments depend on the current editor
+ * mode.
+ *
+ * @private
+ */
+ _doUncomment: function SE__doUncomment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ let firstLine = this._model.getLineAtOffset(selection.start);
+ let lastLine = this._model.getLineAtOffset(selection.end);
+
+ // Uncomment a block of text.
+ let firstLineText = this._model.getLine(firstLine);
+ let lastLineText = this._model.getLine(lastLine);
+ let openIndex = firstLineText.indexOf(commentObject.blockStart);
+ let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd);
+ if (openIndex != -1 && closeIndex != -1 &&
+ (firstLine != lastLine ||
+ (closeIndex - openIndex) >= commentObject.blockStart.length)) {
+ let firstLineStartOffset = this.getLineStart(firstLine);
+ let lastLineStartOffset = this.getLineStart(lastLine);
+ let openOffset = firstLineStartOffset + openIndex;
+ let closeOffset = lastLineStartOffset + closeIndex;
+
+ this.startCompoundChange();
+ this.setText("", closeOffset, closeOffset + commentObject.blockEnd.length);
+ this.setText("", openOffset, openOffset + commentObject.blockStart.length);
+ this.endCompoundChange();
+
+ return true;
+ }
+
+ if (!commentObject.line) {
+ return true;
+ }
+
+ // If the selected text is not a block of comment, then uncomment each line.
+ this.startCompoundChange();
+ let lineCaret = firstLine;
+ while (lineCaret <= lastLine) {
+ let currentLine = this._model.getLine(lineCaret);
+ let lineStart = this.getLineStart(lineCaret);
+ let openIndex = currentLine.indexOf(commentObject.line);
+ let openOffset = lineStart + openIndex;
+ let textUntilComment = this.getText(lineStart, openOffset);
+ if (openIndex != -1 &&
+ (!textUntilComment || /^\s+$/.test(textUntilComment))) {
+ this.setText("", openOffset, openOffset + commentObject.line.length);
+ }
+ lineCaret++;
+ }
+ this.endCompoundChange();
+
+ return true;
+ },
+
+ /**
+ * Helper function for _moveToBracket{Opening/Closing} to find the offset of
+ * matching bracket.
+ *
+ * @param number aOffset
+ * The offset of the bracket for which you want to find the bracket.
+ * @private
+ */
+ _getMatchingBracketIndex: function SE__getMatchingBracketIndex(aOffset)
+ {
+ return this._styler._findMatchingBracket(this._model, aOffset);
+ },
+
+ /**
+ * Move the cursor to the matching opening bracket if at corresponding closing
+ * bracket, otherwise move to the opening bracket for the current block of code.
+ *
+ * @private
+ */
+ _moveToBracketOpening: function SE__moveToBracketOpening()
+ {
+ let mode = this.getMode();
+ // Returning early if not in JavaScipt or CSS mode.
+ if (mode != SourceEditor.MODES.JAVASCRIPT &&
+ mode != SourceEditor.MODES.CSS) {
+ return false;
+ }
+
+ let caretOffset = this.getCaretOffset() - 1;
+ let matchingIndex = this._getMatchingBracketIndex(caretOffset);
+
+ // If the caret is not at the closing bracket "}", find the index of the
+ // opening bracket "{" for the current code block.
+ if (matchingIndex == -1 || matchingIndex > caretOffset) {
+ matchingIndex = -1;
+ let text = this.getText();
+ let closingOffset = text.indexOf("}", caretOffset);
+ while (closingOffset > -1) {
+ let closingMatchingIndex = this._getMatchingBracketIndex(closingOffset);
+ if (closingMatchingIndex < caretOffset && closingMatchingIndex != -1) {
+ matchingIndex = closingMatchingIndex;
+ break;
+ }
+ closingOffset = text.indexOf("}", closingOffset + 1);
+ }
+ // Moving to the previous code block starting bracket if caret not inside
+ // any code block.
+ if (matchingIndex == -1) {
+ let lastClosingOffset = text.lastIndexOf("}", caretOffset);
+ while (lastClosingOffset > -1) {
+ let closingMatchingIndex =
+ this._getMatchingBracketIndex(lastClosingOffset);
+ if (closingMatchingIndex < caretOffset &&
+ closingMatchingIndex != -1) {
+ matchingIndex = closingMatchingIndex;
+ break;
+ }
+ lastClosingOffset = text.lastIndexOf("}", lastClosingOffset - 1);
+ }
+ }
+ }
+
+ if (matchingIndex > -1) {
+ this.setCaretOffset(matchingIndex + 1);
+ }
+
+ return true;
+ },
+
+ /**
+ * Moves the cursor to the matching closing bracket if at corresponding
+ * opening bracket, otherwise move to the closing bracket for the current
+ * block of code.
+ *
+ * @private
+ */
+ _moveToBracketClosing: function SE__moveToBracketClosing()
+ {
+ let mode = this.getMode();
+ // Returning early if not in JavaScipt or CSS mode.
+ if (mode != SourceEditor.MODES.JAVASCRIPT &&
+ mode != SourceEditor.MODES.CSS) {
+ return false;
+ }
+
+ let caretOffset = this.getCaretOffset();
+ let matchingIndex = this._getMatchingBracketIndex(caretOffset - 1);
+
+ // If the caret is not at the opening bracket "{", find the index of the
+ // closing bracket "}" for the current code block.
+ if (matchingIndex == -1 || matchingIndex < caretOffset) {
+ matchingIndex = -1;
+ let text = this.getText();
+ let openingOffset = text.lastIndexOf("{", caretOffset);
+ while (openingOffset > -1) {
+ let openingMatchingIndex = this._getMatchingBracketIndex(openingOffset);
+ if (openingMatchingIndex > caretOffset) {
+ matchingIndex = openingMatchingIndex;
+ break;
+ }
+ openingOffset = text.lastIndexOf("{", openingOffset - 1);
+ }
+ // Moving to the next code block ending bracket if caret not inside
+ // any code block.
+ if (matchingIndex == -1) {
+ let nextOpeningIndex = text.indexOf("{", caretOffset + 1);
+ while (nextOpeningIndex > -1) {
+ let openingMatchingIndex =
+ this._getMatchingBracketIndex(nextOpeningIndex);
+ if (openingMatchingIndex > caretOffset) {
+ matchingIndex = openingMatchingIndex;
+ break;
+ }
+ nextOpeningIndex = text.indexOf("{", nextOpeningIndex + 1);
+ }
+ }
+ }
+
+ if (matchingIndex > -1) {
+ this.setCaretOffset(matchingIndex);
+ }
+
+ return true;
+ },
+
+ /**
+ * Add an event listener to the editor. You can use one of the known events.
+ *
+ * @see SourceEditor.EVENTS
+ *
+ * @param string aEventType
+ * The event type you want to listen for.
+ * @param function aCallback
+ * The function you want executed when the event is triggered.
+ */
+ addEventListener: function SE_addEventListener(aEventType, aCallback)
+ {
+ if (this._view && aEventType in ORION_EVENTS) {
+ this._view.addEventListener(ORION_EVENTS[aEventType], aCallback);
+ } else if (this._eventTarget.addEventListener) {
+ this._eventTarget.addEventListener(aEventType, aCallback);
+ } else {
+ this._eventListenersQueue.push(["add", aEventType, aCallback]);
+ }
+ },
+
+ /**
+ * Remove an event listener from the editor. You can use one of the known
+ * events.
+ *
+ * @see SourceEditor.EVENTS
+ *
+ * @param string aEventType
+ * The event type you have a listener for.
+ * @param function aCallback
+ * The function you have as the event handler.
+ */
+ removeEventListener: function SE_removeEventListener(aEventType, aCallback)
+ {
+ if (this._view && aEventType in ORION_EVENTS) {
+ this._view.removeEventListener(ORION_EVENTS[aEventType], aCallback);
+ } else if (this._eventTarget.removeEventListener) {
+ this._eventTarget.removeEventListener(aEventType, aCallback);
+ } else {
+ this._eventListenersQueue.push(["remove", aEventType, aCallback]);
+ }
+ },
+
+ /**
+ * Undo a change in the editor.
+ *
+ * @return boolean
+ * True if there was a change undone, false otherwise.
+ */
+ undo: function SE_undo()
+ {
+ let result = this._undoStack.undo();
+ this.ui._onUndoRedo();
+ return result;
+ },
+
+ /**
+ * Redo a change in the editor.
+ *
+ * @return boolean
+ * True if there was a change redone, false otherwise.
+ */
+ redo: function SE_redo()
+ {
+ let result = this._undoStack.redo();
+ this.ui._onUndoRedo();
+ return result;
+ },
+
+ /**
+ * Check if there are changes that can be undone.
+ *
+ * @return boolean
+ * True if there are changes that can be undone, false otherwise.
+ */
+ canUndo: function SE_canUndo()
+ {
+ return this._undoStack.canUndo();
+ },
+
+ /**
+ * Check if there are changes that can be repeated.
+ *
+ * @return boolean
+ * True if there are changes that can be repeated, false otherwise.
+ */
+ canRedo: function SE_canRedo()
+ {
+ return this._undoStack.canRedo();
+ },
+
+ /**
+ * Reset the Undo stack.
+ */
+ resetUndo: function SE_resetUndo()
+ {
+ this._undoStack.reset();
+ this._updateDirty();
+ this.ui._onUndoRedo();
+ },
+
+ /**
+ * Set the "dirty" state of the editor. Set this to false when you save the
+ * text being edited. The dirty state will become true once the user makes
+ * changes to the text.
+ *
+ * @param boolean aNewValue
+ * The new dirty state: true if the text is not saved, false if you
+ * just saved the text.
+ */
+ set dirty(aNewValue)
+ {
+ if (aNewValue == this._dirty) {
+ return;
+ }
+
+ let event = {
+ type: SourceEditor.EVENTS.DIRTY_CHANGED,
+ oldValue: this._dirty,
+ newValue: aNewValue,
+ };
+
+ this._dirty = aNewValue;
+ if (!this._dirty && !this._undoStack.isClean()) {
+ this._undoStack.markClean();
+ }
+ this._dispatchEvent(event);
+ },
+
+ /**
+ * Get the editor "dirty" state. This tells if the text is considered saved or
+ * not.
+ *
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @return boolean
+ * True if there are changes which are not saved, false otherwise.
+ */
+ get dirty()
+ {
+ return this._dirty;
+ },
+
+ /**
+ * Start a compound change in the editor. Compound changes are grouped into
+ * only one change that you can undo later, after you invoke
+ * endCompoundChange().
+ */
+ startCompoundChange: function SE_startCompoundChange()
+ {
+ this._undoStack.startCompoundChange();
+ },
+
+ /**
+ * End a compound change in the editor.
+ */
+ endCompoundChange: function SE_endCompoundChange()
+ {
+ this._undoStack.endCompoundChange();
+ },
+
+ /**
+ * Focus the editor.
+ */
+ focus: function SE_focus()
+ {
+ this._view.focus();
+ },
+
+ /**
+ * Get the first visible line number.
+ *
+ * @return number
+ * The line number, counting from 0.
+ */
+ getTopIndex: function SE_getTopIndex()
+ {
+ return this._view.getTopIndex();
+ },
+
+ /**
+ * Set the first visible line number.
+ *
+ * @param number aTopIndex
+ * The line number, counting from 0.
+ */
+ setTopIndex: function SE_setTopIndex(aTopIndex)
+ {
+ this._view.setTopIndex(aTopIndex);
+ },
+
+ /**
+ * Check if the editor has focus.
+ *
+ * @return boolean
+ * True if the editor is focused, false otherwise.
+ */
+ hasFocus: function SE_hasFocus()
+ {
+ return this._view.hasFocus();
+ },
+
+ /**
+ * 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 SE_getText(aStart, aEnd)
+ {
+ return this._view.getText(aStart, aEnd);
+ },
+
+ /**
+ * Get the start character offset of the line with index aLineIndex.
+ *
+ * @param number aLineIndex
+ * Zero based index of the line.
+ * @return number
+ * Line start offset or -1 if out of range.
+ */
+ getLineStart: function SE_getLineStart(aLineIndex)
+ {
+ return this._model.getLineStart(aLineIndex);
+ },
+
+ /**
+ * Get the end character offset of the line with index aLineIndex,
+ * excluding the end offset. When the line delimiter is present,
+ * the offset is the start offset of the next line or the char count.
+ * Otherwise, it is the offset of the line delimiter.
+ *
+ * @param number aLineIndex
+ * Zero based index of the line.
+ * @param boolean [aIncludeDelimiter = false]
+ * Optional, whether or not to include the line delimiter.
+ * @return number
+ * Line end offset or -1 if out of range.
+ */
+ getLineEnd: function SE_getLineEnd(aLineIndex, aIncludeDelimiter)
+ {
+ return this._model.getLineEnd(aLineIndex, aIncludeDelimiter);
+ },
+
+ /**
+ * Get the number of characters in the editor content.
+ *
+ * @return number
+ * The number of editor content characters.
+ */
+ getCharCount: function SE_getCharCount()
+ {
+ return this._model.getCharCount();
+ },
+
+ /**
+ * Get the selected text.
+ *
+ * @return string
+ * The currently selected text.
+ */
+ getSelectedText: function SE_getSelectedText()
+ {
+ let selection = this.getSelection();
+ return this.getText(selection.start, selection.end);
+ },
+
+ /**
+ * Replace text in the source editor with the given text, in the given range.
+ *
+ * @param string aText
+ * The text you want to put into the editor.
+ * @param number [aStart=0]
+ * Optional, the start offset, zero based, from where you want to start
+ * replacing text in the editor.
+ * @param number [aEnd=char count]
+ * Optional, the end offset, zero based, where you want to stop
+ * replacing text in the editor.
+ */
+ setText: function SE_setText(aText, aStart, aEnd)
+ {
+ this._view.setText(aText, aStart, aEnd);
+ },
+
+ /**
+ * Drop the current selection / deselect.
+ */
+ dropSelection: function SE_dropSelection()
+ {
+ this.setCaretOffset(this.getCaretOffset());
+ },
+
+ /**
+ * Select a specific range in the editor.
+ *
+ * @param number aStart
+ * Selection range start.
+ * @param number aEnd
+ * Selection range end.
+ */
+ setSelection: function SE_setSelection(aStart, aEnd)
+ {
+ this._view.setSelection(aStart, aEnd, true);
+ },
+
+ /**
+ * Get the current selection range.
+ *
+ * @return object
+ * An object with two properties, start and end, that give the
+ * selection range (zero based offsets).
+ */
+ getSelection: function SE_getSelection()
+ {
+ return this._view.getSelection();
+ },
+
+ /**
+ * Get the current caret offset.
+ *
+ * @return number
+ * The current caret offset.
+ */
+ getCaretOffset: function SE_getCaretOffset()
+ {
+ return this._view.getCaretOffset();
+ },
+
+ /**
+ * Set the caret offset.
+ *
+ * @param number aOffset
+ * The new caret offset you want to set.
+ */
+ setCaretOffset: function SE_setCaretOffset(aOffset)
+ {
+ this._view.setCaretOffset(aOffset, true);
+ },
+
+ /**
+ * Get the caret position.
+ *
+ * @return object
+ * An object that holds two properties:
+ * - line: the line number, counting from 0.
+ * - col: the column number, counting from 0.
+ */
+ getCaretPosition: function SE_getCaretPosition()
+ {
+ let offset = this.getCaretOffset();
+ let line = this._model.getLineAtOffset(offset);
+ let lineStart = this.getLineStart(line);
+ let column = offset - lineStart;
+ return {line: line, col: column};
+ },
+
+ /**
+ * Set the caret position: line and column.
+ *
+ * @param number aLine
+ * The new caret line location. Line numbers start from 0.
+ * @param number [aColumn=0]
+ * Optional. The new caret column location. Columns start from 0.
+ * @param number [aAlign=0]
+ * Optional. Position of the line with respect to viewport.
+ * Allowed values are:
+ * SourceEditor.VERTICAL_ALIGN.TOP target line at top of view.
+ * SourceEditor.VERTICAL_ALIGN.CENTER target line at center of view.
+ * SourceEditor.VERTICAL_ALIGN.BOTTOM target line at bottom of view.
+ */
+ setCaretPosition: function SE_setCaretPosition(aLine, aColumn, aAlign)
+ {
+ let editorHeight = this._view.getClientArea().height;
+ let lineHeight = this._view.getLineHeight();
+ let linesVisible = Math.floor(editorHeight/lineHeight);
+ let halfVisible = Math.round(linesVisible/2);
+ let firstVisible = this.getTopIndex();
+ let lastVisible = this._view.getBottomIndex();
+ let caretOffset = this.getLineStart(aLine) + (aColumn || 0);
+
+ this._view.setSelection(caretOffset, caretOffset, false);
+
+ // If the target line is in view, skip the vertical alignment part.
+ if (aLine <= lastVisible && aLine >= firstVisible) {
+ this._view.showSelection();
+ return;
+ }
+
+ // Setting the offset so that the line always falls in the upper half
+ // of visible lines (lower half for BOTTOM aligned).
+ // VERTICAL_OFFSET is the maximum allowed value.
+ let offset = Math.min(halfVisible, VERTICAL_OFFSET);
+
+ let topIndex;
+ switch (aAlign) {
+ case this.VERTICAL_ALIGN.CENTER:
+ topIndex = Math.max(aLine - halfVisible, 0);
+ break;
+
+ case this.VERTICAL_ALIGN.BOTTOM:
+ topIndex = Math.max(aLine - linesVisible + offset, 0);
+ break;
+
+ default: // this.VERTICAL_ALIGN.TOP.
+ topIndex = Math.max(aLine - offset, 0);
+ break;
+ }
+ // Bringing down the topIndex to total lines in the editor if exceeding.
+ topIndex = Math.min(topIndex, this.getLineCount());
+ this.setTopIndex(topIndex);
+
+ let location = this._view.getLocationAtOffset(caretOffset);
+ this._view.setHorizontalPixel(location.x);
+ },
+
+ /**
+ * Get the line count.
+ *
+ * @return number
+ * The number of lines in the document being edited.
+ */
+ getLineCount: function SE_getLineCount()
+ {
+ return this._model.getLineCount();
+ },
+
+ /**
+ * Get the line delimiter used in the document being edited.
+ *
+ * @return string
+ * The line delimiter.
+ */
+ getLineDelimiter: function SE_getLineDelimiter()
+ {
+ return this._model.getLineDelimiter();
+ },
+
+ /**
+ * Get the indentation string used in the document being edited.
+ *
+ * @return string
+ * The indentation string.
+ */
+ getIndentationString: function SE_getIndentationString()
+ {
+ if (this._expandTab) {
+ return (new Array(this._tabSize + 1)).join(" ");
+ }
+ return "\t";
+ },
+
+ /**
+ * Set the source editor mode to the file type you are editing.
+ *
+ * @param string aMode
+ * One of the predefined SourceEditor.MODES.
+ */
+ setMode: function SE_setMode(aMode)
+ {
+ if (this._styler) {
+ this._styler.destroy();
+ this._styler = null;
+ }
+
+ let window = this._iframeWindow;
+
+ switch (aMode) {
+ case SourceEditor.MODES.JAVASCRIPT:
+ case SourceEditor.MODES.CSS:
+ let TextStyler =
+ window.require("examples/textview/textStyler").TextStyler;
+
+ this._styler = new TextStyler(this._view, aMode, this._annotationModel);
+ this._styler.setFoldingEnabled(false);
+ break;
+
+ case SourceEditor.MODES.HTML:
+ case SourceEditor.MODES.XML:
+ let TextMateStyler =
+ window.require("orion/editor/textMateStyler").TextMateStyler;
+ let HtmlGrammar =
+ window.require("orion/editor/htmlGrammar").HtmlGrammar;
+ this._styler = new TextMateStyler(this._view, new HtmlGrammar());
+ break;
+ }
+
+ this._highlightAnnotations();
+ this._mode = aMode;
+ },
+
+ /**
+ * Get the current source editor mode.
+ *
+ * @return string
+ * Returns one of the predefined SourceEditor.MODES.
+ */
+ getMode: function SE_getMode()
+ {
+ return this._mode;
+ },
+
+ /**
+ * Setter for the read-only state of the editor.
+ * @param boolean aValue
+ * Tells if you want the editor to read-only or not.
+ */
+ set readOnly(aValue)
+ {
+ this._view.setOptions({
+ readonly: aValue,
+ themeClass: "mozilla" + (aValue ? " readonly" : ""),
+ });
+ },
+
+ /**
+ * Getter for the read-only state of the editor.
+ * @type boolean
+ */
+ get readOnly()
+ {
+ return this._view.getOptions("readonly");
+ },
+
+ /**
+ * Set the current debugger location at the given line index. This is useful in
+ * a debugger or in any other context where the user needs to track the
+ * current state, where a debugger-like environment is at.
+ *
+ * @param number aLineIndex
+ * Line index of the current debugger location, starting from 0.
+ * Use any negative number to clear the current location.
+ */
+ setDebugLocation: function SE_setDebugLocation(aLineIndex)
+ {
+ let annotations = this._getAnnotationsByType("debugLocation", 0,
+ this.getCharCount());
+ if (annotations.length > 0) {
+ annotations.forEach(this._annotationModel.removeAnnotation,
+ this._annotationModel);
+ }
+ if (aLineIndex < 0) {
+ return;
+ }
+
+ let lineStart = this._model.getLineStart(aLineIndex);
+ let lineEnd = this._model.getLineEnd(aLineIndex);
+ let lineText = this._model.getLine(aLineIndex);
+ let title = SourceEditorUI.strings.
+ formatStringFromName("annotation.debugLocation.title",
+ [lineText], 1);
+
+ let annotation = {
+ type: ORION_ANNOTATION_TYPES.debugLocation,
+ start: lineStart,
+ end: lineEnd,
+ title: title,
+ style: {styleClass: "annotation debugLocation"},
+ html: "<div class='annotationHTML debugLocation'></div>",
+ overviewStyle: {styleClass: "annotationOverview debugLocation"},
+ rangeStyle: {styleClass: "annotationRange debugLocation"},
+ lineStyle: {styleClass: "annotationLine debugLocation"},
+ };
+ this._annotationModel.addAnnotation(annotation);
+ },
+
+ /**
+ * Retrieve the current debugger line index configured for this editor.
+ *
+ * @return number
+ * The line index starting from 0 where the current debugger is
+ * paused. If no debugger location has been set -1 is returned.
+ */
+ getDebugLocation: function SE_getDebugLocation()
+ {
+ let annotations = this._getAnnotationsByType("debugLocation", 0,
+ this.getCharCount());
+ if (annotations.length > 0) {
+ return this._model.getLineAtOffset(annotations[0].start);
+ }
+ return -1;
+ },
+
+ /**
+ * Add a breakpoint at the given line index.
+ *
+ * @param number aLineIndex
+ * Line index where to add the breakpoint (starts from 0).
+ * @param string [aCondition]
+ * Optional breakpoint condition.
+ */
+ addBreakpoint: function SE_addBreakpoint(aLineIndex, aCondition)
+ {
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+ if (annotations.length > 0) {
+ return;
+ }
+
+ let lineText = this._model.getLine(aLineIndex);
+ let title = SourceEditorUI.strings.
+ formatStringFromName("annotation.breakpoint.title",
+ [lineText], 1);
+
+ let annotation = {
+ type: ORION_ANNOTATION_TYPES.breakpoint,
+ start: lineStart,
+ end: lineEnd,
+ breakpointCondition: aCondition,
+ title: title,
+ style: {styleClass: "annotation breakpoint"},
+ html: "<div class='annotationHTML breakpoint'></div>",
+ overviewStyle: {styleClass: "annotationOverview breakpoint"},
+ rangeStyle: {styleClass: "annotationRange breakpoint"}
+ };
+ this._annotationModel.addAnnotation(annotation);
+
+ let event = {
+ type: SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ added: [{line: aLineIndex, condition: aCondition}],
+ removed: [],
+ };
+
+ this._dispatchEvent(event);
+ },
+
+ /**
+ * Remove the current breakpoint from the given line index.
+ *
+ * @param number aLineIndex
+ * Line index from where to remove the breakpoint (starts from 0).
+ * @return boolean
+ * True if a breakpoint was removed, false otherwise.
+ */
+ removeBreakpoint: function SE_removeBreakpoint(aLineIndex)
+ {
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+
+ let event = {
+ type: SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ added: [],
+ removed: [],
+ };
+
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+
+ annotations.forEach(function(annotation) {
+ this._annotationModel.removeAnnotation(annotation);
+ event.removed.push({line: aLineIndex,
+ condition: annotation.breakpointCondition});
+ }, this);
+
+ if (event.removed.length > 0) {
+ this._dispatchEvent(event);
+ }
+
+ return event.removed.length > 0;
+ },
+
+ /**
+ * Get the list of breakpoints in the Source Editor instance.
+ *
+ * @return array
+ * The array of breakpoints. Each item is an object with two
+ * properties: line and condition.
+ */
+ getBreakpoints: function SE_getBreakpoints()
+ {
+ let annotations = this._getAnnotationsByType("breakpoint", 0,
+ this.getCharCount());
+ let breakpoints = [];
+
+ annotations.forEach(function(annotation) {
+ breakpoints.push({line: this._model.getLineAtOffset(annotation.start),
+ condition: annotation.breakpointCondition});
+ }, this);
+
+ return breakpoints;
+ },
+
+ /**
+ * Convert the given rectangle from one coordinate reference to another.
+ *
+ * Known coordinate references:
+ * - "document" - gives the coordinates relative to the entire document.
+ * - "view" - gives the coordinates relative to the editor viewport.
+ *
+ * @param object aRect
+ * The rectangle to convert. Object properties: x, y, width and height.
+ * @param string aFrom
+ * The source coordinate reference.
+ * @param string aTo
+ * The destination coordinate reference.
+ * @return object aRect
+ * Returns the rectangle with changed coordinates.
+ */
+ convertCoordinates: function SE_convertCoordinates(aRect, aFrom, aTo)
+ {
+ return this._view.convert(aRect, aFrom, aTo);
+ },
+
+ /**
+ * Get the character offset nearest to the given pixel location.
+ *
+ * @param number aX
+ * @param number aY
+ * @return number
+ * Returns the character offset at the given location.
+ */
+ getOffsetAtLocation: function SE_getOffsetAtLocation(aX, aY)
+ {
+ return this._view.getOffsetAtLocation(aX, aY);
+ },
+
+ /**
+ * Get the pixel location, relative to the document, at the given character
+ * offset.
+ *
+ * @param number aOffset
+ * @return object
+ * The pixel location relative to the document being edited. Two
+ * properties are included: x and y.
+ */
+ getLocationAtOffset: function SE_getLocationAtOffset(aOffset)
+ {
+ return this._view.getLocationAtOffset(aOffset);
+ },
+
+ /**
+ * Get the line location for a given character offset.
+ *
+ * @param number aOffset
+ * @return number
+ * The line location relative to the give character offset.
+ */
+ getLineAtOffset: function SE_getLineAtOffset(aOffset)
+ {
+ return this._model.getLineAtOffset(aOffset);
+ },
+
+ /**
+ * Destroy/uninitialize the editor.
+ */
+ destroy: function SE_destroy()
+ {
+ if (this._config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
+ this.removeEventListener(SourceEditor.EVENTS.SELECTION,
+ this._onOrionSelection);
+ }
+ this._onOrionSelection = null;
+
+ this.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
+ this._onTextChanged);
+ this._onTextChanged = null;
+
+ if (this._contextMenu) {
+ this.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
+ this._onOrionContextMenu);
+ this._contextMenu = null;
+ }
+ this._onOrionContextMenu = null;
+
+ if (this._primarySelectionTimeout) {
+ let window = this.parentElement.ownerDocument.defaultView;
+ window.clearTimeout(this._primarySelectionTimeout);
+ this._primarySelectionTimeout = null;
+ }
+
+ this._view.destroy();
+ this.ui.destroy();
+ this.ui = null;
+
+ this.parentElement.removeChild(this._iframe);
+ this.parentElement = null;
+ this._iframeWindow = null;
+ this._iframe = null;
+ this._undoStack = null;
+ this._styler = null;
+ this._linesRuler = null;
+ this._annotationRuler = null;
+ this._overviewRuler = null;
+ this._dragAndDrop = null;
+ this._annotationModel = null;
+ this._annotationStyler = null;
+ this._currentLineAnnotation = null;
+ this._eventTarget = null;
+ this._eventListenersQueue = null;
+ this._view = null;
+ this._model = null;
+ this._config = null;
+ this._lastFind = null;
+ },
+};
diff --git a/browser/devtools/sourceeditor/source-editor-overlay.xul b/browser/devtools/sourceeditor/source-editor-overlay.xul
new file mode 100644
index 000000000..e4fb7c91f
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor-overlay.xul
@@ -0,0 +1,204 @@
+<?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 overlay [
+ <!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+ %editMenuStrings;
+ <!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
+ %sourceEditorStrings;
+]>
+<overlay id="sourceEditorOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- This Source Editor overlay requires the editMenuOverlay.xul to be loaded.
+ The globalOverlay.js script is also required in the XUL document where
+ the source-editor-overlay.xul is loaded. Do not use #editMenuKeys to
+ avoid conflicts! -->
+
+ <script type="application/javascript">
+ function goUpdateSourceEditorMenuItems()
+ {
+ goUpdateGlobalEditMenuItems();
+ let commands = ['se-cmd-undo', 'se-cmd-redo', 'se-cmd-cut', 'se-cmd-paste',
+ 'se-cmd-delete'];
+ commands.forEach(goUpdateCommand);
+ }
+ </script>
+
+ <commandset id="sourceEditorCommands">
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
+ <command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')" disabled="true"/>
+ <command id="cmd_findPrevious" oncommand="goDoCommand('cmd_findPrevious')" disabled="true"/>
+ <command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
+ <command id="se-cmd-selectAll" oncommand="goDoCommand('se-cmd-selectAll')"/>
+ <command id="se-cmd-cut" oncommand="goDoCommand('se-cmd-cut')" disabled="true"/>
+ <command id="se-cmd-paste" oncommand="goDoCommand('se-cmd-paste')" disabled="true"/>
+ <command id="se-cmd-delete" oncommand="goDoCommand('se-cmd-delete')" disabled="true"/>
+ <command id="se-cmd-undo" oncommand="goDoCommand('se-cmd-undo')" disabled="true"/>
+ <command id="se-cmd-redo" oncommand="goDoCommand('se-cmd-redo')" disabled="true"/>
+ </commandset>
+
+ <keyset id="sourceEditorKeys">
+ <!-- Do not use both #sourceEditorKeys and #editMenuKeys in the same
+ document to avoid conflicts! -->
+ <key id="key_undo"
+ key="&undoCmd.key;"
+ modifiers="accel"
+ command="se-cmd-undo"/>
+#ifdef XP_UNIX
+ <key id="key_redo"
+ key="&undoCmd.key;"
+ modifiers="accel,shift"
+ command="se-cmd-redo"/>
+#else
+ <key id="key_redo"
+ key="&redoCmd.key;"
+ modifiers="accel"
+ command="se-cmd-redo"/>
+#endif
+ <key id="key_cut"
+ key="&cutCmd.key;"
+ modifiers="accel"
+ command="se-cmd-cut"/>
+ <key id="key_copy"
+ key="&copyCmd.key;"
+ modifiers="accel"
+ command="cmd_copy"/>
+ <key id="key_paste"
+ key="&pasteCmd.key;"
+ modifiers="accel"
+ command="se-cmd-paste"/>
+ <key id="key_gotoLine"
+ key="&gotoLineCmd.key;"
+ command="cmd_gotoLine"
+ modifiers="accel"/>
+ <key id="key_delete"
+ keycode="VK_DELETE"
+ command="se-cmd-delete"/>
+ <key id="key_selectAll"
+ key="&selectAllCmd.key;"
+ modifiers="accel"
+ command="se-cmd-selectAll"/>
+ <key id="key_find"
+ key="&findCmd.key;"
+ modifiers="accel"
+ command="cmd_find"/>
+ <key id="key_findAgain"
+ key="&findAgainCmd.key;"
+ modifiers="accel"
+ command="cmd_findAgain"/>
+ <key id="key_findPrevious"
+ key="&findAgainCmd.key;"
+ modifiers="shift,accel"
+ command="cmd_findPrevious"/>
+ <key id="key_findAgain2"
+ keycode="&findAgainCmd.key2;"
+ command="cmd_findAgain"/>
+ <key id="key_findPrevious2"
+ keycode="&findAgainCmd.key2;"
+ modifiers="shift"
+ command="cmd_findPrevious"/>
+ </keyset>
+
+ <!-- Items for the Edit menu -->
+
+ <menuitem id="se-menu-undo"
+ label="&undoCmd.label;"
+ key="key_undo"
+ accesskey="&undoCmd.accesskey;"
+ command="se-cmd-undo"/>
+ <menuitem id="se-menu-redo"
+ label="&redoCmd.label;"
+ key="key_redo"
+ accesskey="&redoCmd.accesskey;"
+ command="se-cmd-redo"/>
+ <menuitem id="se-menu-cut"
+ label="&cutCmd.label;"
+ key="key_cut"
+ accesskey="&cutCmd.accesskey;"
+ command="se-cmd-cut"/>
+ <menuitem id="se-menu-copy"
+ label="&copyCmd.label;"
+ key="key_copy"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"/>
+ <menuitem id="se-menu-paste"
+ label="&pasteCmd.label;"
+ key="key_paste"
+ accesskey="&pasteCmd.accesskey;"
+ command="se-cmd-paste"/>
+ <menuitem id="se-menu-delete"
+ label="&deleteCmd.label;"
+ key="key_delete"
+ accesskey="&deleteCmd.accesskey;"
+ command="se-cmd-delete"/>
+ <menuitem id="se-menu-selectAll"
+ label="&selectAllCmd.label;"
+ key="key_selectAll"
+ accesskey="&selectAllCmd.accesskey;"
+ command="se-cmd-selectAll"/>
+ <menuitem id="se-menu-find"
+ label="&findCmd.label;"
+ accesskey="&findCmd.accesskey;"
+ key="key_find"
+ command="cmd_find"/>
+ <menuitem id="se-menu-findAgain"
+ label="&findAgainCmd.label;"
+ accesskey="&findAgainCmd.accesskey;"
+ key="key_findAgain"
+ command="cmd_findAgain"/>
+ <menuitem id="se-menu-gotoLine"
+ label="&gotoLineCmd.label;"
+ accesskey="&gotoLineCmd.accesskey;"
+ key="key_gotoLine"
+ command="cmd_gotoLine"/>
+
+ <!-- Items for context menus -->
+
+ <menuitem id="se-cMenu-undo"
+ label="&undoCmd.label;"
+ key="key_undo"
+ accesskey="&undoCmd.accesskey;"
+ command="se-cmd-undo"/>
+ <menuitem id="se-cMenu-cut"
+ label="&cutCmd.label;"
+ key="key_cut"
+ accesskey="&cutCmd.accesskey;"
+ command="se-cmd-cut"/>
+ <menuitem id="se-cMenu-copy"
+ label="&copyCmd.label;"
+ key="key_copy"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"/>
+ <menuitem id="se-cMenu-paste"
+ label="&pasteCmd.label;"
+ key="key_paste"
+ accesskey="&pasteCmd.accesskey;"
+ command="se-cmd-paste"/>
+ <menuitem id="se-cMenu-delete"
+ label="&deleteCmd.label;"
+ key="key_delete"
+ accesskey="&deleteCmd.accesskey;"
+ command="se-cmd-delete"/>
+ <menuitem id="se-cMenu-selectAll"
+ label="&selectAllCmd.label;"
+ key="key_selectAll"
+ accesskey="&selectAllCmd.accesskey;"
+ command="se-cmd-selectAll"/>
+ <menuitem id="se-cMenu-find"
+ label="&findCmd.label;"
+ accesskey="&findCmd.accesskey;"
+ key="key_find"
+ command="cmd_find"/>
+ <menuitem id="se-cMenu-findAgain"
+ label="&findAgainCmd.label;"
+ accesskey="&findAgainCmd.accesskey;"
+ key="key_findAgain"
+ command="cmd_findAgain"/>
+ <menuitem id="se-cMenu-gotoLine"
+ label="&gotoLineCmd.label;"
+ accesskey="&gotoLineCmd.accesskey;"
+ key="key_gotoLine"
+ command="cmd_gotoLine"/>
+</overlay>
diff --git a/browser/devtools/sourceeditor/source-editor-ui.jsm b/browser/devtools/sourceeditor/source-editor-ui.jsm
new file mode 100644
index 000000000..8b74d1623
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor-ui.jsm
@@ -0,0 +1,332 @@
+/* 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 Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["SourceEditorUI"];
+
+/**
+ * The Source Editor component user interface.
+ */
+this.SourceEditorUI = function SourceEditorUI(aEditor)
+{
+ this.editor = aEditor;
+ this._onDirtyChanged = this._onDirtyChanged.bind(this);
+}
+
+SourceEditorUI.prototype = {
+ /**
+ * Initialize the user interface. This is called by the SourceEditor.init()
+ * method.
+ */
+ init: function SEU_init()
+ {
+ this._ownerWindow = this.editor.parentElement.ownerDocument.defaultView;
+ },
+
+ /**
+ * The UI onReady function is executed once the Source Editor completes
+ * initialization and it is ready for usage. Currently this code sets up the
+ * nsIController.
+ */
+ onReady: function SEU_onReady()
+ {
+ if (this._ownerWindow.controllers) {
+ this._controller = new SourceEditorController(this.editor);
+ this._ownerWindow.controllers.insertControllerAt(0, this._controller);
+ this.editor.addEventListener(this.editor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ }
+ },
+
+ /**
+ * The "go to line" command UI. This displays a prompt that allows the user to
+ * input the line number to jump to.
+ */
+ gotoLine: function SEU_gotoLine()
+ {
+ let oldLine = this.editor.getCaretPosition ?
+ this.editor.getCaretPosition().line : null;
+ let newLine = {value: oldLine !== null ? oldLine + 1 : ""};
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptMessage"),
+ newLine, null, {});
+
+ newLine.value = parseInt(newLine.value);
+ if (result && !isNaN(newLine.value) && --newLine.value != oldLine) {
+ if (this.editor.getLineCount) {
+ let lines = this.editor.getLineCount() - 1;
+ this.editor.setCaretPosition(Math.max(0, Math.min(lines, newLine.value)));
+ } else {
+ this.editor.setCaretPosition(Math.max(0, newLine.value));
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * The "find" command UI. This displays a prompt that allows the user to input
+ * the string to search for in the code. By default the current selection is
+ * used as a search string, or the last search string.
+ */
+ find: function SEU_find()
+ {
+ let str = {value: this.editor.getSelectedText()};
+ if (!str.value && this.editor.lastFind) {
+ str.value = this.editor.lastFind.str;
+ }
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptMessage"),
+ str, null, {});
+
+ if (result && str.value) {
+ let start = this.editor.getSelection().end;
+ let pos = this.editor.find(str.value, {ignoreCase: true, start: start});
+ if (pos == -1) {
+ this.editor.find(str.value, {ignoreCase: true});
+ }
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the next occurrence of the last search string.
+ */
+ findNext: function SEU_findNext()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findNext(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the previous occurrence of the last search string.
+ */
+ findPrevious: function SEU_findPrevious()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findPrevious(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * This executed after each find/findNext/findPrevious operation.
+ * @private
+ */
+ _onFind: function SEU__onFind()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind && lastFind.index > -1) {
+ this.editor.setSelection(lastFind.index, lastFind.index + lastFind.str.length);
+ }
+
+ if (this._ownerWindow.goUpdateCommand) {
+ this._ownerWindow.goUpdateCommand("cmd_findAgain");
+ this._ownerWindow.goUpdateCommand("cmd_findPrevious");
+ }
+ },
+
+ /**
+ * This is executed after each undo/redo operation.
+ * @private
+ */
+ _onUndoRedo: function SEU__onUndoRedo()
+ {
+ if (this._ownerWindow.goUpdateCommand) {
+ this._ownerWindow.goUpdateCommand("se-cmd-undo");
+ this._ownerWindow.goUpdateCommand("se-cmd-redo");
+ }
+ },
+
+ /**
+ * The DirtyChanged event handler for the editor. This tracks the editor state
+ * changes to make sure the Source Editor overlay Undo/Redo commands are kept
+ * up to date.
+ * @private
+ */
+ _onDirtyChanged: function SEU__onDirtyChanged()
+ {
+ this._onUndoRedo();
+ },
+
+ /**
+ * Destroy the SourceEditorUI instance. This is called by the
+ * SourceEditor.destroy() method.
+ */
+ destroy: function SEU_destroy()
+ {
+ if (this._ownerWindow.controllers) {
+ this.editor.removeEventListener(this.editor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ }
+
+ this._ownerWindow = null;
+ this.editor = null;
+ this._controller = null;
+ },
+};
+
+/**
+ * The Source Editor nsIController implements features that need to be available
+ * from XUL commands.
+ *
+ * @constructor
+ * @param object aEditor
+ * SourceEditor object instance for which the controller is instanced.
+ */
+function SourceEditorController(aEditor)
+{
+ this._editor = aEditor;
+}
+
+SourceEditorController.prototype = {
+ /**
+ * Check if a command is supported by the controller.
+ *
+ * @param string aCommand
+ * The command name you want to check support for.
+ * @return boolean
+ * True if the command is supported, false otherwise.
+ */
+ supportsCommand: function SEC_supportsCommand(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ case "cmd_gotoLine":
+ case "se-cmd-undo":
+ case "se-cmd-redo":
+ case "se-cmd-cut":
+ case "se-cmd-paste":
+ case "se-cmd-delete":
+ case "se-cmd-selectAll":
+ result = true;
+ break;
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Check if a command is enabled or not.
+ *
+ * @param string aCommand
+ * The command name you want to check if it is enabled or not.
+ * @return boolean
+ * True if the command is enabled, false otherwise.
+ */
+ isCommandEnabled: function SEC_isCommandEnabled(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_gotoLine":
+ case "se-cmd-selectAll":
+ result = true;
+ break;
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ result = this._editor.lastFind && this._editor.lastFind.lastFound != -1;
+ break;
+ case "se-cmd-undo":
+ result = this._editor.canUndo();
+ break;
+ case "se-cmd-redo":
+ result = this._editor.canRedo();
+ break;
+ case "se-cmd-cut":
+ case "se-cmd-delete": {
+ let selection = this._editor.getSelection();
+ result = selection.start != selection.end && !this._editor.readOnly;
+ break;
+ }
+ case "se-cmd-paste": {
+ let window = this._editor._view._frameWindow;
+ let controller = window.controllers.getControllerForCommand("cmd_paste");
+ result = !this._editor.readOnly &&
+ controller.isCommandEnabled("cmd_paste");
+ break;
+ }
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Perform a command.
+ *
+ * @param string aCommand
+ * The command name you want to execute.
+ * @return void
+ */
+ doCommand: function SEC_doCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_find":
+ this._editor.ui.find();
+ break;
+ case "cmd_findAgain":
+ this._editor.ui.findNext();
+ break;
+ case "cmd_findPrevious":
+ this._editor.ui.findPrevious();
+ break;
+ case "cmd_gotoLine":
+ this._editor.ui.gotoLine();
+ break;
+ case "se-cmd-selectAll":
+ this._editor._view.invokeAction("selectAll");
+ break;
+ case "se-cmd-undo":
+ this._editor.undo();
+ break;
+ case "se-cmd-redo":
+ this._editor.redo();
+ break;
+ case "se-cmd-cut":
+ this._editor.ui._ownerWindow.goDoCommand("cmd_cut");
+ break;
+ case "se-cmd-paste":
+ this._editor.ui._ownerWindow.goDoCommand("cmd_paste");
+ break;
+ case "se-cmd-delete": {
+ let selection = this._editor.getSelection();
+ this._editor.setText("", selection.start, selection.end);
+ break;
+ }
+ }
+ },
+
+ onEvent: function() { }
+};
diff --git a/browser/devtools/sourceeditor/source-editor.jsm b/browser/devtools/sourceeditor/source-editor.jsm
new file mode 100644
index 000000000..04622183f
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor.jsm
@@ -0,0 +1,455 @@
+/* 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 Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/source-editor-ui.jsm");
+
+const PREF_EDITOR_COMPONENT = "devtools.editor.component";
+const SOURCEEDITOR_L10N = "chrome://browser/locale/devtools/sourceeditor.properties";
+
+var component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
+var obj = {};
+try {
+ if (component == "ui") {
+ throw new Error("The ui editor component is not available.");
+ }
+ Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
+} catch (ex) {
+ Cu.reportError(ex);
+ Cu.reportError("SourceEditor component failed to load: " + component);
+
+ // If the component does not exist, clear the user pref back to the default.
+ Services.prefs.clearUserPref(PREF_EDITOR_COMPONENT);
+
+ // Load the default editor component.
+ component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
+ Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
+}
+
+// Export the SourceEditor.
+this.SourceEditor = obj.SourceEditor;
+this.EXPORTED_SYMBOLS = ["SourceEditor"];
+
+// Add the constants used by all SourceEditors.
+
+XPCOMUtils.defineLazyGetter(SourceEditorUI, "strings", function() {
+ return Services.strings.createBundle(SOURCEEDITOR_L10N);
+});
+
+/**
+ * Known SourceEditor preferences.
+ */
+SourceEditor.PREFS = {
+ TAB_SIZE: "devtools.editor.tabsize",
+ EXPAND_TAB: "devtools.editor.expandtab",
+ COMPONENT: PREF_EDITOR_COMPONENT,
+};
+
+/**
+ * Predefined source editor modes for JavaScript, CSS and other languages.
+ */
+SourceEditor.MODES = {
+ JAVASCRIPT: "js",
+ CSS: "css",
+ TEXT: "text",
+ HTML: "html",
+ XML: "xml",
+};
+
+/**
+ * Predefined themes for syntax highlighting.
+ */
+SourceEditor.THEMES = {
+ MOZILLA: "mozilla",
+};
+
+/**
+ * Source editor configuration defaults.
+ * @see SourceEditor.init
+ */
+SourceEditor.DEFAULTS = {
+ /**
+ * The text you want shown when the editor opens up.
+ * @type string
+ */
+ initialText: "",
+
+ /**
+ * The editor mode, based on the file type you want to edit. You can use one of
+ * the predefined modes.
+ *
+ * @see SourceEditor.MODES
+ * @type string
+ */
+ mode: SourceEditor.MODES.TEXT,
+
+ /**
+ * The syntax highlighting theme you want. You can use one of the predefined
+ * themes, or you can point to your CSS file.
+ *
+ * @see SourceEditor.THEMES.
+ * @type string
+ */
+ theme: SourceEditor.THEMES.MOZILLA,
+
+ /**
+ * How many steps should the undo stack hold.
+ * @type number
+ */
+ undoLimit: 200,
+
+ /**
+ * Define how many spaces to use for a tab character. This value is overridden
+ * by a user preference, see SourceEditor.PREFS.TAB_SIZE.
+ *
+ * @type number
+ */
+ tabSize: 4,
+
+ /**
+ * Tells if you want tab characters to be expanded to spaces. This value is
+ * overridden by a user preference, see SourceEditor.PREFS.EXPAND_TAB.
+ * @type boolean
+ */
+ expandTab: true,
+
+ /**
+ * Tells if you want the editor to be read only or not.
+ * @type boolean
+ */
+ readOnly: false,
+
+ /**
+ * Display the line numbers gutter.
+ * @type boolean
+ */
+ showLineNumbers: false,
+
+ /**
+ * Display the annotations gutter/ruler. This gutter currently supports
+ * annotations of breakpoint type.
+ * @type boolean
+ */
+ showAnnotationRuler: false,
+
+ /**
+ * Display the overview gutter/ruler. This gutter presents an overview of the
+ * current annotations in the editor, for example the breakpoints.
+ * @type boolean
+ */
+ showOverviewRuler: false,
+
+ /**
+ * Highlight the current line.
+ * @type boolean
+ */
+ highlightCurrentLine: true,
+
+ /**
+ * An array of objects that allows you to define custom editor keyboard
+ * bindings. Each object can have:
+ * - action - name of the editor action to invoke.
+ * - code - keyCode for the shortcut.
+ * - accel - boolean for the Accel key (Cmd on Macs, Ctrl on Linux/Windows).
+ * - ctrl - boolean for the Control key
+ * - shift - boolean for the Shift key.
+ * - alt - boolean for the Alt key.
+ * - callback - optional function to invoke, if the action is not predefined
+ * in the editor.
+ * @type array
+ */
+ keys: null,
+
+ /**
+ * The editor context menu you want to display when the user right-clicks
+ * within the editor. This property can be:
+ * - a string that tells the ID of the xul:menupopup you want. This needs to
+ * be available within the editor parentElement.ownerDocument.
+ * - an nsIDOMElement object reference pointing to the xul:menupopup you
+ * want to open when the contextmenu event is fired.
+ *
+ * Set this property to a falsey value to disable the default context menu.
+ *
+ * @see SourceEditor.EVENTS.CONTEXT_MENU for more control over the contextmenu
+ * event.
+ * @type string|nsIDOMElement
+ */
+ contextMenu: "sourceEditorContextMenu",
+};
+
+/**
+ * Known editor events you can listen for.
+ */
+SourceEditor.EVENTS = {
+ /**
+ * The contextmenu event is fired when the editor context menu is invoked. The
+ * event object properties:
+ * - x - the pointer location on the x axis, relative to the document the
+ * user is editing.
+ * - y - the pointer location on the y axis, relative to the document the
+ * user is editing.
+ * - screenX - the pointer location on the x axis, relative to the screen.
+ * This value comes from the DOM contextmenu event.screenX property.
+ * - screenY - the pointer location on the y axis, relative to the screen.
+ * This value comes from the DOM contextmenu event.screenY property.
+ *
+ * @see SourceEditor.DEFAULTS.contextMenu
+ */
+ CONTEXT_MENU: "ContextMenu",
+
+ /**
+ * The TextChanged event is fired when the editor content changes. The event
+ * object properties:
+ * - start - the character offset in the document where the change has
+ * occured.
+ * - removedCharCount - the number of characters removed from the document.
+ * - addedCharCount - the number of characters added to the document.
+ */
+ TEXT_CHANGED: "TextChanged",
+
+ /**
+ * The Selection event is fired when the editor selection changes. The event
+ * object properties:
+ * - oldValue - the old selection range.
+ * - newValue - the new selection range.
+ * Both ranges are objects which hold two properties: start and end.
+ */
+ SELECTION: "Selection",
+
+ /**
+ * The focus event is fired when the editor is focused.
+ */
+ FOCUS: "Focus",
+
+ /**
+ * The blur event is fired when the editor goes out of focus.
+ */
+ BLUR: "Blur",
+
+ /**
+ * The MouseMove event is sent when the user moves the mouse over a line.
+ * The event object properties:
+ * - event - the DOM mousemove event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_MOVE: "MouseMove",
+
+ /**
+ * The MouseOver event is sent when the mouse pointer enters a line.
+ * The event object properties:
+ * - event - the DOM mouseover event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_OVER: "MouseOver",
+
+ /**
+ * This MouseOut event is sent when the mouse pointer exits a line.
+ * The event object properties:
+ * - event - the DOM mouseout event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_OUT: "MouseOut",
+
+ /**
+ * The BreakpointChange event is fired when a new breakpoint is added or when
+ * a breakpoint is removed - either through API use or through the editor UI.
+ * Event object properties:
+ * - added - array that holds the new breakpoints.
+ * - removed - array that holds the breakpoints that have been removed.
+ * Each object in the added/removed arrays holds two properties: line and
+ * condition.
+ */
+ BREAKPOINT_CHANGE: "BreakpointChange",
+
+ /**
+ * The DirtyChanged event is fired when the dirty state of the editor is
+ * changed. The dirty state of the editor tells if the are text changes that
+ * have not been saved yet. Event object properties: oldValue and newValue.
+ * Both are booleans telling the old dirty state and the new state,
+ * respectively.
+ */
+ DIRTY_CHANGED: "DirtyChanged",
+};
+
+/**
+ * Allowed vertical alignment options for the line index
+ * when you call SourceEditor.setCaretPosition().
+ */
+SourceEditor.VERTICAL_ALIGN = {
+ TOP: 0,
+ CENTER: 1,
+ BOTTOM: 2,
+};
+
+/**
+ * Extend a destination object with properties from a source object.
+ *
+ * @param object aDestination
+ * @param object aSource
+ */
+function extend(aDestination, aSource)
+{
+ for (let name in aSource) {
+ if (!aDestination.hasOwnProperty(name)) {
+ aDestination[name] = aSource[name];
+ }
+ }
+}
+
+/**
+ * Add methods common to all components.
+ */
+extend(SourceEditor.prototype, {
+ // Expose the static constants on the SourceEditor instances.
+ EVENTS: SourceEditor.EVENTS,
+ MODES: SourceEditor.MODES,
+ THEMES: SourceEditor.THEMES,
+ DEFAULTS: SourceEditor.DEFAULTS,
+ VERTICAL_ALIGN: SourceEditor.VERTICAL_ALIGN,
+
+ _lastFind: null,
+
+ /**
+ * Find a string in the editor.
+ *
+ * @param string aString
+ * The string you want to search for. If |aString| is not given the
+ * currently selected text is used.
+ * @param object [aOptions]
+ * Optional find options:
+ * - start: (integer) offset to start searching from. Default: 0 if
+ * backwards is false. If backwards is true then start = text.length.
+ * - ignoreCase: (boolean) tells if you want the search to be case
+ * insensitive or not. Default: false.
+ * - backwards: (boolean) tells if you want the search to go backwards
+ * from the given |start| offset. Default: false.
+ * @return integer
+ * The offset where the string was found.
+ */
+ find: function SE_find(aString, aOptions)
+ {
+ if (typeof(aString) != "string") {
+ return -1;
+ }
+
+ aOptions = aOptions || {};
+
+ let str = aOptions.ignoreCase ? aString.toLowerCase() : aString;
+
+ let text = this.getText();
+ if (aOptions.ignoreCase) {
+ text = text.toLowerCase();
+ }
+
+ let index = aOptions.backwards ?
+ text.lastIndexOf(str, aOptions.start) :
+ text.indexOf(str, aOptions.start);
+
+ let lastFoundIndex = index;
+ if (index == -1 && this.lastFind && this.lastFind.index > -1 &&
+ this.lastFind.str === aString &&
+ this.lastFind.ignoreCase === !!aOptions.ignoreCase) {
+ lastFoundIndex = this.lastFind.index;
+ }
+
+ this._lastFind = {
+ str: aString,
+ index: index,
+ lastFound: lastFoundIndex,
+ ignoreCase: !!aOptions.ignoreCase,
+ };
+
+ return index;
+ },
+
+ /**
+ * Find the next occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the beginning of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findNext: function SE_findNext(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound + this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ };
+
+ let index = this.find(this.lastFind.str, options);
+ if (index == -1 && aWrap) {
+ options.start = 0;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+
+ /**
+ * Find the previous occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the end of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findPrevious: function SE_findPrevious(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound - this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ backwards: true,
+ };
+
+ let index;
+ if (options.start > 0) {
+ index = this.find(this.lastFind.str, options);
+ } else {
+ index = this._lastFind.index = -1;
+ }
+
+ if (index == -1 && aWrap) {
+ options.start = this.getCharCount() - 1;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+});
+
+/**
+ * Retrieve the last find operation result. This object holds the following
+ * properties:
+ * - str: the last search string.
+ * - index: stores the result of the most recent find operation. This is the
+ * index in the text where |str| was found or -1 otherwise.
+ * - lastFound: tracks the index where |str| was last found, throughout
+ * multiple find operations. This can be -1 if |str| was never found in the
+ * document.
+ * - ignoreCase: tells if the search was case insensitive or not.
+ * @type object
+ */
+Object.defineProperty(SourceEditor.prototype, "lastFind", {
+ get: function() { return this._lastFind; },
+ enumerable: true,
+ configurable: true,
+});
+
diff --git a/browser/devtools/sourceeditor/test/Makefile.in b/browser/devtools/sourceeditor/test/Makefile.in
new file mode 100644
index 000000000..018370a80
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_sourceeditor_initialization.js \
+ browser_bug684862_paste_html.js \
+ browser_bug687573_vscroll.js \
+ browser_bug687568_pagescroll.js \
+ browser_bug687580_drag_and_drop.js \
+ browser_bug684546_reset_undo.js \
+ browser_bug695035_middle_click_paste.js \
+ browser_bug687160_line_api.js \
+ browser_bug650345_find.js \
+ browser_bug703692_focus_blur.js \
+ browser_bug725388_mouse_events.js \
+ browser_bug707987_debugger_breakpoints.js \
+ browser_bug712982_line_ruler_click.js \
+ browser_bug725618_moveLines_shortcut.js \
+ browser_bug700893_dirty_state.js \
+ browser_bug729480_line_vertical_align.js \
+ browser_bug725430_comment_uncomment.js \
+ browser_bug731721_debugger_stepping.js \
+ browser_bug729960_block_bracket_jump.js \
+ browser_bug744021_next_prev_bracket_jump.js \
+ browser_bug725392_mouse_coords_char_offset.js \
+ head.js \
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/sourceeditor/test/browser_bug650345_find.js b/browser/devtools/sourceeditor/test/browser_bug650345_find.js
new file mode 100644
index 000000000..c4fa74d81
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug650345_find.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 650345' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+
+ let needle = "foobar";
+ is(editor.find(), -1, "find() works");
+ ok(!editor.lastFind, "no editor.lastFind yet");
+
+ is(editor.find(needle), 0, "find('" + needle + "') works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ let newIndex = text.indexOf(needle, needle.length);
+ is(editor.findNext(), newIndex, "findNext() works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ is(editor.findNext(), -1, "findNext() works again");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(), 0, "findPrevious() works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(), -1, "findPrevious() works again");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findNext(), newIndex, "findNext() works");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), 0, "findNext(true) works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), newIndex, "findNext(true) works again");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(true), 0, "findPrevious(true) works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(true), newIndex, "findPrevious(true) works again");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ needle = "error";
+ is(editor.find(needle), -1, "find('" + needle + "') works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct");
+ is(editor.findNext(true), -1, "findNext(true) works");
+
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), -1, "findPrevious(true) works");
+
+ needle = "bug650345";
+ newIndex = text.indexOf(needle);
+
+ is(editor.find(needle), newIndex, "find('" + needle + "') works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.findNext(true), newIndex, "findNext(true) works");
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), newIndex, "findPrevious(true) works");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.find(needle, {ignoreCase: 1}), newIndex,
+ "find('" + needle + "', {ignoreCase: 1}) works");
+ is(editor.lastFind.ignoreCase, true, "lastFind.ignoreCase is correct");
+
+ let newIndex2 = text.toLowerCase().indexOf(needle, newIndex + needle.length);
+ is(editor.findNext(), newIndex2, "findNext() works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex2, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), newIndex, "findNext(true) works");
+
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), newIndex2, "findPrevious(true) works");
+ is(editor.findPrevious(), newIndex, "findPrevious() works again");
+
+ needle = "foobar";
+ newIndex = text.indexOf(needle, 2);
+ is(editor.find(needle, {start: 2}), newIndex,
+ "find('" + needle + "', {start:2}) works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.findNext(true), 0, "findNext(true) works");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js b/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js
new file mode 100644
index 000000000..4fbb01bde
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 684546 - reset undo' width='300' height='500'>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ editor = new SourceEditor();
+ editor.init(box, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.setText("First");
+ editor.setText("Second", 5);
+ is(editor.getText(), "FirstSecond", "text set correctly.");
+ editor.undo();
+ is(editor.getText(), "First", "undo works.");
+ editor.redo();
+ is(editor.getText(), "FirstSecond", "redo works.");
+ editor.resetUndo();
+ ok(!editor.canUndo(), "canUndo() is correct");
+ ok(!editor.canRedo(), "canRedo() is correct");
+ editor.undo();
+ is(editor.getText(), "FirstSecond", "reset undo works correctly");
+ editor.setText("Third", 11);
+ is(editor.getText(), "FirstSecondThird", "text set correctly");
+ editor.undo();
+ is(editor.getText(), "FirstSecond", "undo works after reset");
+ editor.redo();
+ is(editor.getText(), "FirstSecondThird", "redo works after reset");
+ editor.resetUndo();
+ ok(!editor.canUndo(), "canUndo() is correct (again)");
+ ok(!editor.canRedo(), "canRedo() is correct (again)");
+ editor.undo();
+ is(editor.getText(), "FirstSecondThird", "reset undo still works correctly");
+
+ finish();
+}
+
+registerCleanupFunction(function() {
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+});
diff --git a/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js b/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js
new file mode 100644
index 000000000..44a8854c5
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.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/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const pageUrl = "data:text/html,<ul><li>test<li>foobarBug684862";
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ waitForFocus(pageLoaded, content);
+ }, true);
+
+ content.location = pageUrl;
+}
+
+function pageLoaded()
+{
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 684862 - paste HTML' width='600' height='500'>" +
+ "<script type='application/javascript' src='chrome://global/content/globalOverlay.js'/>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let doCopy = function() {
+ gBrowser.selectedBrowser.focus();
+ EventUtils.synthesizeKey("a", {accelKey: true}, content);
+ EventUtils.synthesizeKey("c", {accelKey: true}, content);
+ };
+
+ let clipboardValidator = function(aData) aData.indexOf("foobarBug684862") > -1;
+
+ let onCopy = function() {
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+ };
+
+ waitForClipboard(clipboardValidator, doCopy, onCopy, testEnd);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ editor = new SourceEditor();
+ editor.init(box, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ ok(!editor.getText(), "editor has no content");
+ is(editor.getCaretOffset(), 0, "caret location");
+
+ let onPaste = function() {
+ editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+
+ let text = editor.getText();
+ ok(text, "editor has content after paste");
+
+ let pos = text.indexOf("foobarBug684862");
+ isnot(pos, -1, "editor content is correct");
+ // Test for bug 699541 - Pasted HTML shows twice in Orion.
+ is(text.lastIndexOf("foobarBug684862"), pos, "editor content is correct (no duplicate)");
+
+ executeSoon(function() {
+ editor.setCaretOffset(4);
+ EventUtils.synthesizeKey("a", {}, testWin);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, testWin);
+
+ text = editor.getText();
+
+ is(text.indexOf("foobarBug684862"), pos + 1,
+ "editor content is correct after navigation");
+ is(editor.getCaretOffset(), 6, "caret location");
+
+ executeSoon(testEnd);
+ });
+ };
+
+ editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+
+ // Do paste
+ executeSoon(function() {
+ testWin.goDoCommand("cmd_paste");
+ });
+}
+
+function testEnd()
+{
+ if (editor) {
+ editor.destroy();
+ }
+ if (testWin) {
+ testWin.close();
+ }
+ testWin = editor = null;
+ gBrowser.removeCurrentTab();
+
+ waitForFocus(finish, window);
+}
+
diff --git a/browser/devtools/sourceeditor/test/browser_bug687160_line_api.js b/browser/devtools/sourceeditor/test/browser_bug687160_line_api.js
new file mode 100644
index 000000000..6eb3c22fe
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug687160_line_api.js
@@ -0,0 +1,90 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 660784' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+
+ editor.focus();
+
+ editor.setText("line1\nline2\nline3");
+
+ if (component != "textarea") {
+ is(editor.getLineCount(), 3, "getLineCount() works");
+ }
+
+ editor.setCaretPosition(1);
+ is(editor.getCaretOffset(), 6, "setCaretPosition(line) works");
+
+ let pos;
+ if (component != "textarea") {
+ pos = editor.getCaretPosition();
+ ok(pos.line == 1 && pos.col == 0, "getCaretPosition() works");
+ }
+
+ editor.setCaretPosition(1, 3);
+ is(editor.getCaretOffset(), 9, "setCaretPosition(line, column) works");
+
+ if (component != "textarea") {
+ pos = editor.getCaretPosition();
+ ok(pos.line == 1 && pos.col == 3, "getCaretPosition() works");
+ }
+
+ editor.setCaretPosition(2);
+ is(editor.getCaretOffset(), 12, "setCaretLine() works, confirmed");
+
+ if (component != "textarea") {
+ pos = editor.getCaretPosition();
+ ok(pos.line == 2 && pos.col == 0, "setCaretPosition(line) works, again");
+ }
+
+ let offsetLine = editor.getLineAtOffset(0);
+ is(offsetLine, 0, "getLineAtOffset() is correct for offset 0");
+
+ let offsetLine = editor.getLineAtOffset(6);
+ is(offsetLine, 1, "getLineAtOffset() is correct for offset 6");
+
+ let offsetLine = editor.getLineAtOffset(12);
+ is(offsetLine, 2, "getLineAtOffset() is correct for offset 12");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
+
diff --git a/browser/devtools/sourceeditor/test/browser_bug687568_pagescroll.js b/browser/devtools/sourceeditor/test/browser_bug687568_pagescroll.js
new file mode 100644
index 000000000..39ed4fb0c
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug687568_pagescroll.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component != "orion") {
+ ok(true, "skip test for bug 687568: only applicable for Orion");
+ return; // Testing for the fix requires direct Orion API access.
+ }
+
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 687568 - page scroll' width='600' height='500'>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ editor = new SourceEditor();
+ editor.init(box, { showLineNumbers: true }, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ let view = editor._view;
+ let model = editor._model;
+
+ let lineHeight = view.getLineHeight();
+ let editorHeight = view.getClientArea().height;
+ let linesPerPage = Math.floor(editorHeight / lineHeight);
+ let totalLines = 3 * linesPerPage;
+
+ let text = "";
+ for (let i = 0; i < totalLines; i++) {
+ text += "l" + i + "\n";
+ }
+
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+
+ let bottomLine = view.getBottomIndex(true);
+ view.setTopIndex(bottomLine + 1);
+
+ executeSoon(function() {
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {shiftKey: true}, testWin);
+
+ executeSoon(function() {
+ let topLine = view.getTopIndex(true);
+ let topLineOffset = model.getLineStart(topLine);
+ let selection = editor.getSelection();
+ ok(selection.start < topLineOffset && topLineOffset < selection.end,
+ "top visible line is selected");
+
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ });
+ });
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug687573_vscroll.js b/browser/devtools/sourceeditor/test/browser_bug687573_vscroll.js
new file mode 100644
index 000000000..06f790033
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug687573_vscroll.js
@@ -0,0 +1,133 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component == "textarea") {
+ ok(true, "skip test for bug 687573: not applicable for TEXTAREAs");
+ return; // TEXTAREAs have different behavior
+ }
+
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 687573 - vertical scroll' width='300' height='500'>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ let text = "abba\n" +
+ "\n" +
+ "abbaabbaabbaabbaabbaabbaabbaabbaabbaabba\n" +
+ "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba\n" +
+ "abbaabbaabbaabbaabbaabbaabbaabbaabbaabba\n" +
+ "\n" +
+ "abba\n";
+
+ let config = {
+ showLineNumbers: true,
+ initialText: text,
+ };
+
+ editor = new SourceEditor();
+ editor.init(box, config, editorLoaded);
+}
+
+function editorLoaded()
+{
+ let VK_LINE_END = "VK_END";
+ let VK_LINE_END_OPT = {};
+ let OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ if (OS == "Darwin") {
+ VK_LINE_END = "VK_RIGHT";
+ VK_LINE_END_OPT = {accelKey: true};
+ }
+
+ editor.focus();
+
+ editor.setCaretOffset(0);
+ is(editor.getCaretOffset(), 0, "caret location at start");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+
+ // line 3
+ is(editor.getCaretOffset(), 6, "caret location, keypress Down two times, line 3");
+
+ // line 3 end
+ EventUtils.synthesizeKey(VK_LINE_END, VK_LINE_END_OPT, testWin);
+ is(editor.getCaretOffset(), 46, "caret location, keypress End, line 3 end");
+
+ // line 4
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ is(editor.getCaretOffset(), 87, "caret location, keypress Down, line 4");
+
+ // line 4 end
+ EventUtils.synthesizeKey(VK_LINE_END, VK_LINE_END_OPT, testWin);
+ is(editor.getCaretOffset(), 135, "caret location, keypress End, line 4 end");
+
+ // line 5 end
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ is(editor.getCaretOffset(), 176, "caret location, keypress Down, line 5 end");
+
+ // line 6 end
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ is(editor.getCaretOffset(), 177, "caret location, keypress Down, line 6 end");
+
+ // The executeSoon() calls are needed to allow reflows...
+ EventUtils.synthesizeKey("VK_UP", {}, testWin);
+ executeSoon(function() {
+ // line 5 end
+ is(editor.getCaretOffset(), 176, "caret location, keypress Up, line 5 end");
+
+ EventUtils.synthesizeKey("VK_UP", {}, testWin);
+ executeSoon(function() {
+ // line 4 end
+ is(editor.getCaretOffset(), 135, "caret location, keypress Up, line 4 end");
+
+ // line 3 end
+ EventUtils.synthesizeKey("VK_UP", {}, testWin);
+ is(editor.getCaretOffset(), 46, "caret location, keypress Up, line 3 end");
+
+ // line 2 end
+ EventUtils.synthesizeKey("VK_UP", {}, testWin);
+ is(editor.getCaretOffset(), 5, "caret location, keypress Up, line 2 end");
+
+ // line 3 end
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ is(editor.getCaretOffset(), 46, "caret location, keypress Down, line 3 end");
+
+ // line 4 end
+ EventUtils.synthesizeKey("VK_DOWN", {}, testWin);
+ is(editor.getCaretOffset(), 135, "caret location, keypress Down, line 4 end");
+
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ });
+ });
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug687580_drag_and_drop.js b/browser/devtools/sourceeditor/test/browser_bug687580_drag_and_drop.js
new file mode 100644
index 000000000..6b493969b
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug687580_drag_and_drop.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component != "orion") {
+ ok(true, "skip test for bug 687580: only applicable for Orion");
+ return; // Testing for the fix requires direct Orion API access.
+ }
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 687580 - drag and drop' width='600' height='500'>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ editor = new SourceEditor();
+ editor.init(box, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ let view = editor._view;
+ let model = editor._model;
+
+ let lineHeight = view.getLineHeight();
+ let editorHeight = view.getClientArea().height;
+ let linesPerPage = Math.floor(editorHeight / lineHeight);
+ let totalLines = 2 * linesPerPage;
+
+ let text = "foobarBug687580-";
+ for (let i = 0; i < totalLines; i++) {
+ text += "l" + i + "\n";
+ }
+
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ let bottomPixel = view.getBottomPixel();
+
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+ EventUtils.synthesizeKey("VK_DOWN", {shiftKey: true}, testWin);
+
+ let initialSelection = editor.getSelection();
+
+ let ds = Cc["@mozilla.org/widget/dragservice;1"].
+ getService(Ci.nsIDragService);
+
+ let target = view._clientDiv;
+ let targetWin = target.ownerDocument.defaultView;
+
+ let dataTransfer = null;
+
+ let onDragStart = function(aEvent) {
+ target.removeEventListener("dragstart", onDragStart, false);
+
+ dataTransfer = aEvent.dataTransfer;
+ ok(dataTransfer, "dragstart event fired");
+ ok(dataTransfer.types.contains("text/plain"),
+ "dataTransfer text/plain available");
+ let text = dataTransfer.getData("text/plain");
+ isnot(text.indexOf("foobarBug687580"), -1, "text/plain data is correct");
+
+ dataTransfer.dropEffect = "move";
+ };
+
+ let onDrop = executeSoon.bind(null, function() {
+ target.removeEventListener("drop", onDrop, false);
+
+ let selection = editor.getSelection();
+ is(selection.end - selection.start,
+ initialSelection.end - initialSelection.start,
+ "selection is correct");
+ is(editor.getText(0, 2), "l3", "drag and drop worked");
+
+ let offset = editor.getCaretOffset();
+ ok(offset > initialSelection.end, "new caret location");
+
+ let initialLength = initialSelection.end - initialSelection.start;
+ let dropText = editor.getText(offset - initialLength, offset);
+ isnot(dropText.indexOf("foobarBug687580"), -1, "drop text is correct");
+
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ });
+
+ executeSoon(function() {
+ ds.startDragSession();
+
+ target.addEventListener("dragstart", onDragStart, false);
+ target.addEventListener("drop", onDrop, false);
+
+ EventUtils.synthesizeMouse(target, 10, 10, {type: "mousedown"}, targetWin);
+
+ EventUtils.synthesizeMouse(target, 11, bottomPixel - 25, {type: "mousemove"},
+ targetWin);
+
+ EventUtils.synthesizeMouse(target, 12, bottomPixel - 15, {type: "mousemove"},
+ targetWin);
+
+ let clientX = 5;
+ let clientY = bottomPixel - 10;
+
+ let event = targetWin.document.createEvent("DragEvents");
+ event.initDragEvent("dragenter", true, true, targetWin, 0, 0, 0, clientX,
+ clientY, false, false, false, false, 0, null,
+ dataTransfer);
+ target.dispatchEvent(event);
+
+ event = targetWin.document.createEvent("DragEvents");
+ event.initDragEvent("dragover", true, true, targetWin, 0, 0, 0, clientX + 1,
+ clientY + 2, false, false, false, false, 0, null,
+ dataTransfer);
+ target.dispatchEvent(event);
+
+ EventUtils.synthesizeMouse(target, clientX + 2, clientY + 1,
+ {type: "mouseup"}, targetWin);
+
+ event = targetWin.document.createEvent("DragEvents");
+ event.initDragEvent("drop", true, true, targetWin, 0, 0, 0, clientX + 2,
+ clientY + 3, false, false, false, false, 0, null,
+ dataTransfer);
+ target.dispatchEvent(event);
+
+ event = targetWin.document.createEvent("DragEvents");
+ event.initDragEvent("dragend", true, true, targetWin, 0, 0, 0, clientX + 3,
+ clientY + 2, false, false, false, false, 0, null,
+ dataTransfer);
+ target.dispatchEvent(event);
+
+ ds.endDragSession(true);
+ });
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug695035_middle_click_paste.js b/browser/devtools/sourceeditor/test/browser_bug695035_middle_click_paste.js
new file mode 100644
index 000000000..40775a11c
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug695035_middle_click_paste.js
@@ -0,0 +1,100 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ if (Services.appinfo.OS != "Linux") {
+ ok(true, "this test only applies to Linux, skipping.")
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 695035' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ let initialText = "initial text!";
+
+ editor.setText(initialText);
+
+ let expectedString = "foobarBug695035-" + Date.now();
+
+ let doCopy = function() {
+ let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboardHelper.copyStringToClipboard(expectedString,
+ Ci.nsIClipboard.kSelectionClipboard,
+ testWin.document);
+ };
+
+ let onCopy = function() {
+ editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+
+ EventUtils.synthesizeMouse(editor.editorElement, 10, 10, {}, testWin);
+ EventUtils.synthesizeMouse(editor.editorElement, 11, 11, {button: 1}, testWin);
+ };
+
+ let onPaste = function() {
+ editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
+
+ let text = editor.getText();
+ isnot(text.indexOf(expectedString), -1, "middle-click paste works");
+ isnot(text, initialText, "middle-click paste works (confirmed)");
+
+ executeSoon(doTestBug695032);
+ };
+
+ let doTestBug695032 = function() {
+ info("test for bug 695032 - editor selection should be placed in the X11 primary selection buffer");
+
+ let text = "foobarBug695032 test me, test me!";
+ editor.setText(text);
+
+ waitForSelection(text, function() {
+ EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
+ }, testEnd, testEnd);
+ };
+
+ waitForSelection(expectedString, doCopy, onCopy, testEnd);
+}
+
+function testEnd()
+{
+ editor.destroy();
+ testWin.close();
+
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug700893_dirty_state.js b/browser/devtools/sourceeditor/test/browser_bug700893_dirty_state.js
new file mode 100644
index 000000000..3b9552659
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug700893_dirty_state.js
@@ -0,0 +1,94 @@
+/* vim: set 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() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component == "textarea") {
+ ok(true, "skip test for bug 700893: only applicable for non-textarea components");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 700893' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {initialText: "foobar"}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+
+ is(editor.dirty, false, "editory is not dirty");
+
+ let event = null;
+ let eventHandler = function(aEvent) {
+ event = aEvent;
+ };
+ editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED, eventHandler);
+
+ editor.setText("omg");
+
+ is(editor.dirty, true, "editor is dirty");
+ ok(event, "DirtyChanged event fired")
+ is(event.oldValue, false, "event.oldValue is correct");
+ is(event.newValue, true, "event.newValue is correct");
+
+ event = null;
+ editor.setText("foo 2");
+ ok(!event, "no DirtyChanged event fired");
+
+ editor.dirty = false;
+
+ is(editor.dirty, false, "editor marked as clean");
+ ok(event, "DirtyChanged event fired")
+ is(event.oldValue, true, "event.oldValue is correct");
+ is(event.newValue, false, "event.newValue is correct");
+
+ event = null;
+ editor.setText("foo 3");
+
+ is(editor.dirty, true, "editor is dirty after changes");
+ ok(event, "DirtyChanged event fired")
+ is(event.oldValue, false, "event.oldValue is correct");
+ is(event.newValue, true, "event.newValue is correct");
+
+ editor.undo();
+ is(editor.dirty, false, "editor is not dirty after undo");
+ ok(event, "DirtyChanged event fired")
+ is(event.oldValue, true, "event.oldValue is correct");
+ is(event.newValue, false, "event.newValue is correct");
+
+ editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED, eventHandler);
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug703692_focus_blur.js b/browser/devtools/sourceeditor/test/browser_bug703692_focus_blur.js
new file mode 100644
index 000000000..16c20f52c
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug703692_focus_blur.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Test for bug 703692' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ let focusHandler = function(aEvent) {
+ editor.removeEventListener(SourceEditor.EVENTS.FOCUS, focusHandler);
+ editor.addEventListener(SourceEditor.EVENTS.BLUR, blurHandler);
+
+ ok(aEvent, "Focus event fired");
+ window.focus();
+ };
+
+ let blurHandler = function(aEvent) {
+ editor.removeEventListener(SourceEditor.EVENTS.BLUR, blurHandler);
+
+ ok(aEvent, "Blur event fired");
+ executeSoon(testEnd);
+ }
+
+ editor.addEventListener(SourceEditor.EVENTS.FOCUS, focusHandler);
+
+ editor.focus();
+}
+
+function testEnd()
+{
+ if (editor) {
+ editor.destroy();
+ }
+ if (testWin) {
+ testWin.close();
+ }
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug707987_debugger_breakpoints.js b/browser/devtools/sourceeditor/test/browser_bug707987_debugger_breakpoints.js
new file mode 100644
index 000000000..f7d839b6c
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug707987_debugger_breakpoints.js
@@ -0,0 +1,169 @@
+/* vim: set 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() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component == "textarea") {
+ ok(true, "skip test for bug 707987: only applicable for non-textarea components");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 707987' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showAnnotationRuler: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+
+ editor.setText("line1\nline2\nline3\nline4");
+
+ is(editor.getBreakpoints().length, 0, "no breakpoints");
+
+ let event = null;
+ let eventHandler = function(aEvent) {
+ event = aEvent;
+ };
+ editor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, eventHandler);
+
+ // Add breakpoint at line 0
+
+ editor.addBreakpoint(0);
+
+ let breakpoints = editor.getBreakpoints();
+ is(breakpoints.length, 1, "one breakpoint added");
+ is(breakpoints[0].line, 0, "breakpoint[0].line is correct");
+ ok(!breakpoints[0].condition, "breakpoint[0].condition is correct");
+
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 1, "one breakpoint added (confirmed)");
+ is(event.removed.length, 0, "no breakpoint removed");
+ is(event.added[0].line, 0, "event added[0].line is correct");
+ ok(!event.added[0].condition, "event added[0].condition is correct");
+
+ // Add breakpoint at line 3
+
+ event = null;
+ editor.addBreakpoint(3, "foo == 'bar'");
+
+ breakpoints = editor.getBreakpoints();
+ is(breakpoints.length, 2, "another breakpoint added");
+ is(breakpoints[0].line, 0, "breakpoint[0].line is correct");
+ ok(!breakpoints[0].condition, "breakpoint[0].condition is correct");
+ is(breakpoints[1].line, 3, "breakpoint[1].line is correct");
+ is(breakpoints[1].condition, "foo == 'bar'",
+ "breakpoint[1].condition is correct");
+
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 1, "another breakpoint added (confirmed)");
+ is(event.removed.length, 0, "no breakpoint removed");
+ is(event.added[0].line, 3, "event added[0].line is correct");
+ is(event.added[0].condition, "foo == 'bar'",
+ "event added[0].condition is correct");
+
+ // Try to add another breakpoint at line 0
+
+ event = null;
+ editor.addBreakpoint(0);
+
+ is(editor.getBreakpoints().length, 2, "no breakpoint added");
+ is(event, null, "no breakpoint event fired");
+
+ // Try to remove a breakpoint from line 1
+
+ is(editor.removeBreakpoint(1), false, "removeBreakpoint(1) returns false");
+ is(editor.getBreakpoints().length, 2, "no breakpoint removed");
+ is(event, null, "no breakpoint event fired");
+
+ // Remove the breakpoint from line 0
+
+ is(editor.removeBreakpoint(0), true, "removeBreakpoint(0) returns true");
+
+ breakpoints = editor.getBreakpoints();
+ is(breakpoints[0].line, 3, "breakpoint[0].line is correct");
+ is(breakpoints[0].condition, "foo == 'bar'",
+ "breakpoint[0].condition is correct");
+
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 0, "no breakpoint added");
+ is(event.removed.length, 1, "one breakpoint removed");
+ is(event.removed[0].line, 0, "event removed[0].line is correct");
+ ok(!event.removed[0].condition, "event removed[0].condition is correct");
+
+ // Remove the breakpoint from line 3
+
+ event = null;
+ is(editor.removeBreakpoint(3), true, "removeBreakpoint(3) returns true");
+
+ is(editor.getBreakpoints().length, 0, "no breakpoints");
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 0, "no breakpoint added");
+ is(event.removed.length, 1, "one breakpoint removed");
+ is(event.removed[0].line, 3, "event removed[0].line is correct");
+ is(event.removed[0].condition, "foo == 'bar'",
+ "event removed[0].condition is correct");
+
+ // Add a breakpoint with the mouse
+
+ event = null;
+ EventUtils.synthesizeMouse(editor.editorElement, 10, 10, {}, testWin);
+
+ breakpoints = editor.getBreakpoints();
+ is(breakpoints.length, 1, "one breakpoint added");
+ is(breakpoints[0].line, 0, "breakpoint[0].line is correct");
+ ok(!breakpoints[0].condition, "breakpoint[0].condition is correct");
+
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 1, "one breakpoint added (confirmed)");
+ is(event.removed.length, 0, "no breakpoint removed");
+ is(event.added[0].line, 0, "event added[0].line is correct");
+ ok(!event.added[0].condition, "event added[0].condition is correct");
+
+ // Remove a breakpoint with the mouse
+
+ event = null;
+ EventUtils.synthesizeMouse(editor.editorElement, 10, 10, {}, testWin);
+
+ breakpoints = editor.getBreakpoints();
+ is(breakpoints.length, 0, "one breakpoint removed");
+
+ ok(event, "breakpoint event fired");
+ is(event.added.length, 0, "no breakpoint added");
+ is(event.removed.length, 1, "one breakpoint removed (confirmed)");
+ is(event.removed[0].line, 0, "event removed[0].line is correct");
+ ok(!event.removed[0].condition, "event removed[0].condition is correct");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug712982_line_ruler_click.js b/browser/devtools/sourceeditor/test/browser_bug712982_line_ruler_click.js
new file mode 100644
index 000000000..61397fbcf
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug712982_line_ruler_click.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";
+
+function test() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component == "textarea") {
+ ok(true, "skip test for bug 712982: only applicable for non-textarea components");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 712982' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showLineNumbers: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+
+ editor.setText("line1\nline2\nline3\nline4");
+
+ editor.setCaretPosition(3);
+ let pos = editor.getCaretPosition();
+ ok(pos.line == 3 && pos.col == 0, "initial caret location is correct");
+
+ EventUtils.synthesizeMouse(editor.editorElement, 10, 10, {}, testWin);
+
+ is(editor.getCaretOffset(), 0, "click on line 0 worked");
+
+ editor.setCaretPosition(2);
+ EventUtils.synthesizeMouse(editor.editorElement, 11, 11,
+ {shiftKey: true}, testWin);
+ is(editor.getSelectedText().trim(), "line1\nline2", "shift+click works");
+
+ editor.setCaretOffset(0);
+
+ EventUtils.synthesizeMouse(editor.editorElement, 10, 10,
+ {clickCount: 2}, testWin);
+
+ is(editor.getSelectedText().trim(), "line1", "double click works");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug725388_mouse_events.js b/browser/devtools/sourceeditor/test/browser_bug725388_mouse_events.js
new file mode 100644
index 000000000..16e6701af
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug725388_mouse_events.js
@@ -0,0 +1,107 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Test for bug 725388' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+ testWin.resizeBy(1, 2);
+
+ let text = "BrowserBug - 725388";
+ editor.setText(text);
+
+ let target = editor.editorElement;
+ let targetWin = target.ownerDocument.defaultView;
+
+ let eventsFired = 0;
+
+ let done = function() {
+ eventsFired++;
+ if (eventsFired == 3) {
+ executeSoon(testEnd);
+ }
+ };
+
+ let mMoveHandler = function(aEvent) {
+ editor.removeEventListener(SourceEditor.EVENTS.MOUSE_MOVE, mMoveHandler);
+
+ is(aEvent.event.type, "mousemove", "MouseMove event fired.");
+
+ executeSoon(done);
+ };
+
+ let mOverHandler = function(aEvent) {
+ editor.removeEventListener(SourceEditor.EVENTS.MOUSE_OVER, mOverHandler);
+
+ is(aEvent.event.type, "mouseover", "MouseOver event fired.");
+
+ executeSoon(done);
+ };
+
+ let mOutHandler = function(aEvent) {
+ editor.removeEventListener(SourceEditor.EVENTS.MOUSE_OUT, mOutHandler);
+
+ is(aEvent.event.type, "mouseout", "MouseOut event fired.");
+
+ executeSoon(done);
+ };
+
+ editor.addEventListener(SourceEditor.EVENTS.MOUSE_OVER, mOverHandler);
+ editor.addEventListener(SourceEditor.EVENTS.MOUSE_MOVE, mMoveHandler);
+ editor.addEventListener(SourceEditor.EVENTS.MOUSE_OUT, mOutHandler);
+
+ waitForFocus(function() {
+ EventUtils.synthesizeMouse(target, 10, 10, {type: "mouseover"},
+ targetWin);
+ EventUtils.synthesizeMouse(target, 15, 17, {type: "mousemove"},
+ targetWin);
+ EventUtils.synthesizeMouse(target, -10, -10, {type: "mouseout"},
+ targetWin);
+ }, targetWin);
+}
+
+function testEnd()
+{
+ if (editor) {
+ editor.destroy();
+ }
+ if (testWin) {
+ testWin.close();
+ }
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug725392_mouse_coords_char_offset.js b/browser/devtools/sourceeditor/test/browser_bug725392_mouse_coords_char_offset.js
new file mode 100644
index 000000000..5d5dcff5f
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug725392_mouse_coords_char_offset.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/ */
+
+"use strict";
+
+function test()
+{
+ let testWin;
+ let editor;
+ let mousePos = { x: 36, y: 4 };
+ let expectedOffset = 5;
+ let maxDiff = 10;
+
+ waitForExplicitFinish();
+
+ function editorLoaded(aEditor, aWindow)
+ {
+ editor = aEditor;
+ testWin = aWindow;
+
+ let text = fillEditor(editor, 3);
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ doMouseMove(testPage1);
+ }
+
+ function doMouseMove(aCallback)
+ {
+ function mouseEventHandler(aEvent)
+ {
+ editor.removeEventListener(editor.EVENTS.MOUSE_OUT, mouseEventHandler);
+ editor.removeEventListener(editor.EVENTS.MOUSE_OVER, mouseEventHandler);
+ editor.removeEventListener(editor.EVENTS.MOUSE_MOVE, mouseEventHandler);
+
+ executeSoon(aCallback.bind(null, aEvent));
+ }
+
+ editor.addEventListener(editor.EVENTS.MOUSE_MOVE, mouseEventHandler);
+ editor.addEventListener(editor.EVENTS.MOUSE_OUT, mouseEventHandler);
+ editor.addEventListener(editor.EVENTS.MOUSE_OVER, mouseEventHandler);
+
+ let target = editor.editorElement;
+ let targetWin = testWin;
+
+ EventUtils.synthesizeMouse(target, mousePos.x, mousePos.y,
+ {type: "mousemove"}, targetWin);
+ EventUtils.synthesizeMouse(target, mousePos.x, mousePos.y,
+ {type: "mouseout"}, targetWin);
+ EventUtils.synthesizeMouse(target, mousePos.x, mousePos.y,
+ {type: "mouseover"}, targetWin);
+ }
+
+ function checkValue(aValue, aExpectedValue)
+ {
+ let result = Math.abs(aValue - aExpectedValue) <= maxDiff;
+ if (!result) {
+ info("checkValue() given " + aValue + " expected " + aExpectedValue);
+ }
+ return result;
+ }
+
+ function testPage1(aEvent)
+ {
+ let {event: { clientX: clientX, clientY: clientY }, x: x, y: y} = aEvent;
+
+ info("testPage1 " + aEvent.type +
+ " clientX " + clientX + " clientY " + clientY +
+ " x " + x + " y " + y);
+
+ // x and y are in document coordinates.
+ // clientX and clientY are in view coordinates.
+ // since we are scrolled at the top, both are expected to be approximately
+ // the same.
+ ok(checkValue(x, mousePos.x), "x is in range");
+ ok(checkValue(y, mousePos.y), "y is in range");
+
+ ok(checkValue(clientX, mousePos.x), "clientX is in range");
+ ok(checkValue(clientY, mousePos.y), "clientY is in range");
+
+ // we give document-relative coordinates here.
+ let offset = editor.getOffsetAtLocation(x, y);
+ ok(checkValue(offset, expectedOffset), "character offset is correct");
+
+ let rect = {x: x, y: y};
+ let viewCoords = editor.convertCoordinates(rect, "document", "view");
+ ok(checkValue(viewCoords.x, clientX), "viewCoords.x is in range");
+ ok(checkValue(viewCoords.y, clientY), "viewCoords.y is in range");
+
+ rect = {x: clientX, y: clientY};
+ let docCoords = editor.convertCoordinates(rect, "view", "document");
+ ok(checkValue(docCoords.x, x), "docCoords.x is in range");
+ ok(checkValue(docCoords.y, y), "docCoords.y is in range");
+
+ // we are given document-relative coordinates.
+ let offsetPos = editor.getLocationAtOffset(expectedOffset);
+ ok(checkValue(offsetPos.x, x), "offsetPos.x is in range");
+ ok(checkValue(offsetPos.y, y), "offsetPos.y is in range");
+
+ // Scroll the view and test again.
+ let topIndex = Math.round(editor.getLineCount() / 2);
+ editor.setTopIndex(topIndex);
+ expectedOffset += editor.getLineStart(topIndex);
+
+ executeSoon(doMouseMove.bind(null, testPage2));
+ }
+
+ function testPage2(aEvent)
+ {
+ let {event: { clientX: clientX, clientY: clientY }, x: x, y: y} = aEvent;
+
+ info("testPage2 " + aEvent.type +
+ " clientX " + clientX + " clientY " + clientY +
+ " x " + x + " y " + y);
+
+ // after page scroll document coordinates need to be different from view
+ // coordinates.
+ ok(checkValue(x, mousePos.x), "x is not different from clientX");
+ ok(!checkValue(y, mousePos.y), "y is different from clientY");
+
+ ok(checkValue(clientX, mousePos.x), "clientX is in range");
+ ok(checkValue(clientY, mousePos.y), "clientY is in range");
+
+ // we give document-relative coordinates here.
+ let offset = editor.getOffsetAtLocation(x, y);
+ ok(checkValue(offset, expectedOffset), "character offset is correct");
+
+ let rect = {x: x, y: y};
+ let viewCoords = editor.convertCoordinates(rect, "document", "view");
+ ok(checkValue(viewCoords.x, clientX), "viewCoords.x is in range");
+ ok(checkValue(viewCoords.y, clientY), "viewCoords.y is in range");
+
+ rect = {x: clientX, y: clientY};
+ let docCoords = editor.convertCoordinates(rect, "view", "document");
+ ok(checkValue(docCoords.x, x), "docCoords.x is in range");
+ ok(checkValue(docCoords.y, y), "docCoords.y is in range");
+
+ // we are given document-relative coordinates.
+ let offsetPos = editor.getLocationAtOffset(expectedOffset);
+ ok(checkValue(offsetPos.x, x), "offsetPos.x is in range");
+ ok(checkValue(offsetPos.y, y), "offsetPos.y is in range");
+
+ executeSoon(testEnd);
+ }
+
+ function testEnd()
+ {
+ if (editor) {
+ editor.destroy();
+ }
+ if (testWin) {
+ testWin.close();
+ }
+
+ waitForFocus(finish, window);
+ }
+
+ openSourceEditorWindow(editorLoaded);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug725430_comment_uncomment.js b/browser/devtools/sourceeditor/test/browser_bug725430_comment_uncomment.js
new file mode 100644
index 000000000..5fbe1d89b
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug725430_comment_uncomment.js
@@ -0,0 +1,151 @@
+/* vim: set 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() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 725430' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showLineNumbers: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+ let text = "firstline\nsecondline\nthirdline\nfourthline";
+
+ editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ editor.setText(text)
+
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), "//" + text, "JS Single line Commenting Works");
+ editor.undo();
+ is(editor.getText(), text, "Undo Single Line Commenting action works");
+ editor.redo();
+ is(editor.getText(), "//" + text, "Redo works");
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "JS Single Line Uncommenting works");
+
+ editor.setText(text);
+
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), "/*" + text + "*/", "JS Block Commenting works");
+ editor.undo();
+ is(editor.getText(), text, "Undo Block Commenting action works");
+ editor.redo();
+ is(editor.getText(), "/*" + text + "*/", "Redo works");
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "JS Block Uncommenting works");
+ editor.undo();
+ is(editor.getText(), "/*" + text + "*/", "Undo Block Uncommenting works");
+ editor.redo();
+ is(editor.getText(), text, "Redo works");
+
+ let regText = "//firstline\n // secondline\nthird//line\n//fourthline";
+ let expText = "firstline\n secondline\nthird//line\nfourthline";
+ editor.setText(regText);
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), expText, "JS Multiple Line Uncommenting works");
+ editor.undo();
+ is(editor.getText(), regText, "Undo Multiple Line Uncommenting works");
+ editor.redo();
+ is(editor.getText(), expText, "Redo works");
+
+ editor.setMode(SourceEditor.MODES.CSS);
+ editor.setText(text);
+
+ expText = "/*firstline*/\nsecondline\nthirdline\nfourthline";
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), expText, "CSS Commenting without selection works");
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "CSS Uncommenting without selection works");
+
+ editor.setText(text);
+
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), "/*" + text + "*/", "CSS Multiple Line Commenting works");
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "CSS Multiple Line Uncommenting works");
+
+ editor.setMode(SourceEditor.MODES.HTML);
+ editor.setText(text);
+
+ expText = "<!--firstline-->\nsecondline\nthirdline\nfourthline";
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), expText, "HTML Commenting without selection works");
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "HTML Uncommenting without selection works");
+
+ editor.setText(text);
+
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), "<!--" + text + "-->", "HTML Multiple Line Commenting works");
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "HTML Multiple Line Uncommenting works");
+
+ editor.setMode(SourceEditor.MODES.TEXT);
+ editor.setText(text);
+
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "Commenting disabled in Text mode");
+ editor.setText(regText);
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), regText, "Uncommenting disabled in Text mode");
+
+ editor.setText(text);
+ editor.readOnly = true;
+
+ editor.setCaretPosition(0);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), text, "Commenting disabled in ReadOnly mode");
+ editor.setText(regText);
+ EventUtils.synthesizeKey("VK_A", {accelKey: true}, testWin);
+ EventUtils.synthesizeKey("/", {accelKey: true}, testWin);
+ is(editor.getText(), regText, "Uncommenting disabled in ReadOnly mode");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug725618_moveLines_shortcut.js b/browser/devtools/sourceeditor/test/browser_bug725618_moveLines_shortcut.js
new file mode 100644
index 000000000..c86a436c6
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug725618_moveLines_shortcut.js
@@ -0,0 +1,117 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let editor;
+let testWin;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 725618 - moveLines shortcut' width='300' height='500'>" +
+ "<box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ let text = "target\nfoo\nbar"
+ let config = {
+ initialText: text,
+ };
+
+ editor = new SourceEditor();
+ editor.init(box, config, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ editor.setCaretOffset(0);
+
+ let modifiers = {altKey: true, ctrlKey: Services.appinfo.OS == "Darwin"};
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "foo\ntarget\nbar", "Move lines down works");
+ is(editor.getSelectedText(), "target\n", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "foo\nbar\ntarget", "Move lines down works");
+ is(editor.getSelectedText(), "target", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "foo\nbar\ntarget", "Check for bottom of editor works");
+ is(editor.getSelectedText(), "target", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "foo\ntarget\nbar", "Move lines up works");
+ is(editor.getSelectedText(), "target\n", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar", "Move lines up works");
+ is(editor.getSelectedText(), "target\n", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar", "Check for top of editor works");
+ is(editor.getSelectedText(), "target\n", "selection is correct");
+
+ editor.setSelection(0, 10);
+ info("text within selection =" + editor.getSelectedText());
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "bar\ntarget\nfoo", "Multiple line move down works");
+ is(editor.getSelectedText(), "target\nfoo", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "bar\ntarget\nfoo",
+ "Check for bottom of editor works with multiple line selection");
+ is(editor.getSelectedText(), "target\nfoo", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar", "Multiple line move up works");
+ is(editor.getSelectedText(), "target\nfoo\n", "selection is correct");
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar",
+ "Check for top of editor works with multiple line selection");
+ is(editor.getSelectedText(), "target\nfoo\n", "selection is correct");
+
+ editor.readOnly = true;
+
+ editor.setCaretOffset(0);
+
+ EventUtils.synthesizeKey("VK_UP", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar",
+ "Check for readOnly mode works with move lines up");
+
+ EventUtils.synthesizeKey("VK_DOWN", modifiers, testWin);
+ is(editor.getText(), "target\nfoo\nbar",
+ "Check for readOnly mode works with move lines down");
+
+ finish();
+}
+
+registerCleanupFunction(function()
+{
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+});
diff --git a/browser/devtools/sourceeditor/test/browser_bug729480_line_vertical_align.js b/browser/devtools/sourceeditor/test/browser_bug729480_line_vertical_align.js
new file mode 100644
index 000000000..171141ba5
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug729480_line_vertical_align.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";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let editor;
+const VERTICAL_OFFSET = 3;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:application/vnd.mozilla.xul+xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 729480 - allow setCaretPosition align the target line" +
+ " vertically in view according to a third argument'" +
+ " width='300' height='300'><box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let box = testWin.document.querySelector("box");
+
+ editor = new SourceEditor();
+ editor.init(box, {showLineNumbers: true}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ // setting 3 pages of lines containing the line number.
+ let view = editor._view;
+
+ let lineHeight = view.getLineHeight();
+ let editorHeight = view.getClientArea().height;
+ let linesPerPage = Math.floor(editorHeight / lineHeight);
+ let totalLines = 3 * linesPerPage;
+
+ let text = "";
+ for (let i = 0; i < totalLines; i++) {
+ text += "Line " + i + "\n";
+ }
+
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ let offset = Math.min(Math.round(linesPerPage/2), VERTICAL_OFFSET);
+ // Building the iterator array.
+ // [line, alignment, topIndex_check]
+ let iterateOn = [
+ [0, "TOP", 0],
+ [25, "TOP", 25 - offset],
+ // Case when the target line is already in view.
+ [27, "TOP", 25 - offset],
+ [0, "BOTTOM", 0],
+ [5, "BOTTOM", 0],
+ [38, "BOTTOM", 38 - linesPerPage + offset],
+ [0, "CENTER", 0],
+ [4, "CENTER", 0],
+ [34, "CENTER", 34 - Math.round(linesPerPage/2)]
+ ];
+
+ function testEnd() {
+ editor.destroy();
+ testWin.close();
+ testWin = editor = null;
+ waitForFocus(finish, window);
+ }
+
+ function testPosition(pos) {
+ is(editor.getTopIndex(), iterateOn[pos][2], "scroll is correct for test #" + pos);
+ iterator(++pos);
+ }
+
+ function iterator(i) {
+ if (i == iterateOn.length) {
+ testEnd();
+ } else {
+ editor.setCaretPosition(iterateOn[i][0], 0,
+ editor.VERTICAL_ALIGN[iterateOn[i][1]]);
+ executeSoon(testPosition.bind(this, i));
+ }
+ }
+ iterator(0);
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug729960_block_bracket_jump.js b/browser/devtools/sourceeditor/test/browser_bug729960_block_bracket_jump.js
new file mode 100644
index 000000000..534018e24
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug729960_block_bracket_jump.js
@@ -0,0 +1,164 @@
+/* vim: set 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() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 729960' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showLineNumbers: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+ let JSText = "function foo(aVar) {\n" +
+ " // Block Level 1\n\n" +
+ " function level2() {\n" +
+ " let baz = aVar;\n" +
+ " // Block Level 2\n" +
+ " function level3() {\n" +
+ " // Block Level 3\n" +
+ " }\n" +
+ " }\n" +
+ " // Block Level 1" +
+ " function bar() { /* Block Level 2 */ }\n" +
+ "}";
+
+ editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ editor.setText(JSText);
+
+ // Setting caret at Line 1 bracket start.
+ editor.setCaretOffset(19);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 220,
+ "JS : Jump to closing bracket of the code block when caret at block start");
+
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 20,
+ "JS : Jump to opening bracket of the code block when caret at block end");
+
+ // Setting caret at Line 10 start.
+ editor.setCaretOffset(161);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 20,
+ "JS : Jump to opening bracket of code block when inside the function");
+
+ editor.setCaretOffset(161);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 220,
+ "JS : Jump to closing bracket of code block when inside the function");
+
+ // Setting caret at Line 6 start.
+ editor.setCaretOffset(67);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 159,
+ "JS : Jump to closing bracket in a nested function with caret inside");
+
+ editor.setCaretOffset(67);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 62,
+ "JS : Jump to opening bracket in a nested function with caret inside");
+
+ let CSSText = "#object {\n" +
+ " property: value;\n" +
+ " /* comment */\n" +
+ "}";
+
+ editor.setMode(SourceEditor.MODES.CSS);
+ editor.setText(CSSText);
+
+ // Setting caret at Line 1 bracket start.
+ editor.setCaretOffset(8);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 45,
+ "CSS : Jump to closing bracket of the code block when caret at block start");
+
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 9,
+ "CSS : Jump to opening bracket of the code block when caret at block end");
+
+ // Setting caret at Line 3 start.
+ editor.setCaretOffset(28);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 9,
+ "CSS : Jump to opening bracket of code block when inside the function");
+
+ editor.setCaretOffset(28);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 45,
+ "CSS : Jump to closing bracket of code block when inside the function");
+
+ let HTMLText = "<html>\n" +
+ " <head>\n" +
+ " <title>Testing Block Jump</title>\n" +
+ " </head>\n" +
+ " <body></body>\n" +
+ "</html>";
+
+ editor.setMode(SourceEditor.MODES.HTML);
+ editor.setText(HTMLText);
+
+ // Setting caret at Line 1 end.
+ editor.setCaretOffset(6);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 6,
+ "HTML : Jump to block end : Nothing happens in html mode");
+
+ // Setting caret at Line 4 end.
+ editor.setCaretOffset(64);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 64,
+ "HTML : Jump to block start : Nothing happens in html mode");
+
+ let text = "line 1\n" +
+ "line 2\n" +
+ "line 3\n" +
+ "line 4\n";
+
+ editor.setMode(SourceEditor.MODES.TEXT);
+ editor.setText(text);
+
+ // Setting caret at Line 1 start.
+ editor.setCaretOffset(0);
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 0,
+ "Text : Jump to block end : Nothing happens in text mode");
+
+ // Setting caret at Line 4 end.
+ editor.setCaretOffset(28);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 28,
+ "Text : Jump to block start : Nothing happens in text mode");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug731721_debugger_stepping.js b/browser/devtools/sourceeditor/test/browser_bug731721_debugger_stepping.js
new file mode 100644
index 000000000..3326f563e
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug731721_debugger_stepping.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";
+
+function test() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 731721' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showAnnotationRuler: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+
+ editor.setText("line1\nline2\nline3\nline4");
+
+ is(editor.getDebugLocation(), -1, "no debugger location");
+
+ editor.setDebugLocation(1);
+ is(editor.getDebugLocation(), 1, "set debugger location works");
+
+ editor.setDebugLocation(3);
+ is(editor.getDebugLocation(), 3, "change debugger location works");
+
+ editor.setDebugLocation(-1);
+ is(editor.getDebugLocation(), -1, "clear debugger location works");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_bug744021_next_prev_bracket_jump.js b/browser/devtools/sourceeditor/test/browser_bug744021_next_prev_bracket_jump.js
new file mode 100644
index 000000000..72b7d45c6
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug744021_next_prev_bracket_jump.js
@@ -0,0 +1,104 @@
+/* vim: set 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() {
+
+ let temp = {};
+ Cu.import("resource:///modules/source-editor.jsm", temp);
+ let SourceEditor = temp.SourceEditor;
+
+ waitForExplicitFinish();
+
+ let editor;
+
+ const windowUrl = "data:text/xml;charset=utf8,<?xml version='1.0'?><window " +
+ "xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' " +
+ "title='test for bug 744021' width='600' height='500'><hbox flex='1'/>" +
+ "</window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable," +
+ "dialog=no";
+
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank",
+ windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {showLineNumbers: true}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+ let JSText = "function foo() {\n" +
+ " \n" +
+ " function level2() {\n" +
+ " \n" +
+ " function level3() {\n" +
+ " \n" +
+ " }\n" +
+ " }\n" +
+ " function bar() { /* Block Level 2 */ }\n" +
+ "}\n" +
+ "function baz() {\n" +
+ " \n" +
+ "}";
+
+ editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ editor.setText(JSText);
+
+ // Setting caret at end of line 11 (function baz() {).
+ editor.setCaretOffset(147);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 16,
+ "JS : Jump to opening bracket of previous sibling block when no parent");
+
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 129,
+ "JS : Jump to closing bracket of same code block");
+
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 151,
+ "JS : Jump to closing bracket of next sibling code block");
+
+ let CSSText = "#object1 {\n" +
+ " property: value;\n" +
+ " /* comment */\n" +
+ "}\n" +
+ ".class1 {\n" +
+ " property: value;\n" +
+ "}";
+
+ editor.setMode(SourceEditor.MODES.CSS);
+ editor.setText(CSSText);
+
+ // Setting caret at Line 5 end (.class1 {).
+ editor.setCaretOffset(57);
+ EventUtils.synthesizeKey("[", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 10,
+ "CSS : Jump to opening bracket of previous sibling code block");
+
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 46,
+ "CSS : Jump to closing bracket of same code block");
+
+ EventUtils.synthesizeKey("]", {accelKey: true}, testWin);
+ is(editor.getCaretOffset(), 77,
+ "CSS : Jump to closing bracket of next sibling code block");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+ }
+}
diff --git a/browser/devtools/sourceeditor/test/browser_sourceeditor_initialization.js b/browser/devtools/sourceeditor/test/browser_sourceeditor_initialization.js
new file mode 100644
index 000000000..c605f9860
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_sourceeditor_initialization.js
@@ -0,0 +1,499 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+let testWin;
+let testDoc;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 660784' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ testDoc = testWin.document;
+
+ let hbox = testDoc.querySelector("hbox");
+
+ editor = new SourceEditor();
+ let config = {
+ showLineNumbers: true,
+ initialText: "foobarbaz",
+ tabSize: 7,
+ expandTab: true,
+ };
+
+ editor.init(hbox, config, editorLoaded);
+}
+
+function editorLoaded()
+{
+ ok(editor.editorElement, "editor loaded");
+
+ is(editor.parentElement, testDoc.querySelector("hbox"),
+ "parentElement is correct");
+
+ editor.focus();
+
+ is(editor.getMode(), SourceEditor.DEFAULTS.mode, "default editor mode");
+
+ // Test general editing methods.
+
+ ok(!editor.canUndo(), "canUndo() works (nothing to undo), just loaded");
+
+ ok(!editor.readOnly, "editor is not read-only");
+
+ is(editor.getText(), "foobarbaz", "placeholderText works");
+
+ is(editor.getText().length, editor.getCharCount(),
+ "getCharCount() is correct");
+
+ is(editor.getText(3, 5), "ba", "getText() range works");
+
+ editor.setText("source-editor");
+
+ is(editor.getText(), "source-editor", "setText() works");
+
+ editor.setText("code", 0, 6);
+
+ is(editor.getText(), "code-editor", "setText() range works");
+
+ ok(editor.canUndo(), "canUndo() works (things to undo)");
+ ok(!editor.canRedo(), "canRedo() works (nothing to redo yet)");
+
+ editor.undo();
+
+ is(editor.getText(), "source-editor", "undo() works");
+
+ ok(editor.canRedo(), "canRedo() works (things to redo)");
+
+ editor.redo();
+
+ is(editor.getText(), "code-editor", "redo() works");
+
+ EventUtils.synthesizeKey("VK_Z", {accelKey: true}, testWin);
+
+ is(editor.getText(), "source-editor", "Ctrl-Z (undo) works");
+
+ EventUtils.synthesizeKey("VK_Z", {accelKey: true, shiftKey: true}, testWin);
+
+ is(editor.getText(), "code-editor", "Ctrl-Shift-Z (redo) works");
+
+ editor.undo();
+
+ EventUtils.synthesizeKey("VK_Y", {accelKey: true}, testWin);
+ if (Services.appinfo.OS == "WINNT" ||
+ Services.appinfo.OS == "Linux") {
+ is(editor.getText(), "code-editor",
+ "CTRL+Y does redo on Linux and Windows");
+ } else {
+ is(editor.getText(), "source-editor",
+ "CTRL+Y doesn't redo on machines other than Linux and Windows");
+ editor.setText("code-editor");
+ }
+
+ // Test selection methods.
+
+ editor.setSelection(0, 4);
+
+ is(editor.getSelectedText(), "code", "getSelectedText() works");
+
+ let selection = editor.getSelection();
+ ok(selection.start == 0 && selection.end == 4, "getSelection() works");
+
+ editor.dropSelection();
+
+ selection = editor.getSelection();
+ ok(selection.start == 4 && selection.end == 4, "dropSelection() works");
+
+ editor.setCaretOffset(7);
+ is(editor.getCaretOffset(), 7, "setCaretOffset() works");
+
+ // Test grouped changes.
+
+ editor.setText("foobar");
+
+ editor.startCompoundChange();
+
+ editor.setText("foo1");
+ editor.setText("foo2");
+ editor.setText("foo3");
+
+ editor.endCompoundChange();
+
+ is(editor.getText(), "foo3", "editor content is correct");
+
+ editor.undo();
+ is(editor.getText(), "foobar", "compound change undo() works");
+
+ editor.redo();
+ is(editor.getText(), "foo3", "compound change redo() works");
+
+ // Minimal keyboard usage tests.
+
+ ok(editor.hasFocus(), "editor has focus");
+
+ editor.setText("code-editor");
+ editor.setCaretOffset(7);
+ EventUtils.synthesizeKey(".", {}, testWin);
+
+ is(editor.getText(), "code-ed.itor", "focus() and typing works");
+
+ EventUtils.synthesizeKey("a", {}, testWin);
+
+ is(editor.getText(), "code-ed.aitor", "typing works");
+
+ is(editor.getCaretOffset(), 9, "caret moved");
+
+ EventUtils.synthesizeKey("VK_LEFT", {}, testWin);
+
+ is(editor.getCaretOffset(), 8, "caret moved to the left");
+
+ EventUtils.synthesizeKey(".", {}, testWin);
+ EventUtils.synthesizeKey("VK_TAB", {}, testWin);
+
+ is(editor.getText(), "code-ed.. aitor", "Tab works");
+
+ is(editor.getCaretOffset(), 14, "caret location is correct");
+
+ // Test the Tab key.
+
+ editor.setText("a\n b\n c");
+ editor.setCaretOffset(0);
+
+ EventUtils.synthesizeKey("VK_TAB", {}, testWin);
+ is(editor.getText(), " a\n b\n c", "Tab works");
+
+ // Code editor specific tests. These are not applicable when the textarea
+ // fallback is used.
+ let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
+ if (component != "textarea") {
+ editor.setMode(SourceEditor.MODES.JAVASCRIPT);
+ is(editor.getMode(), SourceEditor.MODES.JAVASCRIPT, "setMode() works");
+
+ editor.setSelection(0, editor.getCharCount() - 1);
+ EventUtils.synthesizeKey("VK_TAB", {}, testWin);
+ is(editor.getText(), " a\n b\n c", "lines indented");
+
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, testWin);
+ is(editor.getText(), " a\n b\n c", "lines outdented (shift-tab)");
+
+ testEclipseBug362107();
+ testBug687577();
+ testBackspaceKey();
+ testReturnKey();
+ }
+
+ // Test the read-only mode.
+
+ editor.setText("foofoo");
+
+ editor.readOnly = true;
+ EventUtils.synthesizeKey("b", {}, testWin);
+ is(editor.getText(), "foofoo", "editor is now read-only (keyboard)");
+
+ editor.setText("foobar");
+ is(editor.getText(), "foobar", "editor allows programmatic changes (setText)");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, testWin);
+ is(editor.getText(), "foobar", "Enter key does nothing");
+
+ EventUtils.synthesizeKey("VK_TAB", {}, testWin);
+ is(editor.getText(), "foobar", "Tab does nothing");
+
+ editor.setText(" foobar");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, testWin);
+ is(editor.getText(), " foobar", "Shift+Tab does nothing");
+
+ editor.readOnly = false;
+
+ editor.setCaretOffset(editor.getCharCount());
+ EventUtils.synthesizeKey("-", {}, testWin);
+ is(editor.getText(), " foobar-", "editor is now editable again");
+
+ // Test the Selection event.
+
+ editor.setText("foobarbaz");
+
+ editor.setSelection(1, 4);
+
+ let event = null;
+
+ let eventHandler = function(aEvent) {
+ event = aEvent;
+ };
+ editor.addEventListener(SourceEditor.EVENTS.SELECTION, eventHandler);
+
+ editor.setSelection(0, 3);
+
+ ok(event, "selection event fired");
+ ok(event.oldValue.start == 1 && event.oldValue.end == 4,
+ "event.oldValue is correct");
+ ok(event.newValue.start == 0 && event.newValue.end == 3,
+ "event.newValue is correct");
+
+ event = null;
+ editor.dropSelection();
+
+ ok(event, "selection dropped");
+ ok(event.oldValue.start == 0 && event.oldValue.end == 3,
+ "event.oldValue is correct");
+ ok(event.newValue.start == 3 && event.newValue.end == 3,
+ "event.newValue is correct");
+
+ event = null;
+ EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
+
+ ok(event, "select all worked");
+ ok(event.oldValue.start == 3 && event.oldValue.end == 3,
+ "event.oldValue is correct");
+ ok(event.newValue.start == 0 && event.newValue.end == 9,
+ "event.newValue is correct");
+
+ event = null;
+ editor.removeEventListener(SourceEditor.EVENTS.SELECTION, eventHandler);
+ editor.dropSelection();
+
+ ok(!event, "selection event listener removed");
+
+ // Test the TextChanged event.
+
+ editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, eventHandler);
+
+ EventUtils.synthesizeKey(".", {}, testWin);
+
+ ok(event, "the TextChanged event fired after keypress");
+ is(event.start, 9, "event.start is correct");
+ is(event.removedCharCount, 0, "event.removedCharCount is correct");
+ is(event.addedCharCount, 1, "event.addedCharCount is correct");
+
+ editor.setText("line1\nline2\nline3");
+ let chars = editor.getText().length;
+ event = null;
+
+ editor.setText("a\nline4\nline5", chars);
+
+ ok(event, "the TextChanged event fired after setText()");
+ is(event.start, chars, "event.start is correct");
+ is(event.removedCharCount, 0, "event.removedCharCount is correct");
+ is(event.addedCharCount, 13, "event.addedCharCount is correct");
+
+ event = null;
+ editor.setText("line3b\nline4b\nfoo", 12, 24);
+
+ ok(event, "the TextChanged event fired after setText() again");
+ is(event.start, 12, "event.start is correct");
+ is(event.removedCharCount, 12, "event.removedCharCount is correct");
+ is(event.addedCharCount, 17, "event.addedCharCount is correct");
+
+ editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, eventHandler);
+
+ testClipboardEvents();
+}
+
+function testEnd()
+{
+ editor.destroy();
+ ok(!editor.parentElement && !editor.editorElement, "destroy() works");
+
+ testWin.close();
+
+ testWin = testDoc = editor = null;
+
+ waitForFocus(finish, window);
+}
+
+function testBackspaceKey()
+{
+ editor.setText(" a\n b\n c");
+ editor.setCaretOffset(7);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, testWin);
+ is(editor.getText(), "a\n b\n c", "line outdented (Backspace)");
+
+ editor.undo();
+
+ editor.setCaretOffset(6);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, testWin);
+ is(editor.getText(), " a\n b\n c", "backspace one char works");
+}
+
+function testReturnKey()
+{
+ editor.setText(" a\n b\n c");
+
+ editor.setCaretOffset(8);
+ EventUtils.synthesizeKey("VK_RETURN", {}, testWin);
+ EventUtils.synthesizeKey("x", {}, testWin);
+
+ let lineDelimiter = editor.getLineDelimiter();
+ ok(lineDelimiter, "we have the line delimiter");
+
+ let indentationString = editor.getIndentationString();
+ is(" ", indentationString, "we have an indentation string of 7 spaces");
+
+ is(editor.getText(), " a" + lineDelimiter + " x\n b\n c",
+ "return maintains indentation");
+
+ editor.setCaretOffset(12 + lineDelimiter.length);
+ EventUtils.synthesizeKey("z", {}, testWin);
+ EventUtils.synthesizeKey("VK_RETURN", {}, testWin);
+ EventUtils.synthesizeKey("y", {}, testWin);
+ is(editor.getText(), " a" + lineDelimiter +
+ " z" + lineDelimiter + " yx\n b\n c",
+ "return maintains indentation (again)");
+}
+
+function testClipboardEvents()
+{
+ editor.setText("foobar");
+
+ let doCut = function() {
+ EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
+
+ is(editor.getSelectedText(), "foobar", "select all worked");
+
+ EventUtils.synthesizeKey("x", {accelKey: true}, testWin);
+ };
+
+ let onCut = function() {
+ ok(!editor.getText(), "cut works");
+ editor.setText("test--");
+ editor.setCaretOffset(5);
+
+ editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste1);
+ EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
+ };
+
+ let onPaste1 = function() {
+ editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste1);
+
+ is(editor.getText(), "test-foobar-", "paste works");
+
+ executeSoon(waitForClipboard.bind(this, "test", doCopy, onCopy, testEnd));
+ };
+
+ let doCopy = function() {
+ editor.setSelection(0, 4);
+ EventUtils.synthesizeKey("c", {accelKey: true}, testWin);
+ };
+
+ let onCopy = function() {
+ editor.setSelection(5, 11);
+ editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste2);
+ EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
+ };
+
+ let pasteTextChanges = 0;
+ let removedCharCount = 0;
+ let addedCharCount = 0;
+ let onPaste2 = function(aEvent) {
+ pasteTextChanges++;
+ ok(aEvent && (pasteTextChanges == 1 || pasteTextChanges == 2),
+ "event TEXT_CHANGED fired " + pasteTextChanges + " time(s)");
+
+ is(aEvent.start, 5, "event.start is correct");
+ if (aEvent.removedCharCount) {
+ removedCharCount = aEvent.removedCharCount;
+ }
+ if (aEvent.addedCharCount) {
+ addedCharCount = aEvent.addedCharCount;
+ }
+
+ if (pasteTextChanges == 2 || addedCharCount && removedCharCount) {
+ editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste2);
+ executeSoon(checkPaste2Result);
+ }
+ };
+
+ let checkPaste2Result = function() {
+ is(removedCharCount, 6, "event.removedCharCount is correct");
+ is(addedCharCount, 4, "event.addedCharCount is correct");
+
+ is(editor.getText(), "test-test-", "paste works after copy");
+ testEnd();
+ };
+
+ waitForClipboard("foobar", doCut, onCut, testEnd);
+}
+
+function testEclipseBug362107()
+{
+ // Test for Eclipse Bug 362107:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=362107
+ let OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ if (OS != "Linux") {
+ return;
+ }
+
+ editor.setText("line 1\nline 2\nline 3");
+ editor.setCaretOffset(16);
+
+ EventUtils.synthesizeKey("VK_UP", {ctrlKey: true}, testWin);
+ is(editor.getCaretOffset(), 9, "Ctrl-Up works");
+
+ EventUtils.synthesizeKey("VK_UP", {ctrlKey: true}, testWin);
+ is(editor.getCaretOffset(), 2, "Ctrl-Up works twice");
+
+ EventUtils.synthesizeKey("VK_DOWN", {ctrlKey: true}, testWin);
+ is(editor.getCaretOffset(), 9, "Ctrl-Down works");
+
+ EventUtils.synthesizeKey("VK_DOWN", {ctrlKey: true}, testWin);
+ is(editor.getCaretOffset(), 16, "Ctrl-Down works twice");
+}
+
+function testBug687577()
+{
+ // Test for Mozilla Bug 687577:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=687577
+ let OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ if (OS != "Linux") {
+ return;
+ }
+
+ editor.setText("line foobar 1\nline foobar 2\nline foobar 3");
+ editor.setCaretOffset(39);
+
+ EventUtils.synthesizeKey("VK_LEFT", {ctrlKey: true, shiftKey: true}, testWin);
+ let selection = editor.getSelection();
+ is(selection.start, 33, "select.start after Ctrl-Shift-Left");
+ is(selection.end, 39, "select.end after Ctrl-Shift-Left");
+
+ EventUtils.synthesizeKey("VK_UP", {ctrlKey: true, shiftKey: true}, testWin);
+ selection = editor.getSelection();
+ is(selection.start, 14, "select.start after Ctrl-Shift-Up");
+ is(selection.end, 39, "select.end after Ctrl-Shift-Up");
+
+ EventUtils.synthesizeKey("VK_UP", {ctrlKey: true, shiftKey: true}, testWin);
+ selection = editor.getSelection();
+ is(selection.start, 0, "select.start after Ctrl-Shift-Up (again)");
+ is(selection.end, 39, "select.end after Ctrl-Shift-Up (again)");
+
+ EventUtils.synthesizeKey("VK_DOWN", {ctrlKey: true, shiftKey: true}, testWin);
+ selection = editor.getSelection();
+ is(selection.start, 27, "select.start after Ctrl-Shift-Down");
+ is(selection.end, 39, "select.end after Ctrl-Shift-Down");
+
+ EventUtils.synthesizeKey("VK_DOWN", {ctrlKey: true, shiftKey: true}, testWin);
+ selection = editor.getSelection();
+ is(selection.start, 39, "select.start after Ctrl-Shift-Down (again)");
+ is(selection.end, 41, "select.end after Ctrl-Shift-Down (again)");
+}
diff --git a/browser/devtools/sourceeditor/test/head.js b/browser/devtools/sourceeditor/test/head.js
new file mode 100644
index 000000000..e348509c4
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/head.js
@@ -0,0 +1,182 @@
+/* vim: set 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 getLoadContext() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+/*
+ * Polls the X11 primary selection buffer waiting for the expected value. A known
+ * value different than the expected value is put on the clipboard first (and
+ * also polled for) so we can be sure the value we get isn't just the expected
+ * value because it was already in the buffer.
+ *
+ * @param aExpectedStringOrValidatorFn
+ * The string value that is expected to be in the X11 primary selection buffer
+ * or a validator function getting clipboard data and returning a bool.
+ * @param aSetupFn
+ * A function responsible for setting the primary selection buffer to the
+ * expected value, called after the known value setting succeeds.
+ * @param aSuccessFn
+ * A function called when the expected value is found in the primary
+ * selection buffer.
+ * @param aFailureFn
+ * A function called if the expected value isn't found in the primary
+ * selection buffer within 5s. It can also be called if the known value
+ * can't be found.
+ * @param aFlavor [optional] The flavor to look for. Defaults to "text/unicode".
+ */
+function waitForSelection(aExpectedStringOrValidatorFn, aSetupFn,
+ aSuccessFn, aFailureFn, aFlavor) {
+ let requestedFlavor = aFlavor || "text/unicode";
+
+ // Build a default validator function for common string input.
+ var inputValidatorFn = typeof(aExpectedStringOrValidatorFn) == "string"
+ ? function(aData) aData == aExpectedStringOrValidatorFn
+ : aExpectedStringOrValidatorFn;
+
+ let clipboard = Cc["@mozilla.org/widget/clipboard;1"].
+ getService(Ci.nsIClipboard);
+
+ // reset for the next use
+ function reset() {
+ waitForSelection._polls = 0;
+ }
+
+ function wait(validatorFn, successFn, failureFn, flavor) {
+ if (++waitForSelection._polls > 50) {
+ // Log the failure.
+ ok(false, "Timed out while polling the X11 primary selection buffer.");
+ reset();
+ failureFn();
+ return;
+ }
+
+ let transferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ transferable.init(getLoadContext());
+ transferable.addDataFlavor(requestedFlavor);
+
+ clipboard.getData(transferable, clipboard.kSelectionClipboard);
+
+ let str = {};
+ let strLength = {};
+
+ transferable.getTransferData(requestedFlavor, str, strLength);
+
+ let data = null;
+ if (str.value) {
+ let strValue = str.value.QueryInterface(Ci.nsISupportsString);
+ data = strValue.data.substring(0, strLength.value / 2);
+ }
+
+ if (validatorFn(data)) {
+ // Don't show the success message when waiting for preExpectedVal
+ if (preExpectedVal) {
+ preExpectedVal = null;
+ } else {
+ ok(true, "The X11 primary selection buffer has the correct value");
+ }
+ reset();
+ successFn();
+ } else {
+ setTimeout(function() wait(validatorFn, successFn, failureFn, flavor), 100);
+ }
+ }
+
+ // First we wait for a known value different from the expected one.
+ var preExpectedVal = waitForSelection._monotonicCounter +
+ "-waitForSelection-known-value";
+
+ let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboardHelper.copyStringToClipboard(preExpectedVal,
+ Ci.nsIClipboard.kSelectionClipboard,
+ document);
+
+ wait(function(aData) aData == preExpectedVal,
+ function() {
+ // Call the original setup fn
+ aSetupFn();
+ wait(inputValidatorFn, aSuccessFn, aFailureFn, requestedFlavor);
+ }, aFailureFn, "text/unicode");
+}
+
+waitForSelection._polls = 0;
+waitForSelection.__monotonicCounter = 0;
+waitForSelection.__defineGetter__("_monotonicCounter", function () {
+ return waitForSelection.__monotonicCounter++;
+});
+
+/**
+ * Open a new window with a source editor inside.
+ *
+ * @param function aCallback
+ * The function you want invoked once the editor is loaded. The function
+ * is given two arguments: editor instance and the window object.
+ * @param object [aOptions]
+ * The options object to pass to the SourceEditor.init() method.
+ */
+function openSourceEditorWindow(aCallback, aOptions) {
+ const windowUrl = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Test for Source Editor' width='600' height='500'><box flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ let editor = null;
+ let testWin = Services.ww.openWindow(null, windowUrl, "_blank",
+ windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+
+ function initEditor()
+ {
+ let tempScope = {};
+ Cu.import("resource:///modules/source-editor.jsm", tempScope);
+
+ let box = testWin.document.querySelector("box");
+ editor = new tempScope.SourceEditor();
+ editor.init(box, aOptions || {}, editorLoaded);
+ }
+
+ function editorLoaded()
+ {
+ editor.focus();
+ waitForFocus(aCallback.bind(null, editor, testWin), testWin);
+ }
+}
+
+/**
+ * Get text needed to fill the editor view.
+ *
+ * @param object aEditor
+ * The SourceEditor instance you work with.
+ * @param number aPages
+ * The number of pages you want filled with lines.
+ * @return string
+ * The string you can insert into the editor so you fill the desired
+ * number of pages.
+ */
+function fillEditor(aEditor, aPages) {
+ let view = aEditor._view;
+ let model = aEditor._model;
+
+ let lineHeight = view.getLineHeight();
+ let editorHeight = view.getClientArea().height;
+ let linesPerPage = Math.floor(editorHeight / lineHeight);
+ let totalLines = aPages * linesPerPage;
+
+ let text = "";
+ for (let i = 0; i < totalLines; i++) {
+ text += "l" + i + " lorem ipsum dolor sit amet. lipsum foobaris bazbaz,\n";
+ }
+
+ return text;
+}
diff --git a/browser/devtools/sourceeditor/test/moz.build b/browser/devtools/sourceeditor/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/styleeditor/CmdEdit.jsm b/browser/devtools/styleeditor/CmdEdit.jsm
new file mode 100644
index 000000000..f8b7f8ec6
--- /dev/null
+++ b/browser/devtools/styleeditor/CmdEdit.jsm
@@ -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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+
+/**
+ * 'edit' command
+ */
+gcli.addCommand({
+ name: "edit",
+ description: gcli.lookup("editDesc"),
+ manual: gcli.lookup("editManual2"),
+ params: [
+ {
+ name: 'resource',
+ type: {
+ name: 'resource',
+ include: 'text/css'
+ },
+ description: gcli.lookup("editResourceDesc")
+ },
+ {
+ name: "line",
+ defaultValue: 1,
+ type: {
+ name: "number",
+ min: 1,
+ step: 10
+ },
+ description: gcli.lookup("editLineToJumpToDesc")
+ }
+ ],
+ exec: function(args, context) {
+ let target = context.environment.target;
+ return gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ let styleEditor = toolbox.getCurrentPanel();
+ styleEditor.selectStyleSheet(args.resource.element, args.line);
+ return null;
+ });
+ }
+});
diff --git a/browser/devtools/styleeditor/Makefile.in b/browser/devtools/styleeditor/Makefile.in
new file mode 100644
index 000000000..fe75dad69
--- /dev/null
+++ b/browser/devtools/styleeditor/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/styleeditor/StyleEditorDebuggee.jsm b/browser/devtools/styleeditor/StyleEditorDebuggee.jsm
new file mode 100644
index 000000000..48c346dd9
--- /dev/null
+++ b/browser/devtools/styleeditor/StyleEditorDebuggee.jsm
@@ -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";
+
+this.EXPORTED_SYMBOLS = ["StyleEditorDebuggee", "StyleSheet"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+/**
+ * A StyleEditorDebuggee represents the document the style editor is debugging.
+ * It maintains a list of StyleSheet objects that represent the stylesheets in
+ * the target's document. It wraps remote debugging protocol comunications.
+ *
+ * It emits these events:
+ * 'document-load': debuggee's document is loaded, style sheets are argument
+ * 'stylesheets-cleared': The debuggee's stylesheets have been reset (e.g. the
+ * page navigated)
+ *
+ * @param {Target} target
+ * The target the debuggee is listening to
+ */
+let StyleEditorDebuggee = function(target) {
+ EventEmitter.decorate(this);
+
+ this.styleSheets = [];
+
+ this.clear = this.clear.bind(this);
+ this._onNewDocument = this._onNewDocument.bind(this);
+ this._onDocumentLoad = this._onDocumentLoad.bind(this);
+
+ this._target = target;
+ this._actor = this.target.form.styleEditorActor;
+
+ this.client.addListener("documentLoad", this._onDocumentLoad);
+ this._target.on("navigate", this._onNewDocument);
+
+ this._onNewDocument();
+}
+
+StyleEditorDebuggee.prototype = {
+ /**
+ * list of StyleSheet objects for this target
+ */
+ styleSheets: null,
+
+ /**
+ * baseURIObject for the current document
+ */
+ baseURI: null,
+
+ /**
+ * The target we're debugging
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Client for communicating with server with remote debug protocol.
+ */
+ get client() {
+ return this._target.client;
+ },
+
+ /**
+ * Get the StyleSheet object with the given href.
+ *
+ * @param {string} href
+ * Url of the stylesheet to find
+ * @return {StyleSheet}
+ * StyleSheet with the matching href
+ */
+ styleSheetFromHref: function(href) {
+ for (let sheet of this.styleSheets) {
+ if (sheet.href == href) {
+ return sheet;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Clear stylesheets and state.
+ */
+ clear: function() {
+ this.baseURI = null;
+ this.clearStyleSheets();
+ },
+
+ /**
+ * Clear stylesheets.
+ */
+ clearStyleSheets: function() {
+ for (let stylesheet of this.styleSheets) {
+ stylesheet.destroy();
+ }
+ this.styleSheets = [];
+ this.emit("stylesheets-cleared");
+ },
+
+ /**
+ * Called when target is created or has navigated.
+ * Clear previous sheets and request new document's
+ */
+ _onNewDocument: function() {
+ this.clear();
+
+ this._getBaseURI();
+
+ let message = { type: "newDocument" };
+ this._sendRequest(message);
+ },
+
+ /**
+ * request baseURIObject information from the document
+ */
+ _getBaseURI: function() {
+ let message = { type: "getBaseURI" };
+ this._sendRequest(message, (response) => {
+ this.baseURI = response.baseURI;
+ });
+ },
+
+ /**
+ * Handler for document load, forward event with
+ * all the stylesheets available on load.
+ *
+ * @param {string} type
+ * Event type
+ * @param {object} request
+ * Object with 'styleSheets' array of actor forms
+ */
+ _onDocumentLoad: function(type, request) {
+ if (this.styleSheets.length > 0) {
+ this.clearStyleSheets();
+ }
+ let sheets = [];
+ for (let form of request.styleSheets) {
+ let sheet = this._addStyleSheet(form);
+ sheets.push(sheet);
+ }
+ this.emit("document-load", sheets);
+ },
+
+ /**
+ * Create a new StyleSheet object from the form
+ * and add to our stylesheet list.
+ *
+ * @param {object} form
+ * Initial properties of the stylesheet
+ */
+ _addStyleSheet: function(form) {
+ let sheet = new StyleSheet(form, this);
+ this.styleSheets.push(sheet);
+ return sheet;
+ },
+
+ /**
+ * Create a new stylesheet with the given text
+ * and attach it to the document.
+ *
+ * @param {string} text
+ * Initial text of the stylesheet
+ * @param {function} callback
+ * Function to call when the stylesheet has been added to the document
+ */
+ createStyleSheet: function(text, callback) {
+ let message = { type: "newStyleSheet", text: text };
+ this._sendRequest(message, (response) => {
+ let sheet = this._addStyleSheet(response.styleSheet);
+ callback(sheet);
+ });
+ },
+
+ /**
+ * Send a request to our actor on the server
+ *
+ * @param {object} message
+ * Message to send to the actor
+ * @param {function} callback
+ * Function to call with reponse from actor
+ */
+ _sendRequest: function(message, callback) {
+ message.to = this._actor;
+ this.client.request(message, callback);
+ },
+
+ /**
+ * Clean up and remove listeners
+ */
+ destroy: function() {
+ this.clear();
+
+ this._target.off("navigate", this._onNewDocument);
+ }
+}
+
+/**
+ * A StyleSheet object represents a stylesheet on the debuggee. It wraps
+ * communication with a complimentary StyleSheetActor on the server.
+ *
+ * It emits these events:
+ * 'source-load' - The full text source of the stylesheet has been fetched
+ * 'property-change' - Any property (e.g 'disabled') has changed
+ * 'style-applied' - A change has been applied to the live stylesheet on the server
+ * 'error' - An error occured when loading or saving stylesheet
+ *
+ * @param {object} form
+ * Initial properties of the stylesheet
+ * @param {StyleEditorDebuggee} debuggee
+ * Owner of the stylesheet
+ */
+let StyleSheet = function(form, debuggee) {
+ EventEmitter.decorate(this);
+
+ this.debuggee = debuggee;
+ this._client = debuggee.client;
+ this._actor = form.actor;
+
+ this._onSourceLoad = this._onSourceLoad.bind(this);
+ this._onPropertyChange = this._onPropertyChange.bind(this);
+ this._onError = this._onError.bind(this);
+ this._onStyleApplied = this._onStyleApplied.bind(this);
+
+ this._client.addListener("sourceLoad-" + this._actor, this._onSourceLoad);
+ this._client.addListener("propertyChange-" + this._actor, this._onPropertyChange);
+ this._client.addListener("error-" + this._actor, this._onError);
+ this._client.addListener("styleApplied-" + this._actor, this._onStyleApplied);
+
+ // set initial property values
+ for (let attr in form) {
+ this[attr] = form[attr];
+ }
+}
+
+StyleSheet.prototype = {
+ /**
+ * Toggle the disabled attribute of the stylesheet
+ */
+ toggleDisabled: function() {
+ let message = { type: "toggleDisabled" };
+ this._sendRequest(message);
+ },
+
+ /**
+ * Request that the source of the stylesheet be fetched.
+ * 'source-load' event will be fired when it's been fetched.
+ */
+ fetchSource: function() {
+ let message = { type: "fetchSource" };
+ this._sendRequest(message);
+ },
+
+ /**
+ * Update the stylesheet in place with the given full source.
+ *
+ * @param {string} sheetText
+ * Full text to update the stylesheet with
+ */
+ update: function(sheetText) {
+ let message = { type: "update", text: sheetText, transition: true };
+ this._sendRequest(message);
+ },
+
+ /**
+ * Handle source load event from the client.
+ *
+ * @param {string} type
+ * Event type
+ * @param {object} request
+ * Event details
+ */
+ _onSourceLoad: function(type, request) {
+ this.emit("source-load", request.source);
+ },
+
+ /**
+ * Handle a property change on the stylesheet
+ *
+ * @param {string} type
+ * Event type
+ * @param {object} request
+ * Event details
+ */
+ _onPropertyChange: function(type, request) {
+ this[request.property] = request.value;
+ this.emit("property-change", request.property);
+ },
+
+ /**
+ * Propogate errors from the server that relate to this stylesheet.
+ *
+ * @param {string} type
+ * Event type
+ * @param {object} request
+ * Event details
+ */
+ _onError: function(type, request) {
+ this.emit("error", request.errorMessage);
+ },
+
+ /**
+ * Handle event when update has been successfully applied and propogate it.
+ */
+ _onStyleApplied: function() {
+ this.emit("style-applied");
+ },
+
+ /**
+ * Send a request to our actor on the server
+ *
+ * @param {object} message
+ * Message to send to the actor
+ * @param {function} callback
+ * Function to call with reponse from actor
+ */
+ _sendRequest: function(message, callback) {
+ message.to = this._actor;
+ this._client.request(message, callback);
+ },
+
+ /**
+ * Clean up and remove event listeners
+ */
+ destroy: function() {
+ this._client.removeListener("sourceLoad-" + this._actor, this._onSourceLoad);
+ this._client.removeListener("propertyChange-" + this._actor, this._onPropertyChange);
+ this._client.removeListener("error-" + this._actor, this._onError);
+ this._client.removeListener("styleApplied-" + this._actor, this._onStyleApplied);
+ }
+}
diff --git a/browser/devtools/styleeditor/StyleEditorPanel.jsm b/browser/devtools/styleeditor/StyleEditorPanel.jsm
new file mode 100644
index 000000000..783dfe84d
--- /dev/null
+++ b/browser/devtools/styleeditor/StyleEditorPanel.jsm
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+this.EXPORTED_SYMBOLS = ["StyleEditorPanel"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/StyleEditorDebuggee.jsm");
+Cu.import("resource:///modules/devtools/StyleEditorUI.jsm");
+Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorChrome",
+ "resource:///modules/devtools/StyleEditorChrome.jsm");
+
+this.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);
+}
+
+StyleEditorPanel.prototype = {
+ get target() this._toolbox.target,
+
+ get panelWindow() this._panelWin,
+
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ open: function() {
+ let deferred = Promise.defer();
+
+ let promise;
+ // We always interact with the target as if it were remote
+ if (!this.target.isRemote) {
+ promise = this.target.makeRemote();
+ } else {
+ promise = Promise.resolve(this.target);
+ }
+
+ promise.then(() => {
+ this.target.on("close", this.destroy);
+
+ this._debuggee = new StyleEditorDebuggee(this.target);
+
+ this.UI = new StyleEditorUI(this._debuggee, this._panelDoc);
+ this.UI.on("error", this._showError);
+
+ this.isReady = true;
+ deferred.resolve(this);
+ })
+
+ return deferred.promise;
+ },
+
+ /**
+ * Show an error message from the style editor in the toolbox
+ * notification box.
+ *
+ * @param {string} event
+ * Type of event
+ * @param {string} errorCode
+ * Error code of error to report
+ */
+ _showError: function(event, errorCode) {
+ let message = _(errorCode);
+ let notificationBox = this._toolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("styleeditor-error");
+ if (!notification) {
+ notificationBox.appendNotification(message,
+ "styleeditor-error", "", notificationBox.PRIORITY_CRITICAL_LOW);
+ }
+ },
+
+ /**
+ * 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
+ */
+ selectStyleSheet: function(href, line, col) {
+ if (!this._debuggee || !this.UI) {
+ return;
+ }
+ let stylesheet = this._debuggee.styleSheetFromHref(href);
+ this.UI.selectStyleSheet(href, line - 1, col - 1);
+ },
+
+ /**
+ * 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._panelDoc = null;
+
+ this._debuggee.destroy();
+ this.UI.destroy();
+ }
+
+ return Promise.resolve(null);
+ },
+}
+
+XPCOMUtils.defineLazyGetter(StyleEditorPanel.prototype, "strings",
+ function () {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/styleeditor.properties");
+ });
diff --git a/browser/devtools/styleeditor/StyleEditorUI.jsm b/browser/devtools/styleeditor/StyleEditorUI.jsm
new file mode 100644
index 000000000..d6d41d1fc
--- /dev/null
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -0,0 +1,460 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PluralForm.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
+Cu.import("resource:///modules/devtools/SplitView.jsm");
+Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
+
+
+const LOAD_ERROR = "error-load";
+
+const STYLE_EDITOR_TEMPLATE = "stylesheet";
+
+/**
+ * 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 {StyleEditorDebuggee} debuggee
+ * Debuggee of whose stylesheets should be shown in the UI
+ * @param {Document} panelDoc
+ * Document of the toolbox panel to populate UI in.
+ */
+function StyleEditorUI(debuggee, panelDoc) {
+ EventEmitter.decorate(this);
+
+ this._debuggee = debuggee;
+ this._panelDoc = panelDoc;
+ this._window = this._panelDoc.defaultView;
+ this._root = this._panelDoc.getElementById("style-editor-chrome");
+
+ this.editors = [];
+ this.selectedEditor = null;
+
+ this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
+ this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
+ this._onDocumentLoad = this._onDocumentLoad.bind(this);
+ this._onError = this._onError.bind(this);
+
+ debuggee.on("document-load", this._onDocumentLoad);
+ debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
+
+ this.createUI();
+}
+
+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.dirty;
+ });
+ },
+
+ /*
+ * 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;
+ },
+
+ /**
+ * 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", function onNew() {
+ this._debuggee.createStyleSheet(null, this._onStyleSheetCreated);
+ }.bind(this));
+
+ wire(this._view.rootElement, ".style-editor-importButton", function onImport() {
+ this._importFromFile(this._mockImportFile || null, this._window);
+ }.bind(this));
+ },
+
+ /**
+ * 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 = function(file) {
+ if (!file) {
+ this.emit("error", LOAD_ERROR);
+ return;
+ }
+ NetUtil.asyncFetch(file, (stream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ this.emit("error", LOAD_ERROR);
+ return;
+ }
+ let source = NetUtil.readInputStreamToString(stream, stream.available());
+ stream.close();
+
+ this._debuggee.createStyleSheet(source, (styleSheet) => {
+ this._onStyleSheetCreated(styleSheet, file);
+ });
+ });
+
+ }.bind(this);
+
+ showFilePicker(file, false, parentWindow, onFileSelected);
+ },
+
+ /**
+ * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
+ */
+ _onStyleSheetsCleared: function() {
+ // remember selected sheet and line number for next load
+ if (this.selectedEditor && this.selectedEditor.sourceEditor) {
+ let href = this.selectedEditor.styleSheet.href;
+ let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
+ this.selectStyleSheet(href, line, col);
+ }
+
+ this._clearStyleSheetEditors();
+ this._view.removeAll();
+
+ this.selectedEditor = null;
+
+ this._root.classList.add("loading");
+ },
+
+ /**
+ * 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);
+ },
+
+ /**
+ * Handler for debuggee's 'document-load' event. Add editors
+ * for all style sheets in the document
+ *
+ * @param {string} event
+ * Event name
+ * @param {StyleSheet} styleSheet
+ * StyleSheet object for new sheet
+ */
+ _onDocumentLoad: function(event, styleSheets) {
+ if (this._styleSheetToSelect) {
+ // if selected stylesheet from previous load isn't here,
+ // just set first stylesheet to be selected instead
+ let selectedExists = styleSheets.some((sheet) => {
+ return this._styleSheetToSelect.href == sheet.href;
+ })
+ if (!selectedExists) {
+ this._styleSheetToSelect = null;
+ }
+ }
+ for (let sheet of styleSheets) {
+ this._addStyleSheetEditor(sheet);
+ }
+
+ this._root.classList.remove("loading");
+
+ this.emit("document-load");
+ },
+
+ /**
+ * Forward any error from a stylesheet.
+ *
+ * @param {string} event
+ * Event name
+ * @param {string} errorCode
+ * Code represeting type of error
+ */
+ _onError: function(event, errorCode) {
+ this.emit("error", errorCode);
+ },
+
+ /**
+ * Add a new editor to the UI for a stylesheet.
+ *
+ * @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
+ */
+ _addStyleSheetEditor: function(styleSheet, file, isNew) {
+ let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew);
+
+ editor.once("source-load", this._sourceLoaded.bind(this, editor));
+ editor.on("property-change", this._summaryChange.bind(this, editor));
+ editor.on("style-applied", this._summaryChange.bind(this, editor));
+ editor.on("error", this._onError);
+
+ this.editors.push(editor);
+
+ // Queue editor loading. This helps responsivity during loading when
+ // there are many heavy stylesheets.
+ this._window.setTimeout(editor.fetchSource.bind(editor), 0);
+ },
+
+ /**
+ * Clear all the editors from the UI.
+ */
+ _clearStyleSheetEditors: function() {
+ for (let editor of this.editors) {
+ editor.destroy();
+ }
+ this.editors = [];
+ },
+
+ /**
+ * Handler for an StyleSheetEditor's 'source-load' event.
+ * Create a summary UI for the editor.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to create UI for.
+ */
+ _sourceLoaded: function(editor) {
+ // add new sidebar item and editor to the UI
+ this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
+ data: {
+ editor: editor
+ },
+ disableAnimations: this._alwaysDisableAnimations,
+ ordinal: editor.styleSheet.styleSheetIndex,
+ onCreate: function(summary, details, data) {
+ let editor = data.editor;
+ editor.summary = summary;
+
+ wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
+ event.stopPropagation();
+ event.target.blur();
+
+ editor.toggleDisabled();
+ });
+
+ wire(summary, ".stylesheet-name", {
+ events: {
+ "keypress": function onStylesheetNameActivate(aEvent) {
+ if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+ this._view.activeSummary = summary;
+ }
+ }.bind(this)
+ }
+ });
+
+ wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
+ event.stopPropagation();
+ event.target.blur();
+
+ editor.saveToFile(editor.savedFile);
+ });
+
+ this._updateSummaryForEditor(editor, summary);
+
+ summary.addEventListener("focus", function onSummaryFocus(event) {
+ if (event.target == summary) {
+ // autofocus the stylesheet name
+ summary.querySelector(".stylesheet-name").focus();
+ }
+ }, false);
+
+ // autofocus if it's a new user-created stylesheet
+ if (editor.isNew) {
+ this._selectEditor(editor);
+ }
+
+ if (this._styleSheetToSelect
+ && this._styleSheetToSelect.href == editor.styleSheet.href) {
+ this.switchToSelectedSheet();
+ }
+
+ // If this is the first stylesheet, select it
+ if (this.selectedStyleSheetIndex == -1
+ && !this._styleSheetToSelect
+ && editor.styleSheet.styleSheetIndex == 0) {
+ this._selectEditor(editor);
+ }
+
+ this.emit("editor-added", editor);
+ }.bind(this),
+
+ onShow: function(summary, details, data) {
+ let editor = data.editor;
+ this.selectedEditor = editor;
+ this._styleSheetToSelect = null;
+
+ if (!editor.sourceEditor) {
+ // only initialize source editor when we switch to this view
+ let inputElement = details.querySelector(".stylesheet-editor-input");
+ editor.load(inputElement);
+ }
+ editor.onShow();
+
+ this.emit("editor-selected", editor);
+ }.bind(this)
+ });
+ },
+
+ /**
+ * Switch to the editor that has been marked to be selected.
+ */
+ switchToSelectedSheet: function() {
+ let sheet = this._styleSheetToSelect;
+
+ for each (let editor in this.editors) {
+ if (editor.styleSheet.href == sheet.href) {
+ this._selectEditor(editor, sheet.line, sheet.col);
+ break;
+ }
+ }
+ },
+
+ /**
+ * 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
+ */
+ _selectEditor: function(editor, line, col) {
+ line = line || 0;
+ col = col || 0;
+
+ editor.getSourceEditor().then(() => {
+ editor.sourceEditor.setCaretPosition(line, col);
+ });
+
+ this._view.activeSummary = editor.summary;
+ },
+
+ /**
+ * selects a stylesheet and optionally moves the cursor to a selected line
+ *
+ * @param {string} [href]
+ * Href of stylesheet that should be selected. If a stylesheet is not passed
+ * and the editor is not initialized we focus the first stylesheet. If
+ * a stylesheet is not passed and the editor is initialized we ignore
+ * the call.
+ * @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).
+ */
+ selectStyleSheet: function(href, line, col)
+ {
+ let alreadyCalled = !!this._styleSheetToSelect;
+
+ this._styleSheetToSelect = {
+ href: href,
+ line: line,
+ col: col,
+ };
+
+ if (alreadyCalled) {
+ return;
+ }
+
+ /* Switch to the editor for this sheet, if it exists yet.
+ Otherwise each editor will be checked when it's created. */
+ 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 = "-";
+ if (editor.styleSheet.ruleCount !== undefined) {
+ ruleCount = editor.styleSheet.ruleCount;
+ }
+
+ var flags = [];
+ if (editor.styleSheet.disabled) {
+ flags.push("disabled");
+ }
+ if (editor.unsaved) {
+ flags.push("unsaved");
+ }
+ this._view.setItemClassName(summary, flags.join(" "));
+
+ let label = summary.querySelector(".stylesheet-name > label");
+ label.setAttribute("value", editor.friendlyName);
+
+ text(summary, ".stylesheet-title", editor.styleSheet.title || "");
+ text(summary, ".stylesheet-rule-count",
+ PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
+ text(summary, ".stylesheet-error-message", editor.errorMessage);
+ },
+
+ destroy: function() {
+ this._clearStyleSheetEditors();
+
+ this._debuggee.off("document-load", this._onDocumentLoad);
+ this._debuggee.off("stylesheets-cleared", this._onStyleSheetsCleared);
+ }
+}
diff --git a/browser/devtools/styleeditor/StyleEditorUtil.jsm b/browser/devtools/styleeditor/StyleEditorUtil.jsm
new file mode 100644
index 000000000..f6df3953a
--- /dev/null
+++ b/browser/devtools/styleeditor/StyleEditorUtil.jsm
@@ -0,0 +1,221 @@
+/* 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 = [
+ "_",
+ "assert",
+ "log",
+ "text",
+ "wire",
+ "showFilePicker"
+];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PROPERTIES_URL = "chrome://browser/locale/devtools/styleeditor.properties";
+
+const console = Services.console;
+const gStringBundle = Services.strings.createBundle(PROPERTIES_URL);
+
+
+/**
+ * Returns a localized string with the given key name from the string bundle.
+ *
+ * @param aName
+ * @param ...rest
+ * Optional arguments to format in the string.
+ * @return string
+ */
+this._ = function _(aName)
+{
+
+ if (arguments.length == 1) {
+ return gStringBundle.GetStringFromName(aName);
+ }
+ let rest = Array.prototype.slice.call(arguments, 1);
+ return gStringBundle.formatStringFromName(aName, rest, rest.length);
+}
+
+/**
+ * Assert an expression is true or throw if false.
+ *
+ * @param aExpression
+ * @param aMessage
+ * Optional message.
+ * @return aExpression
+ */
+this.assert = function assert(aExpression, aMessage)
+{
+ if (!!!(aExpression)) {
+ let msg = aMessage ? "ASSERTION FAILURE:" + aMessage : "ASSERTION FAILURE";
+ log(msg);
+ throw new Error(msg);
+ }
+ return aExpression;
+}
+
+/**
+ * Retrieve or set the text content of an element.
+ *
+ * @param DOMElement aRoot
+ * The element to use for querySelector.
+ * @param string aSelector
+ * Selector string for the element to get/set the text content.
+ * @param string aText
+ * Optional text to set.
+ * @return string
+ * Text content of matching element or null if there were no element
+ * matching aSelector.
+ */
+this.text = function text(aRoot, aSelector, aText)
+{
+ let element = aRoot.querySelector(aSelector);
+ if (!element) {
+ return null;
+ }
+
+ if (aText === undefined) {
+ return element.textContent;
+ }
+ element.textContent = aText;
+ return aText;
+}
+
+/**
+ * Iterates _own_ properties of an object.
+ *
+ * @param aObject
+ * The object to iterate.
+ * @param function aCallback(aKey, aValue)
+ */
+function forEach(aObject, aCallback)
+{
+ for (let key in aObject) {
+ if (aObject.hasOwnProperty(key)) {
+ aCallback(key, aObject[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.
+ */
+this.log = function log()
+{
+ console.logStringMessage(Array.prototype.slice.call(arguments).join(" "));
+}
+
+/**
+ * Wire up element(s) matching selector with attributes, event listeners, etc.
+ *
+ * @param DOMElement aRoot
+ * The element to use for querySelectorAll.
+ * Can be null if aSelector is a DOMElement.
+ * @param string|DOMElement aSelectorOrElement
+ * Selector string or DOMElement for the element(s) to wire up.
+ * @param object aDescriptor
+ * 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 aDescriptor is a function, the argument is equivalent to :
+ * {events: {'click': aDescriptor}}
+ */
+this.wire = function wire(aRoot, aSelectorOrElement, aDescriptor)
+{
+ let matches;
+ if (typeof(aSelectorOrElement) == "string") { // selector
+ matches = aRoot.querySelectorAll(aSelectorOrElement);
+ if (!matches.length) {
+ return;
+ }
+ } else {
+ matches = [aSelectorOrElement]; // element
+ }
+
+ if (typeof(aDescriptor) == "function") {
+ aDescriptor = {events: {click: aDescriptor}};
+ }
+
+ for (let i = 0; i < matches.length; i++) {
+ let element = matches[i];
+ forEach(aDescriptor.events, function (aName, aHandler) {
+ element.addEventListener(aName, aHandler, false);
+ });
+ forEach(aDescriptor.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.
+ */
+this.showFilePicker = function showFilePicker(path, toSave, parentWindow, callback)
+{
+ 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);
+ }
+ };
+
+ fp.init(parentWindow, _(key + ".title"), mode);
+ fp.appendFilters(_(key + ".filter"), "*.css");
+ fp.appendFilters(fp.filterAll);
+ fp.open(fpCallback);
+ return;
+}
diff --git a/browser/devtools/styleeditor/StyleSheetEditor.jsm b/browser/devtools/styleeditor/StyleSheetEditor.jsm
new file mode 100644
index 000000000..0ede23446
--- /dev/null
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -0,0 +1,548 @@
+/* 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;
+
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
+
+
+const SAVE_ERROR = "error-save";
+
+// max update frequency in ms (avoid potential typing lag and/or flicker)
+// @see StyleEditor.updateStylesheet
+const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
+
+/**
+ * StyleSheetEditor controls the editor linked to a particular StyleSheet
+ * object.
+ *
+ * Emits events:
+ * 'source-load': The source of the stylesheet has been fetched
+ * '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} styleSheet
+ * @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
+ */
+function StyleSheetEditor(styleSheet, win, file, isNew) {
+ EventEmitter.decorate(this);
+
+ this.styleSheet = styleSheet;
+ this._inputElement = null;
+ this._sourceEditor = null;
+ this._window = win;
+ this._isNew = isNew;
+ this.savedFile = file;
+
+ this.errorMessage = null;
+
+ this._state = { // state to use when inputElement attaches
+ text: "",
+ selection: {start: 0, end: 0},
+ readOnly: false,
+ topIndex: 0, // the first visible line
+ };
+
+ this._styleSheetFilePath = null;
+ if (styleSheet.href &&
+ Services.io.extractScheme(this.styleSheet.href) == "file") {
+ this._styleSheetFilePath = this.styleSheet.href;
+ }
+
+ this._onSourceLoad = this._onSourceLoad.bind(this);
+ this._onPropertyChange = this._onPropertyChange.bind(this);
+ this._onError = this._onError.bind(this);
+
+ this._focusOnSourceEditorReady = false;
+
+ this.styleSheet.once("source-load", this._onSourceLoad);
+ this.styleSheet.on("property-change", this._onPropertyChange);
+ this.styleSheet.on("error", this._onError);
+}
+
+StyleSheetEditor.prototype = {
+ /**
+ * This editor's source editor
+ */
+ get sourceEditor() {
+ return this._sourceEditor;
+ },
+
+ /**
+ * Whether there are unsaved changes in the editor
+ */
+ get unsaved() {
+ return this._sourceEditor && this._sourceEditor.dirty;
+ },
+
+ /**
+ * Whether the editor is for a stylesheet created by the user
+ * through the style editor UI.
+ */
+ get isNew() {
+ return this._isNew;
+ },
+
+ /**
+ * Get a user-friendly name for the style sheet.
+ *
+ * @return string
+ */
+ get friendlyName() {
+ if (this.savedFile) { // reuse the saved filename if any
+ return this.savedFile.leafName;
+ }
+
+ if (this._isNew) {
+ let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
+ return _("newStyleSheet", index);
+ }
+
+ if (!this.styleSheet.href) {
+ let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
+ return _("inlineStyleSheet", index);
+ }
+
+ if (!this._friendlyName) {
+ let sheetURI = this.styleSheet.href;
+ let contentURI = this.styleSheet.debuggee.baseURI;
+ let contentURIScheme = contentURI.scheme;
+ let contentURILeafIndex = contentURI.specIgnoringRef.lastIndexOf("/");
+ contentURI = contentURI.specIgnoringRef;
+
+ // get content base URI without leaf name (if any)
+ if (contentURILeafIndex > contentURIScheme.length) {
+ contentURI = contentURI.substring(0, contentURILeafIndex + 1);
+ }
+
+ // avoid verbose repetition of absolute URI when the style sheet URI
+ // is relative to the content URI
+ this._friendlyName = (sheetURI.indexOf(contentURI) == 0)
+ ? sheetURI.substring(contentURI.length)
+ : sheetURI;
+ try {
+ this._friendlyName = decodeURI(this._friendlyName);
+ } catch (ex) {
+ }
+ }
+ return this._friendlyName;
+ },
+
+ /**
+ * Start fetching the full text source for this editor's sheet.
+ */
+ fetchSource: function() {
+ this.styleSheet.fetchSource();
+ },
+
+ /**
+ * Handle source fetched event. Forward source-load event.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} source
+ * Full-text source of the stylesheet
+ */
+ _onSourceLoad: function(event, source) {
+ this._state.text = prettifyCSS(source);
+ this.sourceLoaded = true;
+ this.emit("source-load");
+ },
+
+ /**
+ * Forward property-change event from stylesheet.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} property
+ * Property that has changed on sheet
+ */
+ _onPropertyChange: function(event, property) {
+ this.emit("property-change", property);
+ },
+
+ /**
+ * Forward error event from stylesheet.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} errorCode
+ */
+ _onError: function(event, errorCode) {
+ this.emit("error", errorCode);
+ },
+
+ /**
+ * Create source editor and load state into it.
+ * @param {DOMElement} inputElement
+ * Element to load source editor in
+ */
+ load: function(inputElement) {
+ this._inputElement = inputElement;
+
+ let sourceEditor = new SourceEditor();
+ let config = {
+ initialText: this._state.text,
+ showLineNumbers: true,
+ mode: SourceEditor.MODES.CSS,
+ readOnly: this._state.readOnly,
+ keys: this._getKeyBindings()
+ };
+
+ sourceEditor.init(inputElement, config, function onSourceEditorReady() {
+ setupBracketCompletion(sourceEditor);
+ sourceEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
+ function onTextChanged(event) {
+ this.updateStyleSheet();
+ }.bind(this));
+
+ this._sourceEditor = sourceEditor;
+
+ if (this._focusOnSourceEditorReady) {
+ this._focusOnSourceEditorReady = false;
+ sourceEditor.focus();
+ }
+
+ sourceEditor.setTopIndex(this._state.topIndex);
+ sourceEditor.setSelection(this._state.selection.start,
+ this._state.selection.end);
+
+ this.emit("source-editor-load");
+ }.bind(this));
+
+ sourceEditor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onPropertyChange);
+ },
+
+ /**
+ * Get the source editor for this editor.
+ *
+ * @return {Promise}
+ * Promise that will resolve with the editor.
+ */
+ getSourceEditor: function() {
+ let deferred = Promise.defer();
+
+ if (this.sourceEditor) {
+ return Promise.resolve(this);
+ }
+ this.on("source-editor-load", (event) => {
+ 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) {
+ this._sourceEditor.setTopIndex(this._state.topIndex);
+ }
+ this.focus();
+ },
+
+ /**
+ * Toggled the disabled state of the underlying stylesheet.
+ */
+ toggleDisabled: function() {
+ this.styleSheet.toggleDisabled();
+ },
+
+ /**
+ * Queue a throttled task to update the live style sheet.
+ *
+ * @param boolean immediate
+ * Optional. If true the update is performed immediately.
+ */
+ updateStyleSheet: function(immediate) {
+ if (this._updateTask) {
+ // cancel previous queued task not executed within throttle delay
+ this._window.clearTimeout(this._updateTask);
+ }
+
+ if (immediate) {
+ this._updateStyleSheet();
+ } else {
+ this._updateTask = this._window.setTimeout(this._updateStyleSheet.bind(this),
+ UPDATE_STYLESHEET_THROTTLE_DELAY);
+ }
+ },
+
+ /**
+ * Update live style sheet according to modifications.
+ */
+ _updateStyleSheet: function() {
+ if (this.styleSheet.disabled) {
+ return; // TODO: do we want to do this?
+ }
+
+ this._updateTask = null; // 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
+
+ if (this.sourceEditor) {
+ this._state.text = this.sourceEditor.getText();
+ }
+
+ this.styleSheet.update(this._state.text);
+ },
+
+ /**
+ * 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, function onStreamCopied(status) {
+ if (!Components.isSuccessCode(status)) {
+ if (callback) {
+ callback(null);
+ }
+ this.emit("error", SAVE_ERROR);
+ return;
+ }
+ FileUtils.closeSafeFileOutputStream(ostream);
+ // remember filename for next save if any
+ this._friendlyName = null;
+ this.savedFile = returnFile;
+
+ if (callback) {
+ callback(returnFile);
+ }
+ this.sourceEditor.dirty = false;
+ }.bind(this));
+ };
+
+ showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile);
+ },
+
+ /**
+ * Retrieve custom key bindings objects as expected by SourceEditor.
+ * SourceEditor action names are not displayed to the user.
+ *
+ * @return {array} key binding objects for the source editor
+ */
+ _getKeyBindings: function() {
+ let bindings = [];
+
+ bindings.push({
+ action: "StyleEditor.save",
+ code: _("saveStyleSheet.commandkey"),
+ accel: true,
+ callback: function save() {
+ this.saveToFile(this.savedFile);
+ return true;
+ }.bind(this)
+ });
+
+ bindings.push({
+ action: "StyleEditor.saveAs",
+ code: _("saveStyleSheet.commandkey"),
+ accel: true,
+ shift: true,
+ callback: function saveAs() {
+ this.saveToFile();
+ return true;
+ }.bind(this)
+ });
+
+ return bindings;
+ },
+
+ /**
+ * Clean up for this editor.
+ */
+ destroy: function() {
+ this.styleSheet.off("source-load", this._onSourceLoad);
+ this.styleSheet.off("property-change", this._onPropertyChange);
+ this.styleSheet.off("error", this._onError);
+ }
+}
+
+
+const TAB_CHARS = "\t";
+
+const OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+const LINE_SEPARATOR = OS === "WINNT" ? "\r\n" : "\n";
+
+/**
+ * Return string that repeats text for aCount times.
+ *
+ * @param string text
+ * @param number aCount
+ * @return string
+ */
+function repeat(text, aCount)
+{
+ return (new Array(aCount + 1)).join(text);
+}
+
+/**
+ * Prettify minified CSS text.
+ * This prettifies CSS code where there is no indentation in usual places while
+ * keeping original indentation as-is elsewhere.
+ *
+ * @param string text
+ * The CSS source to prettify.
+ * @return string
+ * Prettified CSS source
+ */
+function prettifyCSS(text)
+{
+ // remove initial and terminating HTML comments and surrounding whitespace
+ text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
+
+ let parts = []; // indented parts
+ let partStart = 0; // start offset of currently parsed part
+ let indent = "";
+ let indentLevel = 0;
+
+ for (let i = 0; i < text.length; i++) {
+ let c = text[i];
+ let shouldIndent = false;
+
+ switch (c) {
+ case "}":
+ if (i - partStart > 1) {
+ // there's more than just } on the line, add line
+ parts.push(indent + text.substring(partStart, i));
+ partStart = i;
+ }
+ indent = repeat(TAB_CHARS, --indentLevel);
+ /* fallthrough */
+ case ";":
+ case "{":
+ shouldIndent = true;
+ break;
+ }
+
+ if (shouldIndent) {
+ let la = text[i+1]; // one-character lookahead
+ if (!/\s/.test(la)) {
+ // following character should be a new line (or whitespace) but it isn't
+ // force indentation then
+ parts.push(indent + text.substring(partStart, i + 1));
+ if (c == "}") {
+ parts.push(""); // for extra line separator
+ }
+ partStart = i + 1;
+ } else {
+ return text; // assume it is not minified, early exit
+ }
+ }
+
+ if (c == "{") {
+ indent = repeat(TAB_CHARS, ++indentLevel);
+ }
+ }
+ return parts.join(LINE_SEPARATOR);
+}
+
+
+/**
+ * Set up bracket completion on a given SourceEditor.
+ * This automatically closes the following CSS brackets: "{", "(", "["
+ *
+ * @param SourceEditor sourceEditor
+ */
+function setupBracketCompletion(sourceEditor)
+{
+ let editorElement = sourceEditor.editorElement;
+ let pairs = {
+ 123: { // {
+ closeString: "}",
+ closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
+ },
+ 40: { // (
+ closeString: ")",
+ closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_0
+ },
+ 91: { // [
+ closeString: "]",
+ closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
+ },
+ };
+
+ editorElement.addEventListener("keypress", function onKeyPress(event) {
+ let pair = pairs[event.charCode];
+ if (!pair || event.ctrlKey || event.metaKey ||
+ event.accelKey || event.altKey) {
+ return true;
+ }
+
+ // We detected an open bracket, sending closing character
+ let keyCode = pair.closeKeyCode;
+ let charCode = pair.closeString.charCodeAt(0);
+ let modifiers = 0;
+ let utils = editorElement.ownerDocument.defaultView.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ let handled = utils.sendKeyEvent("keydown", keyCode, 0, modifiers);
+ utils.sendKeyEvent("keypress", 0, charCode, modifiers, !handled);
+ utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
+ // and rewind caret
+ sourceEditor.setCaretOffset(sourceEditor.getCaretOffset() - 1);
+ }, false);
+}
+
diff --git a/browser/devtools/styleeditor/moz.build b/browser/devtools/styleeditor/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/styleeditor/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/styleeditor/styleeditor.css b/browser/devtools/styleeditor/styleeditor.css
new file mode 100644
index 000000000..7ff8c9905
--- /dev/null
+++ b/browser/devtools/styleeditor/styleeditor.css
@@ -0,0 +1,89 @@
+/* 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/. */
+
+.stylesheet-error-message {
+ display: none;
+}
+
+li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message {
+ display: block;
+}
+
+.splitview-nav > li,
+.stylesheet-info,
+.stylesheet-more {
+ display: -moz-box;
+}
+
+.splitview-nav > li {
+ -moz-box-orient: horizontal;
+}
+
+.splitview-nav > li > hgroup {
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.stylesheet-info > h1 {
+ -moz-box-flex: 1;
+}
+
+.stylesheet-name > label {
+ display: inline;
+ cursor: pointer;
+}
+
+.splitview-nav > li > hgroup.stylesheet-info {
+ -moz-box-pack: center;
+}
+
+.stylesheet-name {
+ white-space: nowrap;
+}
+
+li.unsaved > hgroup > h1 > .stylesheet-name:before {
+ content: "*";
+}
+
+.stylesheet-enabled {
+ display: -moz-box;
+}
+
+.stylesheet-saveButton {
+ display: none;
+}
+
+.stylesheet-rule-count,
+li.splitview-active > hgroup > .stylesheet-more > h3 > .stylesheet-saveButton,
+li:hover > hgroup > .stylesheet-more > h3 > .stylesheet-saveButton {
+ display: -moz-box;
+}
+
+.stylesheet-more > spacer {
+ -moz-box-flex: 1;
+}
+
+/* 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;
+ }
+
+ .stylesheet-more {
+ -moz-box-flex: 1;
+ -moz-box-pack: end;
+ }
+
+ .splitview-nav > li > hgroup.stylesheet-info {
+ -moz-box-orient: horizontal;
+ -moz-box-flex: 1;
+ }
+
+ .stylesheet-more > spacer {
+ -moz-box-flex: 0;
+ }
+}
diff --git a/browser/devtools/styleeditor/styleeditor.xul b/browser/devtools/styleeditor/styleeditor.xul
new file mode 100644
index 000000000..861115bad
--- /dev/null
+++ b/browser/devtools/styleeditor/styleeditor.xul
@@ -0,0 +1,104 @@
+<?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://browser/locale/devtools/styleeditor.dtd" >
+ %styleEditorDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/splitview.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/splitview.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/styleeditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/styleeditor.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/devtools/source-editor-overlay.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">
+
+ <xul:script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <xul:popupset id="style-editor-popups">
+ <xul:menupopup id="sourceEditorContextMenu"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <xul:menuitem id="se-cMenu-undo"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-cMenu-cut"/>
+ <xul:menuitem id="se-cMenu-copy"/>
+ <xul:menuitem id="se-cMenu-paste"/>
+ <xul:menuitem id="se-cMenu-delete"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-cMenu-selectAll"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-cMenu-find"/>
+ <xul:menuitem id="se-cMenu-findAgain"/>
+ <xul:menuseparator/>
+ <xul:menuitem id="se-cMenu-gotoLine"/>
+ </xul:menupopup>
+ </xul:popupset>
+
+ <xul:commandset id="editMenuCommands"/>
+ <xul:commandset id="sourceEditorCommands"/>
+
+ <xul:keyset id="sourceEditorKeys"/>
+
+ <xul:box id="style-editor-chrome" class="splitview-root loading">
+ <xul:box class="splitview-controller">
+ <xul:box class="splitview-main">
+ <xul:toolbar class="devtools-toolbar">
+ <xul:toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
+ accesskey="&newButton.accesskey;"
+ tooltiptext="&newButton.tooltip;"
+ label="&newButton.label;"/>
+ <xul:toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
+ accesskey="&importButton.accesskey;"
+ tooltiptext="&importButton.tooltip;"
+ label="&importButton.label;"/>
+ </xul:toolbar>
+ </xul:box>
+ <xul:box id="splitview-resizer-target" class="splitview-nav-container"
+ persist="height">
+ <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 splitview-landscape-splitter"/>
+ <xul:box class="splitview-side-details"/>
+
+ <div id="splitview-templates" hidden="true">
+ <li id="splitview-tpl-summary-stylesheet" tabindex="0">
+ <a class="stylesheet-enabled" tabindex="0" href="#"
+ title="&visibilityToggle.tooltip;"
+ accesskey="&saveButton.accesskey;"></a>
+ <hgroup class="stylesheet-info">
+ <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1>
+ <div class="stylesheet-more">
+ <h3 class="stylesheet-title"></h3>
+ <h3 class="stylesheet-rule-count"></h3>
+ <h3 class="stylesheet-error-message"></h3>
+ <xul:spacer/>
+ <h3><a class="stylesheet-saveButton"
+ title="&saveButton.tooltip;"
+ accesskey="&saveButton.accesskey;">&saveButton.label;</a></h3>
+ </div>
+ </hgroup>
+ </li>
+
+ <xul:box id="splitview-tpl-details-stylesheet" class="splitview-details">
+ <xul:resizer class="splitview-portrait-resizer"
+ dir="bottom"
+ element="splitview-resizer-target"/>
+ <xul:box class="stylesheet-editor-input textbox"
+ data-placeholder="&editorTextbox.placeholder;"/>
+ </xul:box>
+ </div> <!-- #splitview-templates -->
+ </xul:box> <!-- .splitview-root -->
+</xul:window>
diff --git a/browser/devtools/styleeditor/test/Makefile.in b/browser/devtools/styleeditor/test/Makefile.in
new file mode 100644
index 000000000..1918c406a
--- /dev/null
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES := \
+ browser_styleeditor_enabled.js \
+ browser_styleeditor_filesave.js \
+ browser_styleeditor_cmd_edit.js \
+ browser_styleeditor_cmd_edit.html \
+ browser_styleeditor_import.js \
+ browser_styleeditor_import_rule.js \
+ browser_styleeditor_init.js \
+ browser_styleeditor_loading.js \
+ browser_styleeditor_new.js \
+ browser_styleeditor_pretty.js \
+ browser_styleeditor_private_perwindowpb.js \
+ browser_styleeditor_sv_keynav.js \
+ browser_styleeditor_sv_resize.js \
+ browser_styleeditor_bug_740541_iframes.js \
+ browser_styleeditor_bug_851132_middle_click.js \
+ browser_styleeditor_bug_870339.js \
+ browser_styleeditor_nostyle.js \
+ browser_styleeditor_reload.js \
+ head.js \
+ four.html \
+ head.js \
+ import.css \
+ import.html \
+ import2.css \
+ longload.html \
+ media.html \
+ media-small.css \
+ minified.html \
+ nostyle.html \
+ resources_inpage.jsi \
+ resources_inpage1.css \
+ resources_inpage2.css \
+ simple.css \
+ simple.css.gz \
+ simple.css.gz^headers^ \
+ simple.gz.html \
+ simple.html \
+ test_private.html \
+ test_private.css \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_740541_iframes.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
new file mode 100644
index 000000000..f438bc454
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+
+ 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;
+
+ waitForExplicitFinish();
+ let styleSheetCount = 0;
+ addTabAndOpenStyleEditor(function (aPanel) {
+ aPanel.UI.on("editor-added", function () {
+ ++styleSheetCount;
+ info(styleSheetCount+" out of "+
+ EXPECTED_STYLE_SHEET_COUNT+" style sheets loaded");
+ if (styleSheetCount == EXPECTED_STYLE_SHEET_COUNT) {
+ ok(true, "all style sheets loaded");
+ // The right number of events have been received; check that
+ // they actually show up in the style editor UI.
+ is(aPanel.UI.editors.length, EXPECTED_STYLE_SHEET_COUNT,
+ "UI elements present");
+ finish();
+ }
+ });
+ });
+ content.location = TESTCASE_URI;
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
new file mode 100644
index 000000000..b196339a2
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TESTCASE_URI = TEST_BASE + "four.html";
+
+let gUI;
+
+function test() {
+ waitForExplicitFinish();
+
+ let count = 0;
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", function(event, editor) {
+ count++;
+ if (count == 2) {
+ runTests();
+ }
+ })
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+let timeoutID;
+
+function runTests() {
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded, false);
+ gUI.editors[0].getSourceEditor().then(onEditor0Attach);
+ gUI.editors[1].getSourceEditor().then(onEditor1Attach);
+}
+
+function getStylesheetNameLinkFor(aEditor) {
+ return aEditor.summary.querySelector(".stylesheet-name");
+}
+
+function onEditor0Attach(aEditor) {
+ waitForFocus(function () {
+ // left mouse click should focus editor 1
+ EventUtils.synthesizeMouseAtCenter(
+ getStylesheetNameLinkFor(gUI.editors[1]),
+ {button: 0},
+ gPanelWindow);
+ }, gPanelWindow);
+}
+
+function onEditor1Attach(aEditor) {
+ ok(aEditor.sourceEditor.hasFocus(),
+ "left mouse click has given editor 1 focus");
+
+ // right mouse click should not open a new tab
+ EventUtils.synthesizeMouseAtCenter(
+ getStylesheetNameLinkFor(gUI.editors[2]),
+ {button: 1},
+ gPanelWindow);
+
+ setTimeout(finish, 0);
+}
+
+function onTabAdded() {
+ ok(false, "middle mouse click has opened a new tab");
+ finish();
+}
+
+registerCleanupFunction(function () {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabAdded, false);
+ gUI = null;
+});
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js
new file mode 100644
index 000000000..6daa0f4f8
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ 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"));
+
+ waitForExplicitFinish();
+ addTabAndOpenStyleEditor(function (aPanel) {
+ let debuggee = aPanel._debuggee;
+
+ // 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) {
+ debuggee._onNewDocument();
+ }
+
+ // Wait for the StyleEditorActor to respond to each "newDocument"
+ // message.
+ let loadCount = 0;
+ debuggee.on("document-load", function () {
+ ++loadCount;
+ if (loadCount == SPAM_COUNT) {
+ // 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(debuggee.styleSheets.length, 1, "correct style sheet count");
+ finish();
+ }
+ });
+ });
+ content.location = DOCUMENT_WITH_ONE_STYLESHEET;
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.html b/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.html
new file mode 100644
index 000000000..28fccb331
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.html
@@ -0,0 +1,50 @@
+<!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" id="script1">
+ window.addEventListener('load', function() {
+ var pid = document.getElementById('pid');
+ var 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>
+ var pid = document.getElementById('pid');
+ var 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/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js b/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js
new file mode 100644
index 000000000..f64e96864
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the edit command works
+
+const TEST_URI = "http://example.com/browser/browser/devtools/styleeditor/" +
+ "test/browser_styleeditor_cmd_edit.html";
+
+
+function test() {
+ let windowClosed = Promise.defer();
+
+ helpers.addTabWithToolbar(TEST_URI, function(options) {
+ return 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/browser/devtools/styleeditor/test/resources_inpage1.css [line]',
+ markup: 'VVVVVIIII',
+ status: 'ERROR',
+ args: {
+ resource: { arg: ' http', status: 'INCOMPLETE', message: '' },
+ line: { status: 'VALID' },
+ }
+ },
+ },
+ {
+ setup: "edit page1",
+ check: {
+ input: 'edit page1',
+ hints: ' [line] -> http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage1.css',
+ markup: 'VVVVVIIIII',
+ status: 'ERROR',
+ args: {
+ resource: { arg: ' page1', status: 'INCOMPLETE', message: '' },
+ line: { status: 'VALID' },
+ }
+ },
+ },
+ {
+ setup: "edit page2",
+ check: {
+ input: 'edit page2',
+ hints: ' [line] -> http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage2.css',
+ markup: 'VVVVVIIIII',
+ status: 'ERROR',
+ args: {
+ resource: { arg: ' page2', status: 'INCOMPLETE', message: '' },
+ line: { status: 'VALID' },
+ }
+ },
+ },
+ {
+ setup: "edit stylez",
+ check: {
+ input: 'edit stylez',
+ hints: ' [line]',
+ markup: 'VVVVVEEEEEE',
+ status: 'ERROR',
+ args: {
+ resource: { arg: ' stylez', status: 'ERROR', message: 'Can\'t use \'stylez\'.' },
+ 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.' },
+ }
+ },
+ },
+ {
+ // Bug 759853
+ skipIf: true,
+ name: "edit exec",
+ setup: function() {
+ var windowListener = {
+ onOpenWindow: function(win) {
+ // Wait for the window to finish loading
+ let win = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ win.close();
+ }, false);
+ win.addEventListener("unload", function onUnload() {
+ win.removeEventListener("unload", onUnload, false);
+ Services.wm.removeListener(windowListener);
+ windowClosed.resolve();
+ }, false);
+ },
+ onCloseWindow: function(win) { },
+ onWindowTitleChange: function(win, title) { }
+ };
+
+ Services.wm.addListener(windowListener);
+
+ helpers.setInput(options, "edit css#style2");
+ },
+ check: {
+ input: "edit css#style2",
+ args: {
+ resource: {
+ value: function(resource) {
+ let style2 = options.window.document.getElementById("style2");
+ return resource.element.ownerNode == style2;
+ }
+ },
+ line: { value: 1 },
+ },
+ },
+ exec: {
+ output: "",
+ },
+ post: function() {
+ return windowClosed.promise;
+ }
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_enabled.js b/browser/devtools/styleeditor/test/browser_styleeditor_enabled.js
new file mode 100644
index 000000000..faa687373
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_enabled.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/ */
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let count = 0;
+ addTabAndOpenStyleEditor(function(panel) {
+ let UI = panel.UI;
+ UI.on("editor-added", function(event, editor) {
+ count++;
+ if (count == 2) {
+ // we test against first stylesheet after all are ready
+ let editor = UI.editors[0];
+ editor.getSourceEditor().then(runTests.bind(this, UI, editor));
+ }
+ })
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+function runTests(UI, editor)
+{
+ testEnabledToggle(UI, editor);
+}
+
+function testEnabledToggle(UI, editor)
+{
+ 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");
+
+ let disabledToggleCount = 0;
+ editor.on("property-change", function(event, property) {
+ if (property != "disabled") {
+ return;
+ }
+ disabledToggleCount++;
+
+ if (disabledToggleCount == 1) {
+ 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");
+
+ // now toggle it back to enabled
+ waitForFocus(function () {
+ EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
+ }, gPanelWindow);
+ return;
+ }
+
+ // disabledToggleCount == 2
+ 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");
+
+ finish();
+ });
+
+ waitForFocus(function () {
+ EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
+ }, gPanelWindow);
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js b/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js
new file mode 100644
index 000000000..d0588b630
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js
@@ -0,0 +1,90 @@
+/* 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 TESTCASE_URI_HTML = TEST_BASE + "simple.html";
+const TESTCASE_URI_CSS = TEST_BASE + "simple.css";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let tempScope = {};
+Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
+Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
+let FileUtils = tempScope.FileUtils;
+let NetUtil = tempScope.NetUtil;
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ copy(TESTCASE_URI_HTML, "simple.html", function(htmlFile) {
+ copy(TESTCASE_URI_CSS, "simple.css", function(cssFile) {
+ addTabAndOpenStyleEditor(function(panel) {
+ let UI = panel.UI;
+ UI.on("editor-added", function(event, editor) {
+ if (editor.styleSheet.styleSheetIndex != 0) {
+ return; // we want to test against the first stylesheet
+ }
+ let editor = UI.editors[0];
+ editor.getSourceEditor().then(runTests.bind(this, editor));
+ })
+ });
+
+ let uri = Services.io.newFileURI(htmlFile);
+ let filePath = uri.resolve("");
+ content.location = filePath;
+ });
+ });
+}
+
+function runTests(editor)
+{
+ editor.saveToFile(null, function (file) {
+ ok(file, "file should get saved directly when using a file:// URI");
+ finish();
+ });
+}
+
+function copy(aSrcChromeURL, aDestFileName, aCallback)
+{
+ let destFile = FileUtils.getFile("ProfD", [aDestFileName]);
+ write(read(aSrcChromeURL), destFile, aCallback);
+}
+
+function read(aSrcChromeURL)
+{
+ let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .getService(Ci.nsIScriptableInputStream);
+
+ let channel = Services.io.newChannel(aSrcChromeURL, null, null);
+ let input = channel.open();
+ scriptableStream.init(input);
+
+ let data = scriptableStream.read(input.available());
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+function write(aData, aFile, aCallback)
+{
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+ converter.charset = "UTF-8";
+
+ let istream = converter.convertToInputStream(aData);
+ let ostream = FileUtils.openSafeFileOutputStream(aFile);
+
+ NetUtil.asyncCopy(istream, ostream, function(status) {
+ if (!Components.isSuccessCode(status)) {
+ info("Coudln't write to " + aFile.path);
+ return;
+ }
+
+ aCallback(aFile);
+ });
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_import.js b/browser/devtools/styleeditor/test/browser_styleeditor_import.js
new file mode 100644
index 000000000..452e97111
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_import.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/ */
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+let tempScope = {};
+Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let FileUtils = tempScope.FileUtils;
+
+const FILENAME = "styleeditor-import-test.css";
+const SOURCE = "body{background:red;}";
+
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", testEditorAdded);
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+function testImport()
+{
+ // 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 (status) {
+ FileUtils.closeSafeFileOutputStream(ostream);
+
+ // click the import button now that the file to import is ready
+ gUI._mockImportFile = file;
+
+ waitForFocus(function () {
+ let document = gPanelWindow.document
+ let importButton = document.querySelector(".style-editor-importButton");
+ ok(importButton, "import button exists");
+
+ EventUtils.synthesizeMouseAtCenter(importButton, {}, gPanelWindow);
+ }, gPanelWindow);
+ });
+}
+
+let gAddedCount = 0;
+function testEditorAdded(aEvent, aEditor)
+{
+ if (++gAddedCount == 2) {
+ // test import after the 2 initial stylesheets have been loaded
+ gUI.editors[0].getSourceEditor().then(function() {
+ testImport();
+ });
+ }
+
+ if (!aEditor.savedFile) {
+ return;
+ }
+
+ is(aEditor.savedFile.leafName, FILENAME,
+ "imported stylesheet will be saved directly into the same file");
+ is(aEditor.friendlyName, FILENAME,
+ "imported stylesheet has the same name as the filename");
+
+ gUI = null;
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_import_rule.js b/browser/devtools/styleeditor/test/browser_styleeditor_import_rule.js
new file mode 100644
index 000000000..345d1ac95
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_import_rule.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/ */
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", onEditorAdded);
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+let gAddedCount = 0;
+function onEditorAdded()
+{
+ if (++gAddedCount != 3) {
+ return;
+ }
+
+ is(gUI.editors.length, 3,
+ "there are 3 stylesheets after loading @imports");
+
+ is(gUI.editors[0].styleSheet.href, TEST_BASE_HTTP + "simple.css",
+ "stylesheet 1 is simple.css");
+
+ is(gUI.editors[1].styleSheet.href, TEST_BASE_HTTP + "import.css",
+ "stylesheet 2 is import.css");
+
+ is(gUI.editors[2].styleSheet.href, TEST_BASE_HTTP + "import2.css",
+ "stylesheet 3 is import2.css");
+
+ gUI = null;
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_init.js b/browser/devtools/styleeditor/test/browser_styleeditor_init.js
new file mode 100644
index 000000000..0e0afedbb
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_init.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/ */
+
+const TESTCASE_URI = TEST_BASE + "simple.html";
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", testEditorAdded);
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+let gEditorAddedCount = 0;
+function testEditorAdded(aEvent, aEditor)
+{
+ if (aEditor.styleSheet.styleSheetIndex == 0) {
+ gEditorAddedCount++;
+ testFirstStyleSheetEditor(aEditor);
+ }
+ if (aEditor.styleSheet.styleSheetIndex == 1) {
+ gEditorAddedCount++;
+ testSecondStyleSheetEditor(aEditor);
+ }
+
+ if (gEditorAddedCount == 2) {
+ gUI = null;
+ finish();
+ }
+}
+
+function testFirstStyleSheetEditor(aEditor)
+{
+ // Note: the html <link> contains charset="UTF-8".
+ ok(aEditor._state.text.indexOf("\u263a") >= 0,
+ "stylesheet is unicode-aware.");
+
+ //testing TESTCASE's simple.css stylesheet
+ is(aEditor.styleSheet.styleSheetIndex, 0,
+ "first stylesheet is at index 0");
+
+ is(aEditor, gUI.editors[0],
+ "first stylesheet corresponds to StyleEditorChrome.editors[0]");
+
+ let summary = aEditor.summary;
+
+ let name = summary.querySelector(".stylesheet-name > label").getAttribute("value");
+ is(name, "simple.css",
+ "first stylesheet's name is `simple.css`");
+
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount), 1,
+ "first stylesheet UI shows rule count as 1");
+
+ ok(summary.classList.contains("splitview-active"),
+ "first stylesheet UI is focused/active");
+}
+
+function testSecondStyleSheetEditor(aEditor)
+{
+ //testing TESTCASE's inline stylesheet
+ is(aEditor.styleSheet.styleSheetIndex, 1,
+ "second stylesheet is at index 1");
+
+ is(aEditor, gUI.editors[1],
+ "second stylesheet corresponds to StyleEditorChrome.editors[1]");
+
+ let summary = aEditor.summary;
+
+ let name = summary.querySelector(".stylesheet-name > label").getAttribute("value");
+ ok(/^<.*>$/.test(name),
+ "second stylesheet's name is surrounded by `<>`");
+
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount), 3,
+ "second stylesheet UI shows rule count as 3");
+
+ ok(!summary.classList.contains("splitview-active"),
+ "second stylesheet UI is NOT focused/active");
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_loading.js b/browser/devtools/styleeditor/test/browser_styleeditor_loading.js
new file mode 100644
index 000000000..144cc01b8
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_loading.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 TESTCASE_URI = TEST_BASE + "longload.html";
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // 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.
+
+ addTabAndOpenStyleEditor(function(panel) {
+ panel.UI.on("editor-added", testEditorAdded);
+
+ content.location = TESTCASE_URI;
+ });
+}
+
+function testEditorAdded(event, editor)
+{
+ let root = gPanelWindow.document.querySelector(".splitview-root");
+ ok(!root.classList.contains("loading"),
+ "style editor root element does not have 'loading' class name anymore");
+
+ let button = gPanelWindow.document.querySelector(".style-editor-newButton");
+ ok(!button.hasAttribute("disabled"),
+ "new style sheet button is enabled");
+
+ button = gPanelWindow.document.querySelector(".style-editor-importButton");
+ ok(!button.hasAttribute("disabled"),
+ "import button is enabled");
+
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_new.js b/browser/devtools/styleeditor/test/browser_styleeditor_new.js
new file mode 100644
index 000000000..8b301b5dd
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.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/ */
+
+const TESTCASE_URI = TEST_BASE + "simple.html";
+
+let TRANSITION_CLASS = "moz-styleeditor-transitioning";
+let TESTCASE_CSS_SOURCE = "body{background-color:red;";
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", testEditorAdded);
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+let gAddedCount = 0; // to add new stylesheet after the 2 initial stylesheets
+let gNewEditor; // to make sure only one new stylesheet got created
+let gOriginalHref;
+
+let checksCompleted = 0;
+
+function testEditorAdded(aEvent, aEditor)
+{
+ gAddedCount++;
+ if (gAddedCount == 2) {
+ waitForFocus(function () {// create a new style sheet
+ let newButton = gPanelWindow.document.querySelector(".style-editor-newButton");
+ ok(newButton, "'new' button exists");
+
+ EventUtils.synthesizeMouseAtCenter(newButton, {}, gPanelWindow);
+ }, gPanelWindow);
+ }
+ if (gAddedCount < 3) {
+ return;
+ }
+
+ ok(!gNewEditor, "creating a new stylesheet triggers one EditorAdded event");
+ gNewEditor = aEditor; // above test will fail if we get a duplicate event
+
+ is(gUI.editors.length, 3,
+ "creating a new stylesheet added a new StyleEditor instance");
+
+ aEditor.styleSheet.once("style-applied", function() {
+ // when changes have been completely applied to live stylesheet after transisiton
+ ok(!content.document.documentElement.classList.contains(TRANSITION_CLASS),
+ "StyleEditor's transition class has been removed from content");
+
+ if (++checksCompleted == 3) {
+ cleanup();
+ }
+ });
+
+ aEditor.styleSheet.on("property-change", function(event, property) {
+ if (property == "ruleCount") {
+ let ruleCount = aEditor.summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount), 1,
+ "new editor shows 1 rule after modification");
+
+ if (++checksCompleted == 3) {
+ cleanup();
+ }
+ }
+ });
+
+ aEditor.getSourceEditor().then(testEditor);
+}
+
+function testEditor(aEditor) {
+ waitForFocus(function () {
+ gOriginalHref = aEditor.styleSheet.href;
+
+ let summary = aEditor.summary;
+
+ ok(aEditor.sourceLoaded,
+ "new editor is loaded when attached");
+ ok(aEditor.isNew,
+ "new editor has isNew flag");
+
+ ok(aEditor.sourceEditor.hasFocus(),
+ "new editor has focus");
+
+ let summary = aEditor.summary;
+ let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount), 0,
+ "new editor initially shows 0 rules");
+
+ let computedStyle = content.getComputedStyle(content.document.body, null);
+ is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
+ "content's background color is initially white");
+
+ EventUtils.synthesizeKey("[", {accelKey: true}, gPanelWindow);
+ is(aEditor.sourceEditor.getText(), "",
+ "Nothing happened as it is a known shortcut in source editor");
+
+ EventUtils.synthesizeKey("]", {accelKey: true}, gPanelWindow);
+ is(aEditor.sourceEditor.getText(), "",
+ "Nothing happened as it is a known shortcut in source editor");
+
+ for each (let c in TESTCASE_CSS_SOURCE) {
+ EventUtils.synthesizeKey(c, {}, gPanelWindow);
+ }
+
+ is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
+ "rule bracket has been auto-closed");
+
+ ok(aEditor.unsaved,
+ "new editor has unsaved flag");
+
+ // we know that the testcase above will start a CSS transition
+ content.addEventListener("transitionend", onTransitionEnd, false);
+}, gPanelWindow) ;
+}
+
+function onTransitionEnd() {
+ content.removeEventListener("transitionend", onTransitionEnd, false);
+
+ let computedStyle = content.getComputedStyle(content.document.body, null);
+ is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
+ "content's background color has been updated to red");
+
+ if (gNewEditor) {
+ is(gNewEditor.styleSheet.href, gOriginalHref,
+ "style sheet href did not change");
+ }
+
+ if (++checksCompleted == 3) {
+ cleanup();
+ }
+}
+
+function cleanup() {
+ gNewEditor = null;
+ gUI = null;
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js b/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js
new file mode 100644
index 000000000..cc7af9556
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.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/ */
+
+const TESTCASE_URI = TEST_BASE + "nostyle.html";
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // 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.
+
+ addTabAndOpenStyleEditor(function(panel) {
+ panel.UI.once("document-load", testDocumentLoad);
+
+ content.location = TESTCASE_URI;
+ });
+}
+
+function testDocumentLoad(event)
+{
+ let root = gPanelWindow.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 = gPanelWindow.document.querySelector(".style-editor-newButton");
+ ok(!button.hasAttribute("disabled"),
+ "new style sheet button is enabled");
+
+ button = gPanelWindow.document.querySelector(".style-editor-importButton");
+ ok(!button.hasAttribute("disabled"),
+ "import button is enabled");
+
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js
new file mode 100644
index 000000000..94cd695c8
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.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/ */
+
+const TESTCASE_URI = TEST_BASE + "minified.html";
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", function(event, editor) {
+ editor.getSourceEditor().then(function() {
+ testEditor(editor);
+ });
+ });
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+let editorTestedCount = 0;
+function testEditor(aEditor)
+{
+ if (aEditor.styleSheet.styleSheetIndex == 0) {
+ let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n";
+ let prettifiedSourceRE = new RegExp(prettifiedSource);
+
+ ok(prettifiedSourceRE.test(aEditor.sourceEditor.getText()),
+ "minified source has been prettified automatically");
+ editorTestedCount++;
+ let summary = gUI.editors[1].summary;
+ EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
+ }
+
+ if (aEditor.styleSheet.styleSheetIndex == 1) {
+ let originalSource = "body \{ background\: red; \}\r?\ndiv \{\r?\nfont\-size\: 5em;\r?\ncolor\: red\r?\n\}";
+ let originalSourceRE = new RegExp(originalSource);
+
+ ok(originalSourceRE.test(aEditor.sourceEditor.getText()),
+ "non-minified source has been left untouched");
+ editorTestedCount++;
+ }
+
+ if (editorTestedCount == 2) {
+ gUI = null;
+ finish();
+ }
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js b/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js
new file mode 100644
index 000000000..3b336e13a
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.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/. */
+
+// This test makes sure that the style editor does not store any
+// content CSS files in the permanent cache when opened from PB mode.
+
+let gUI;
+
+function test() {
+ waitForExplicitFinish();
+ let windowsToClose = [];
+ let testURI = 'http://' + TEST_HOST + '/browser/browser/devtools/styleeditor/test/test_private.html';
+
+ function checkCache() {
+ checkDiskCacheFor(TEST_HOST);
+
+ gUI = null;
+ finish();
+ }
+
+ function doTest(aWindow) {
+ aWindow.gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ aWindow.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ cache.evictEntries(Ci.nsICache.STORE_ANYWHERE);
+ openStyleEditorInWindow(aWindow, function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", onEditorAdded);
+ });
+ }, true);
+
+ aWindow.gBrowser.selectedBrowser.loadURI(testURI);
+ }
+
+ function onEditorAdded(aEvent, aEditor) {
+ if (aEditor.sourceLoaded) {
+ checkCache();
+ }
+ else {
+ aEditor.on("source-load", checkCache);
+ }
+ }
+
+ function testOnWindow(options, callback) {
+ let win = OpenBrowserWindow(options);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ windowsToClose.push(win);
+ executeSoon(function() callback(win));
+ }, false);
+ };
+
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(win) {
+ win.close();
+ });
+ });
+
+ testOnWindow({private: true}, function(win) {
+ doTest(win);
+ });
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_reload.js b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
new file mode 100644
index 000000000..267fbcffe
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.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/ */
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+let gContentWin;
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+ gUI = panel.UI;
+
+ let count = 0;
+ gUI.on("editor-added", function editorAdded(event, editor) {
+ if (++count == 2) {
+ gUI.off("editor-added", editorAdded);
+ gUI.editors[0].getSourceEditor().then(runTests);
+ }
+ })
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+function runTests()
+{
+ let count = 0;
+ gUI.once("editor-selected", (event, editor) => {
+ editor.getSourceEditor().then(() => {
+ info("selected second editor, about to reload page");
+ reloadPage();
+
+ gUI.on("editor-added", function editorAdded(event, editor) {
+ if (++count == 2) {
+ gUI.off("editor-added", editorAdded);
+ gUI.editors[1].getSourceEditor().then(testRemembered);
+ }
+ })
+ });
+ });
+ gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
+}
+
+function testRemembered()
+{
+ is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
+
+ let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+ is(line, LINE_NO, "correct line selected");
+ is(col, COL_NO, "correct column selected");
+
+ testNewPage();
+}
+
+function testNewPage()
+{
+ let count = 0;
+ gUI.on("editor-added", function editorAdded(event, editor) {
+ info("editor added here")
+ if (++count == 2) {
+ gUI.off("editor-added", editorAdded);
+ gUI.editors[0].getSourceEditor().then(testNotRemembered);
+ }
+ })
+
+ info("navigating to a different page");
+ navigatePage();
+}
+
+function testNotRemembered()
+{
+ is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
+
+ let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+ is(line, 0, "first line is selected");
+ is(col, 0, "first column is selected");
+
+ gUI = null;
+ finish();
+}
+
+function reloadPage()
+{
+ gContentWin.location.reload();
+}
+
+function navigatePage()
+{
+ gContentWin.location = NEW_URI;
+} \ No newline at end of file
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js b/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js
new file mode 100644
index 000000000..853417812
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.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/ */
+
+const TESTCASE_URI = TEST_BASE + "four.html";
+
+let gUI;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+ gUI.on("editor-added", function(event, editor) {
+ if (editor == gUI.editors[3]) {
+ runTests();
+ }
+ });
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+function runTests()
+{
+ gUI.editors[0].getSourceEditor().then(onEditor0Attach);
+ gUI.editors[2].getSourceEditor().then(onEditor2Attach);
+}
+
+function getStylesheetNameLinkFor(aEditor)
+{
+ return aEditor.summary.querySelector(".stylesheet-name");
+}
+
+function onEditor0Attach(aEditor)
+{
+ waitForFocus(function () {
+ let summary = aEditor.summary;
+ EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
+
+ let item = getStylesheetNameLinkFor(gUI.editors[0]);
+ is(gPanelWindow.document.activeElement, item,
+ "editor 0 item is the active element");
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, gPanelWindow);
+ item = getStylesheetNameLinkFor(gUI.editors[1]);
+ is(gPanelWindow.document.activeElement, item,
+ "editor 1 item is the active element");
+
+ EventUtils.synthesizeKey("VK_HOME", {}, gPanelWindow);
+ item = getStylesheetNameLinkFor(gUI.editors[0]);
+ is(gPanelWindow.document.activeElement, item,
+ "fist editor item is the active element");
+
+ EventUtils.synthesizeKey("VK_END", {}, gPanelWindow);
+ item = getStylesheetNameLinkFor(gUI.editors[3]);
+ is(gPanelWindow.document.activeElement, item,
+ "last editor item is the active element");
+
+ EventUtils.synthesizeKey("VK_UP", {}, gPanelWindow);
+ item = getStylesheetNameLinkFor(gUI.editors[2]);
+ is(gPanelWindow.document.activeElement, item,
+ "editor 2 item is the active element");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, gPanelWindow);
+ // this will attach and give focus editor 2
+ }, gPanelWindow);
+}
+
+function onEditor2Attach(aEditor)
+{
+ ok(aEditor.sourceEditor.hasFocus(),
+ "editor 2 has focus");
+
+ gUI = null;
+ finish();
+}
diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
new file mode 100644
index 000000000..6b143f180
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.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/ */
+
+const TESTCASE_URI = TEST_BASE + "simple.html";
+
+let gOriginalWidth; // these are set by runTests()
+let gOriginalHeight;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ let UI = panel.UI;
+ UI.on("editor-added", function(event, editor) {
+ if (editor == UI.editors[1]) {
+ // wait until both editors are added
+ runTests(UI);
+ }
+ });
+ });
+
+ content.location = TESTCASE_URI;
+}
+
+function runTests(aUI)
+{
+ is(aUI.editors.length, 2,
+ "there is 2 stylesheets initially");
+
+ aUI.editors[0].getSourceEditor().then(function onEditorAttached(aEditor) {
+ executeSoon(function () {
+ waitForFocus(function () {
+ // queue a resize to inverse aspect ratio
+ // this will trigger a detach and reattach (to workaround bug 254144)
+ let originalSourceEditor = aEditor.sourceEditor;
+ aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
+
+ gOriginalWidth = gPanelWindow.outerWidth;
+ gOriginalHeight = gPanelWindow.outerHeight;
+ gPanelWindow.resizeTo(120, 480);
+
+ executeSoon(function () {
+ is(aEditor.sourceEditor, originalSourceEditor,
+ "the editor still references the same SourceEditor instance");
+ is(aEditor.sourceEditor.getCaretOffset(), 4,
+ "the caret position has been preserved");
+
+ // queue a resize to original aspect ratio
+ waitForFocus(function () {
+ gPanelWindow.resizeTo(gOriginalWidth, gOriginalHeight);
+ executeSoon(function () {
+ finish();
+ });
+ }, gPanelWindow);
+ });
+ }, gPanelWindow);
+ });
+ });
+}
diff --git a/browser/devtools/styleeditor/test/four.html b/browser/devtools/styleeditor/test/four.html
new file mode 100644
index 000000000..c0d51d691
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/head.js b/browser/devtools/styleeditor/test/head.js
new file mode 100644
index 000000000..561493189
--- /dev/null
+++ b/browser/devtools/styleeditor/test/head.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/styleeditor/test/";
+const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/styleeditor/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/styleeditor/test/";
+const TEST_HOST = 'mochi.test:8888';
+
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+let TargetFactory = tempScope.devtools.TargetFactory;
+Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+let console = tempScope.console;
+
+let gPanelWindow;
+let cache = Cc["@mozilla.org/network/cache-service;1"]
+ .getService(Ci.nsICacheService);
+
+
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+
+function cleanup()
+{
+ gPanelWindow = null;
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+function addTabAndOpenStyleEditor(callback) {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openStyleEditorInWindow(window, callback);
+ }, true);
+}
+
+function openStyleEditorInWindow(win, callback) {
+ let target = TargetFactory.forTab(win.gBrowser.selectedTab);
+ win.gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ let panel = toolbox.getCurrentPanel();
+ gPanelWindow = panel._panelWin;
+
+ panel.UI._alwaysDisableAnimations = true;
+
+ /*
+ if (aSheet) {
+ panel.selectStyleSheet(aSheet, aLine, aCol);
+ } */
+
+ callback(panel);
+ });
+}
+
+/*
+function launchStyleEditorChrome(aCallback, aSheet, aLine, aCol)
+{
+ launchStyleEditorChromeFromWindow(window, aCallback, aSheet, aLine, aCol);
+}
+
+function launchStyleEditorChromeFromWindow(aWindow, aCallback, aSheet, aLine, aCol)
+{
+ let target = TargetFactory.forTab(aWindow.gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ let panel = toolbox.getCurrentPanel();
+ gPanelWindow = panel._panelWin;
+ gPanelWindow.styleEditorChrome._alwaysDisableAnimations = true;
+ if (aSheet) {
+ panel.selectStyleSheet(aSheet, aLine, aCol);
+ }
+ aCallback(gPanelWindow.styleEditorChrome);
+ });
+}
+
+function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback, aSheet, aLine, aCol)
+{
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ launchStyleEditorChrome(aCallback, aSheet, aLine, aCol);
+ }, true);
+}
+*/
+
+function checkDiskCacheFor(host)
+{
+ let foundPrivateData = false;
+
+ let visitor = {
+ visitDevice: function(deviceID, deviceInfo) {
+ if (deviceID == "disk")
+ info("disk device contains " + deviceInfo.entryCount + " entries");
+ return deviceID == "disk";
+ },
+
+ visitEntry: function(deviceID, entryInfo) {
+ info(entryInfo.key);
+ foundPrivateData |= entryInfo.key.contains(host);
+ is(foundPrivateData, false, "web content present in disk cache");
+ }
+ };
+ cache.visitEntries(visitor);
+ is(foundPrivateData, false, "private data present in disk cache");
+}
+
+registerCleanupFunction(cleanup);
diff --git a/browser/devtools/styleeditor/test/import.css b/browser/devtools/styleeditor/test/import.css
new file mode 100644
index 000000000..df532fb96
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/import.html b/browser/devtools/styleeditor/test/import.html
new file mode 100644
index 000000000..bc92baeba
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/import2.css b/browser/devtools/styleeditor/test/import2.css
new file mode 100644
index 000000000..fbbe14d9a
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/longload.html b/browser/devtools/styleeditor/test/longload.html
new file mode 100644
index 000000000..8e58daeb7
--- /dev/null
+++ b/browser/devtools/styleeditor/test/longload.html
@@ -0,0 +1,28 @@
+<!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>
+ for (i = 0; i < 5000; i++) {
+ document.write("<br>...");
+ }
+ </script>
+</body>
+</html>
diff --git a/browser/devtools/styleeditor/test/media-small.css b/browser/devtools/styleeditor/test/media-small.css
new file mode 100644
index 000000000..d64756ddc
--- /dev/null
+++ b/browser/devtools/styleeditor/test/media-small.css
@@ -0,0 +1,5 @@
+/* this stylesheet applies when min-width<400px */
+body {
+ background: red;
+}
+
diff --git a/browser/devtools/styleeditor/test/media.html b/browser/devtools/styleeditor/test/media.html
new file mode 100644
index 000000000..ef05818c5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/minified.html b/browser/devtools/styleeditor/test/minified.html
new file mode 100644
index 000000000..a7d3eec32
--- /dev/null
+++ b/browser/devtools/styleeditor/test/minified.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <title>minified testcase</title>
+ <style type="text/css"><!--
+body{background:white;}div{font-size:4em;color:red}
+--></style>
+ <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/browser/devtools/styleeditor/test/moz.build b/browser/devtools/styleeditor/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/styleeditor/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/styleeditor/test/nostyle.html b/browser/devtools/styleeditor/test/nostyle.html
new file mode 100644
index 000000000..f6a6769e6
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/resources_inpage.jsi b/browser/devtools/styleeditor/test/resources_inpage.jsi
new file mode 100644
index 000000000..8b7895af5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/resources_inpage1.css b/browser/devtools/styleeditor/test/resources_inpage1.css
new file mode 100644
index 000000000..644deaaea
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/resources_inpage2.css b/browser/devtools/styleeditor/test/resources_inpage2.css
new file mode 100644
index 000000000..e4fa48e53
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/simple.css b/browser/devtools/styleeditor/test/simple.css
new file mode 100644
index 000000000..829fe9e6c
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/simple.css.gz b/browser/devtools/styleeditor/test/simple.css.gz
new file mode 100644
index 000000000..ee3b9efbc
--- /dev/null
+++ b/browser/devtools/styleeditor/test/simple.css.gz
Binary files differ
diff --git a/browser/devtools/styleeditor/test/simple.css.gz^headers^ b/browser/devtools/styleeditor/test/simple.css.gz^headers^
new file mode 100644
index 000000000..092020ab0
--- /dev/null
+++ b/browser/devtools/styleeditor/test/simple.css.gz^headers^
@@ -0,0 +1,4 @@
+Vary: Accept-Encoding
+Content-Encoding: gzip
+Content-Type: text/css
+
diff --git a/browser/devtools/styleeditor/test/simple.gz.html b/browser/devtools/styleeditor/test/simple.gz.html
new file mode 100644
index 000000000..d63362b8e
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleeditor/test/simple.html b/browser/devtools/styleeditor/test/simple.html
new file mode 100644
index 000000000..2e7ce3eac
--- /dev/null
+++ b/browser/devtools/styleeditor/test/simple.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <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/browser/devtools/styleeditor/test/test_private.css b/browser/devtools/styleeditor/test/test_private.css
new file mode 100644
index 000000000..438954d36
--- /dev/null
+++ b/browser/devtools/styleeditor/test/test_private.css
@@ -0,0 +1,3 @@
+body {
+ background-color: red;
+} \ No newline at end of file
diff --git a/browser/devtools/styleeditor/test/test_private.html b/browser/devtools/styleeditor/test/test_private.html
new file mode 100644
index 000000000..bfde3520e
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/styleinspector/Makefile.in b/browser/devtools/styleinspector/Makefile.in
new file mode 100644
index 000000000..1599859b7
--- /dev/null
+++ b/browser/devtools/styleinspector/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/styleinspector
diff --git a/browser/devtools/styleinspector/computed-view.js b/browser/devtools/styleinspector/computed-view.js
new file mode 100644
index 000000000..7fb0f8ef2
--- /dev/null
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -0,0 +1,953 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+const {Cc, Ci, Cu} = require("chrome");
+
+let ToolDefinitions = require("main").Tools;
+let {CssLogic} = require("devtools/styleinspector/css-logic");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PluralForm.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/Templater.jsm");
+
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+const FILTER_CHANGED_TIMEOUT = 300;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Helper for long-running processes that should yield occasionally to
+ * the mainloop.
+ *
+ * @param {Window} aWin
+ * Timeouts will be set on this window when appropriate.
+ * @param {Generator} aGenerator
+ * Will iterate this generator.
+ * @param {object} aOptions
+ * 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.
+ *
+ * @constructor
+ */
+function UpdateProcess(aWin, aGenerator, aOptions)
+{
+ this.win = aWin;
+ this.iter = _Iterator(aGenerator);
+ this.onItem = aOptions.onItem || function() {};
+ this.onBatch = aOptions.onBatch || function () {};
+ this.onDone = aOptions.onDone || function() {};
+ this.onCancel = aOptions.onCancel || function() {};
+ this.threshold = aOptions.threshold || 45;
+
+ this.canceled = false;
+}
+
+UpdateProcess.prototype = {
+ /**
+ * Schedule a new batch on the main loop.
+ */
+ schedule: function UP_schedule()
+ {
+ if (this.canceled) {
+ return;
+ }
+ this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
+ },
+
+ /**
+ * Cancel the running process. onItem will not be called again,
+ * and onCancel will be called.
+ */
+ cancel: function UP_cancel()
+ {
+ if (this._timeout) {
+ this.win.clearTimeout(this._timeout);
+ this._timeout = 0;
+ }
+ this.canceled = true;
+ this.onCancel();
+ },
+
+ _timeoutHandler: function UP_timeoutHandler() {
+ this._timeout = null;
+ try {
+ this._runBatch();
+ this.schedule();
+ } catch(e) {
+ if (e instanceof StopIteration) {
+ this.onBatch();
+ this.onDone();
+ return;
+ }
+ throw e;
+ }
+ },
+
+ _runBatch: function Y_runBatch()
+ {
+ let time = Date.now();
+ while(!this.canceled) {
+ // Continue until iter.next() throws...
+ let next = this.iter.next();
+ this.onItem(next[1]);
+ if ((Date.now() - time) > this.threshold) {
+ this.onBatch();
+ return;
+ }
+ }
+ }
+};
+
+/**
+ * CssHtmlTree is a panel that manages the display of a table sorted by style.
+ * There should be one instance of CssHtmlTree per style display (of which there
+ * will generally only be one).
+ *
+ * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree
+ * @constructor
+ */
+function CssHtmlTree(aStyleInspector)
+{
+ this.styleWindow = aStyleInspector.window;
+ this.styleDocument = aStyleInspector.window.document;
+ this.styleInspector = aStyleInspector;
+ this.cssLogic = aStyleInspector.cssLogic;
+ this.propertyViews = [];
+
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry);
+ this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
+
+ // Create bound methods.
+ this.siFocusWindow = this.focusWindow.bind(this);
+ this.siBoundCopy = this.computedViewCopy.bind(this);
+
+ this.styleDocument.addEventListener("copy", this.siBoundCopy);
+ this.styleDocument.addEventListener("mousedown", this.siFocusWindow);
+
+ // Nodes used in templating
+ this.root = this.styleDocument.getElementById("root");
+ this.templateRoot = this.styleDocument.getElementById("templateRoot");
+ this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
+
+ // No results text.
+ this.noResults = this.styleDocument.getElementById("noResults");
+
+ // The element that we're inspecting, and the document that it comes from.
+ this.viewedElement = null;
+ this.createStyleViews();
+}
+
+/**
+ * Memoized lookup of a l10n string from a string bundle.
+ * @param {string} aName The key to lookup.
+ * @returns A localized version of the given key.
+ */
+CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
+{
+ try {
+ return CssHtmlTree._strings.GetStringFromName(aName);
+ } catch (ex) {
+ Services.console.logStringMessage("Error reading '" + aName + "'");
+ throw new Error("l10n error with " + aName);
+ }
+};
+
+/**
+ * Clone the given template node, and process it by resolving ${} references
+ * in the template.
+ *
+ * @param {nsIDOMElement} aTemplate the template note to use.
+ * @param {nsIDOMElement} aDestination the destination node where the
+ * processed nodes will be displayed.
+ * @param {object} aData the data to pass to the template.
+ * @param {Boolean} aPreserveDestination If true then the template will be
+ * appended to aDestination's content else aDestination.innerHTML will be
+ * cleared before the template is appended.
+ */
+CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
+ aDestination, aData, aPreserveDestination)
+{
+ if (!aPreserveDestination) {
+ aDestination.innerHTML = "";
+ }
+
+ // All the templater does is to populate a given DOM tree with the given
+ // values, so we need to clone the template first.
+ let duplicated = aTemplate.cloneNode(true);
+
+ // See https://github.com/mozilla/domtemplate/blob/master/README.md
+ // for docs on the template() function
+ template(duplicated, aData, { allowEval: true });
+ while (duplicated.firstChild) {
+ aDestination.appendChild(duplicated.firstChild);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
+ .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+});
+
+CssHtmlTree.prototype = {
+ // Cache the list of properties that match the selected element.
+ _matchedProperties: null,
+
+ htmlComplete: false,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // The search filter
+ searchField: null,
+
+ // Reference to the "Include browser styles" checkbox.
+ includeBrowserStylesCheckbox: null,
+
+ // Holds the ID of the panelRefresh timeout.
+ _panelRefreshTimeout: null,
+
+ // Toggle for zebra striping
+ _darkStripe: true,
+
+ // Number of visible properties
+ numVisibleProperties: 0,
+
+ get includeBrowserStyles()
+ {
+ return this.includeBrowserStylesCheckbox.checked;
+ },
+
+ /**
+ * Update the highlighted element. The CssHtmlTree panel will show the style
+ * information for the given element.
+ * @param {nsIDOMElement} aElement The highlighted node to get styles for.
+ */
+ highlight: function CssHtmlTree_highlight(aElement)
+ {
+ this.viewedElement = aElement;
+ this._matchedProperties = null;
+
+ if (!aElement) {
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+ return;
+ }
+
+ if (this.htmlComplete) {
+ this.refreshSourceFilter();
+ this.refreshPanel();
+ } else {
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
+
+ // Refresh source filter ... this must be done after templateRoot has been
+ // processed.
+ this.refreshSourceFilter();
+ this.numVisibleProperties = 0;
+ let fragment = this.styleDocument.createDocumentFragment();
+ this._refreshProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, {
+ onItem: function(aPropertyName) {
+ // Per-item callback.
+ let propView = new PropertyView(this, aPropertyName);
+ fragment.appendChild(propView.buildMain());
+ fragment.appendChild(propView.buildSelectorContainer());
+
+ if (propView.visible) {
+ this.numVisibleProperties++;
+ }
+ propView.refreshMatchedSelectors();
+ this.propertyViews.push(propView);
+ }.bind(this),
+ onDone: function() {
+ // Completed callback.
+ this.htmlComplete = true;
+ this.propertyContainer.appendChild(fragment);
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ this._refreshProcess = null;
+
+ // If a refresh was scheduled during the building, complete it.
+ if (this._needsRefresh) {
+ delete this._needsRefresh;
+ this.refreshPanel();
+ } else {
+ Services.obs.notifyObservers(null, "StyleInspector-populated", null);
+ }
+ }.bind(this)});
+
+ this._refreshProcess.schedule();
+ }
+ },
+
+ /**
+ * Refresh the panel content.
+ */
+ refreshPanel: function CssHtmlTree_refreshPanel()
+ {
+ // If we're still in the process of creating the initial layout,
+ // leave it alone.
+ if (!this.htmlComplete) {
+ if (this._refreshProcess) {
+ this._needsRefresh = true;
+ }
+ return;
+ }
+
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ this.noResults.hidden = true;
+
+ // Reset visible property count
+ this.numVisibleProperties = 0;
+
+ // Reset zebra striping.
+ this._darkStripe = true;
+
+ let display = this.propertyContainer.style.display;
+ this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
+ onItem: function(aPropView) {
+ aPropView.refresh();
+ }.bind(this),
+ onDone: function() {
+ this._refreshProcess = null;
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ Services.obs.notifyObservers(null, "StyleInspector-populated", null);
+ }.bind(this)
+ });
+ this._refreshProcess.schedule();
+ },
+
+ /**
+ * Called when the user enters a search term.
+ *
+ * @param {Event} aEvent the DOM Event object.
+ */
+ filterChanged: function CssHtmlTree_filterChanged(aEvent)
+ {
+ let win = this.styleWindow;
+
+ if (this._filterChangedTimeout) {
+ win.clearTimeout(this._filterChangedTimeout);
+ }
+
+ this._filterChangedTimeout = win.setTimeout(function() {
+ this.refreshPanel();
+ this._filterChangeTimeout = null;
+ }.bind(this), FILTER_CHANGED_TIMEOUT);
+ },
+
+ /**
+ * The change event handler for the includeBrowserStyles checkbox.
+ *
+ * @param {Event} aEvent the DOM Event object.
+ */
+ includeBrowserStylesChanged:
+ function CssHtmltree_includeBrowserStylesChanged(aEvent)
+ {
+ this.refreshSourceFilter();
+ this.refreshPanel();
+ },
+
+ /**
+ * When includeBrowserStyles.checked is false we only display properties that
+ * have matched selectors and have been included by the document or one of the
+ * document's stylesheets. If .checked is false we display all properties
+ * including those that come from UA stylesheets.
+ */
+ refreshSourceFilter: function CssHtmlTree_setSourceFilter()
+ {
+ this._matchedProperties = null;
+ this.cssLogic.sourceFilter = this.includeBrowserStyles ?
+ CssLogic.FILTER.UA :
+ CssLogic.FILTER.ALL;
+ },
+
+ /**
+ * The CSS as displayed by the UI.
+ */
+ createStyleViews: function CssHtmlTree_createStyleViews()
+ {
+ if (CssHtmlTree.propertyNames) {
+ return;
+ }
+
+ CssHtmlTree.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.charAt(0) == "-") {
+ mozProps.push(prop);
+ } else {
+ CssHtmlTree.propertyNames.push(prop);
+ }
+ }
+
+ CssHtmlTree.propertyNames.sort();
+ CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
+ mozProps.sort());
+ },
+
+ /**
+ * Get a list of properties that have matched selectors.
+ *
+ * @return {object} the object maps property names (keys) to booleans (values)
+ * that tell if the given property has matched selectors or not.
+ */
+ get matchedProperties()
+ {
+ if (!this._matchedProperties) {
+ this._matchedProperties =
+ this.cssLogic.hasMatchedSelectors(CssHtmlTree.propertyNames);
+ }
+ return this._matchedProperties;
+ },
+
+ /**
+ * Focus the window on mousedown.
+ *
+ * @param aEvent The event object
+ */
+ focusWindow: function si_focusWindow(aEvent)
+ {
+ let win = this.styleDocument.defaultView;
+ win.focus();
+ },
+
+ /**
+ * Copy selected text.
+ *
+ * @param aEvent The event object
+ */
+ computedViewCopy: function si_computedViewCopy(aEvent)
+ {
+ let win = this.styleDocument.defaultView;
+ let text = win.getSelection().toString();
+
+ // Tidy up block headings by moving CSS property names and their values onto
+ // the same line and inserting a colon between them.
+ text = text.replace(/(.+)\r\n(.+)/g, "$1: $2;");
+ text = text.replace(/(.+)\n(.+)/g, "$1: $2;");
+
+ let outerDoc = this.styleInspector.outerIFrame.ownerDocument;
+ clipboardHelper.copyString(text, outerDoc);
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ }
+ },
+
+ /**
+ * Destructor for CssHtmlTree.
+ */
+ destroy: function CssHtmlTree_destroy()
+ {
+ delete this.viewedElement;
+
+ // Remove event listeners
+ this.includeBrowserStylesCheckbox.removeEventListener("command",
+ this.includeBrowserStylesChanged);
+ this.searchField.removeEventListener("command", this.filterChanged);
+
+ // Cancel tree construction
+ if (this._refreshProcess) {
+ this._refreshProcess.cancel();
+ }
+
+ // Remove context menu
+ let outerDoc = this.styleInspector.outerIFrame.ownerDocument;
+ let menu = outerDoc.querySelector("#computed-view-context-menu");
+ if (menu) {
+ // Copy selected
+ let menuitem = outerDoc.querySelector("#computed-view-copy");
+ menuitem.removeEventListener("command", this.siBoundCopy);
+
+ // Copy property
+ menuitem = outerDoc.querySelector("#computed-view-copy-declaration");
+ menuitem.removeEventListener("command", this.siBoundCopyDeclaration);
+
+ // Copy property name
+ menuitem = outerDoc.querySelector("#computed-view-copy-property");
+ menuitem.removeEventListener("command", this.siBoundCopyProperty);
+
+ // Copy property value
+ menuitem = outerDoc.querySelector("#computed-view-copy-property-value");
+ menuitem.removeEventListener("command", this.siBoundCopyPropertyValue);
+
+ menu.removeEventListener("popupshowing", this.siBoundMenuUpdate);
+ menu.parentNode.removeChild(menu);
+ }
+
+ // Remove bound listeners
+ this.styleDocument.removeEventListener("copy", this.siBoundCopy);
+ this.styleDocument.removeEventListener("mousedown", this.siFocusWindow);
+
+ // Nodes used in templating
+ delete this.root;
+ delete this.propertyContainer;
+ delete this.panel;
+
+ // The document in which we display the results (csshtmltree.xul).
+ delete this.styleDocument;
+
+ // The element that we're inspecting, and the document that it comes from.
+ delete this.propertyViews;
+ delete this.styleWindow;
+ delete this.styleDocument;
+ delete this.cssLogic;
+ delete this.styleInspector;
+ },
+};
+
+/**
+ * A container to give easy access to property data from the template engine.
+ *
+ * @constructor
+ * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
+ * @param {string} aName the CSS property name for which this PropertyView
+ * instance will render the rules.
+ */
+function PropertyView(aTree, aName)
+{
+ this.tree = aTree;
+ this.name = aName;
+ this.getRTLAttr = aTree.getRTLAttr;
+
+ this.link = "https://developer.mozilla.org/CSS/" + aName;
+
+ this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
+}
+
+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.tree.cssLogic.getPropertyInfo(this.name);
+ },
+
+ /**
+ * Does the property have any matched selectors?
+ */
+ get hasMatchedSelectors()
+ {
+ return this.name in this.tree.matchedProperties;
+ },
+
+ /**
+ * Should this property be visible?
+ */
+ get visible()
+ {
+ if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
+ return false;
+ }
+
+ let searchTerm = this.tree.searchField.value.toLowerCase();
+ if (searchTerm && 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) {
+ this.tree._darkStripe = !this.tree._darkStripe;
+ let darkValue = this.tree._darkStripe ?
+ "property-view theme-bg-darker" : "property-view";
+ return darkValue;
+ }
+ return "property-view-hidden";
+ },
+
+ /**
+ * Returns the className that should be assigned to the propertyView content
+ * container.
+ * @return string
+ */
+ get propertyContentClassName()
+ {
+ if (this.visible) {
+ let darkValue = this.tree._darkStripe ?
+ "property-content theme-bg-darker" : "property-content";
+ return darkValue;
+ }
+ return "property-content-hidden";
+ },
+
+ buildMain: function PropertyView_buildMain()
+ {
+ let doc = this.tree.styleDocument;
+ this.element = doc.createElementNS(HTML_NS, "div");
+ this.element.setAttribute("class", this.propertyHeaderClassName);
+
+ this.matchedExpander = doc.createElementNS(HTML_NS, "div");
+ this.matchedExpander.className = "expander theme-twisty";
+ this.matchedExpander.setAttribute("tabindex", "0");
+ this.matchedExpander.addEventListener("click",
+ this.matchedExpanderClick.bind(this), false);
+ this.matchedExpander.addEventListener("keydown", function(aEvent) {
+ let keyEvent = Ci.nsIDOMKeyEvent;
+ if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
+ this.mdnLinkClick();
+ }
+ if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
+ aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
+ this.matchedExpanderClick(aEvent);
+ }
+ }.bind(this), false);
+ this.element.appendChild(this.matchedExpander);
+
+ this.nameNode = doc.createElementNS(HTML_NS, "div");
+ this.element.appendChild(this.nameNode);
+ this.nameNode.setAttribute("class", "property-name theme-fg-color5");
+ this.nameNode.textContent = this.nameNode.title = this.name;
+ this.nameNode.addEventListener("click", function(aEvent) {
+ this.matchedExpander.focus();
+ }.bind(this), false);
+
+ this.valueNode = doc.createElementNS(HTML_NS, "div");
+ this.element.appendChild(this.valueNode);
+ this.valueNode.setAttribute("class", "property-value theme-fg-color1");
+ this.valueNode.setAttribute("dir", "ltr");
+ this.valueNode.textContent = this.valueNode.title = this.value;
+
+ return this.element;
+ },
+
+ buildSelectorContainer: function PropertyView_buildSelectorContainer()
+ {
+ 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 PropertyView_refresh()
+ {
+ 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++;
+ this.valueNode.textContent = this.valueNode.title = this.propertyInfo.value;
+ this.refreshMatchedSelectors();
+ },
+
+ /**
+ * Refresh the panel matched rules.
+ */
+ refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
+ {
+ 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) {
+ CssHtmlTree.processTemplate(this.templateMatchedSelectors,
+ this.matchedSelectorsContainer, this);
+ this.matchedExpander.setAttribute("open", "");
+ } else {
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedExpander.removeAttribute("open");
+ }
+ },
+
+ /**
+ * Provide access to the matched SelectorViews that we are currently
+ * displaying.
+ */
+ get matchedSelectorViews()
+ {
+ if (!this._matchedSelectorViews) {
+ this._matchedSelectorViews = [];
+ this.propertyInfo.matchedSelectors.forEach(
+ function matchedSelectorViews_convert(aSelectorInfo) {
+ this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
+ }, this);
+ }
+
+ return this._matchedSelectorViews;
+ },
+
+ /**
+ * The action when a user expands matched selectors.
+ *
+ * @param {Event} aEvent Used to determine the class name of the targets click
+ * event.
+ */
+ matchedExpanderClick: function PropertyView_matchedExpanderClick(aEvent)
+ {
+ this.matchedExpanded = !this.matchedExpanded;
+ this.refreshMatchedSelectors();
+ aEvent.preventDefault();
+ },
+
+ /**
+ * The action when a user clicks on the MDN help link for a property.
+ */
+ mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
+ {
+ let inspector = this.tree.styleInspector.inspector;
+
+ if (inspector.target.tab) {
+ let browserWin = inspector.target.tab.ownerDocument.defaultView;
+ browserWin.openUILinkIn(this.link, "tab");
+ }
+ aEvent.preventDefault();
+ },
+};
+
+/**
+ * A container to view us easy access to display data from a CssRule
+ * @param CssHtmlTree aTree, the owning CssHtmlTree
+ * @param aSelectorInfo
+ */
+function SelectorView(aTree, aSelectorInfo)
+{
+ this.tree = aTree;
+ this.selectorInfo = aSelectorInfo;
+ this._cacheStatusNames();
+}
+
+/**
+ * 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.
+ *
+ * @return {void}
+ */
+ _cacheStatusNames: function SelectorView_cacheStatusNames()
+ {
+ if (SelectorView.STATUS_NAMES.length) {
+ return;
+ }
+
+ for (let status in CssLogic.STATUS) {
+ let i = CssLogic.STATUS[status];
+ if (i > CssLogic.STATUS.UNMATCHED) {
+ let value = CssHtmlTree.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];
+ },
+
+ /**
+ * A localized Get localized human readable info
+ */
+ text: function SelectorView_text(aElement) {
+ let result = this.selectorInfo.selector.text;
+ if (this.selectorInfo.elementStyle) {
+ let source = this.selectorInfo.sourceElement;
+ let inspector = this.tree.styleInspector.inspector;
+
+ if (inspector.selection.node == source) {
+ result = "this";
+ } else {
+ result = CssLogic.getShortName(source);
+ }
+ result += ".style";
+ }
+
+ return result;
+ },
+
+ maybeOpenStyleEditor: function(aEvent)
+ {
+ let keyEvent = Ci.nsIDOMKeyEvent;
+ if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
+ this.openStyleEditor();
+ }
+ },
+
+ /**
+ * 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.
+ *
+ * @param aEvent The click event
+ */
+ openStyleEditor: function(aEvent)
+ {
+ let inspector = this.tree.styleInspector.inspector;
+ let contentDoc = inspector.selection.document;
+ let cssSheet = this.selectorInfo.selector._cssRule._cssSheet;
+ let line = this.selectorInfo.ruleLine || 0;
+ let contentSheet = false;
+ let styleSheet;
+ let styleSheets;
+
+ // 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.
+ //
+ // We check if cssSheet exists in case of inline styles (which contain no
+ // sheet)
+ if (cssSheet) {
+ styleSheet = cssSheet.domSheet;
+ styleSheets = contentDoc.styleSheets;
+
+ // Array.prototype.indexOf always returns -1 here so we loop through
+ // the styleSheets array instead.
+ for each (let sheet in styleSheets) {
+ if (sheet == styleSheet) {
+ contentSheet = true;
+ break;
+ }
+ }
+ }
+
+ if (contentSheet) {
+ let target = inspector.target;
+
+ if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ toolbox.getCurrentPanel().selectStyleSheet(styleSheet.href, line);
+ });
+ }
+ } else {
+ let href = styleSheet ? styleSheet.href : "";
+ let viewSourceUtils = inspector.viewSourceUtils;
+
+ if (this.selectorInfo.sourceElement) {
+ href = this.selectorInfo.sourceElement.ownerDocument.location.href;
+ }
+ viewSourceUtils.viewSource(href, null, contentDoc, line);
+ }
+ },
+};
+
+exports.CssHtmlTree = CssHtmlTree;
+exports.PropertyView = PropertyView; \ No newline at end of file
diff --git a/browser/devtools/styleinspector/computedview.xhtml b/browser/devtools/styleinspector/computedview.xhtml
new file mode 100644
index 000000000..b890b1b17
--- /dev/null
+++ b/browser/devtools/styleinspector/computedview.xhtml
@@ -0,0 +1,114 @@
+<?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 % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd">
+ %inspectorDTD;
+ <!ELEMENT loop ANY>
+ <!ATTLIST li foreach CDATA #IMPLIED>
+ <!ATTLIST div foreach CDATA #IMPLIED>
+ <!ATTLIST loop foreach CDATA #IMPLIED>
+ <!ATTLIST a target CDATA #IMPLIED>
+ <!ATTLIST a __pathElement CDATA #IMPLIED>
+ <!ATTLIST div _id CDATA #IMPLIED>
+ <!ATTLIST div save CDATA #IMPLIED>
+ <!ATTLIST table save CDATA #IMPLIED>
+ <!ATTLIST loop if CDATA #IMPLIED>
+ <!ATTLIST tr if CDATA #IMPLIED>
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-body">
+
+ <head>
+
+ <title>&computedViewTitle;</title>
+
+ <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/computedview.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8">
+ window.setPanel = function(panel, iframe) {
+ let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let inspector = devtools.require("devtools/styleinspector/style-inspector");
+ this.computedview = new inspector.ComputedViewTool(panel, window, iframe);
+ }
+ window.onunload = function() {
+ if (this.computedview) {
+ this.computedview.destroy();
+ }
+ }
+ </script>
+ </head>
+
+ <body>
+
+ <!-- The output from #templateProperty (below) is appended here. -->
+ <div id="propertyContainer" class="devtools-monospace">
+ </div>
+
+ <!-- When no properties are found the following block is displayed. -->
+ <div id="noResults" hidden="">
+ &noPropertiesFound;
+ </div>
+
+ <!-- The output from #templateRoot (below) is inserted here. -->
+ <div id="root" class="devtools-monospace"></div>
+
+ <!--
+ To visually debug the templates without running firefox, alter the display:none
+ -->
+ <div style="display:none;">
+ <!--
+ templateRoot sits at the top of the window and contains the "include default
+ styles" checkbox. For data it needs an instance of CssHtmlTree.
+ -->
+ <div id="templateRoot">
+ <xul:hbox class="devtools-toolbar" flex="1" align="center">
+ <xul:checkbox class="includebrowserstyles"
+ save="${includeBrowserStylesCheckbox}"
+ oncommand="${includeBrowserStylesChanged}" checked="false"
+ label="&browserStylesLabel;"/>
+ <xul:textbox class="devtools-searchinput" type="search" save="${searchField}"
+ placeholder="&userStylesSearch;" flex="1"
+ oncommand="${filterChanged}"/>
+ </xul:hbox>
+ </div>
+
+
+ <!--
+ A templateMatchedSelectors sits inside each templateProperties showing the
+ list of selectors that affect that property. Each needs data like this:
+ {
+ matchedSelectorViews: ..., // from cssHtmlTree.propertyViews[name].matchedSelectorViews
+ }
+ This is a template so the parent does not need to be a table, except that
+ using a div as the parent causes the DOM to muck with the tr elements
+ -->
+ <div id="templateMatchedSelectors">
+ <loop foreach="selector in ${matchedSelectorViews}">
+ <p>
+ <span class="rule-link">
+ <a target="_blank" class="link theme-link"
+ onclick="${selector.openStyleEditor}"
+ onkeydown="${selector.maybeOpenStyleEditor}"
+ title="${selector.selectorInfo.href}"
+ tabindex="0">${selector.selectorInfo.source}</a>
+ </span>
+ <span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}">
+ ${selector.text(__element)}
+ <span class="other-property-value theme-fg-color1">${selector.selectorInfo.value}</span>
+ </span>
+ </p>
+ </loop>
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/browser/devtools/styleinspector/css-logic.js b/browser/devtools/styleinspector/css-logic.js
new file mode 100644
index 000000000..467e34692
--- /dev/null
+++ b/browser/devtools/styleinspector/css-logic.js
@@ -0,0 +1,1757 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+/*
+ * About the objects defined in this file:
+ * - CssLogic contains style information about a view context. It provides
+ * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to
+ * information that does not change when the selected element changes while
+ * Css[Property|Selector]Info provide information that is dependent on the
+ * selected element.
+ * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc
+ * It also contains a number of static methods for l10n, naming, etc
+ *
+ * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes,
+ * including shortSource and href.
+ * - CssRule a more useful API to a nsIDOMCSSRule including access to the group
+ * of CssSelectors that the rule provides properties for
+ * - CssSelector A single selector - i.e. not a selector group. In other words
+ * a CssSelector does not contain ','. This terminology is different from the
+ * standard DOM API, but more inline with the definition in the spec.
+ *
+ * - CssPropertyInfo contains style information for a single property for the
+ * highlighted element.
+ * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with
+ * reference to the selected element.
+ */
+
+/**
+ * Provide access to the style information in a page.
+ * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access
+ * styling information in the page, and present this to the user in a way that
+ * helps them understand:
+ * - why their expectations may not have been fulfilled
+ * - how browsers process CSS
+ * @constructor
+ */
+
+const {Cc, Ci, Cu} = require("chrome");
+
+const RX_UNIVERSAL_SELECTOR = /\s*\*\s*/g;
+const RX_NOT = /:not\((.*?)\)/g;
+const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+\().*?\)/g;
+const RX_CONNECTORS = /\s*[\s>+~]\s*/g;
+const RX_ID = /\s*#\w+\s*/g;
+const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g;
+const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function CssLogic()
+{
+ // The cache of examined CSS properties.
+ _propertyInfos: {};
+}
+
+exports.CssLogic = CssLogic;
+
+/**
+ * Special values for filter, in addition to an href these values can be used
+ */
+CssLogic.FILTER = {
+ ALL: "all", // show properties from all user style sheets.
+ UA: "ua", // ALL, plus user-agent (i.e. browser) style sheets
+};
+
+/**
+ * Known media values. To distinguish "all" stylesheets (above) from "all" media
+ * The full list includes braille, embossed, handheld, print, projection,
+ * speech, tty, and tv, but this is only a hack because these are not defined
+ * in the DOM at all.
+ * @see http://www.w3.org/TR/CSS21/media.html#media-types
+ */
+CssLogic.MEDIA = {
+ ALL: "all",
+ SCREEN: "screen",
+};
+
+/**
+ * Each rule has a status, the bigger the number, the better placed it is to
+ * provide styling information.
+ *
+ * These statuses are localized inside the styleinspector.properties string bundle.
+ * @see csshtmltree.js RuleView._cacheStatusNames()
+ */
+CssLogic.STATUS = {
+ BEST: 3,
+ MATCHED: 2,
+ PARENT_MATCH: 1,
+ UNMATCHED: 0,
+ UNKNOWN: -1,
+};
+
+CssLogic.prototype = {
+ // Both setup by highlight().
+ viewedElement: null,
+ viewedDocument: null,
+
+ // The cache of the known sheets.
+ _sheets: null,
+
+ // Have the sheets been cached?
+ _sheetsCached: false,
+
+ // The total number of rules, in all stylesheets, after filtering.
+ _ruleCount: 0,
+
+ // The computed styles for the viewedElement.
+ _computedStyle: null,
+
+ // Source filter. Only display properties coming from the given source
+ _sourceFilter: CssLogic.FILTER.ALL,
+
+ // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of
+ // processMatchedSelectors().
+ _passId: 0,
+
+ // Used for tracking matched CssSelector objects.
+ _matchId: 0,
+
+ _matchedRules: null,
+ _matchedSelectors: null,
+
+ /**
+ * Reset various properties
+ */
+ reset: function CssLogic_reset()
+ {
+ this._propertyInfos = {};
+ this._ruleCount = 0;
+ this._sheetIndex = 0;
+ this._sheets = {};
+ this._sheetsCached = false;
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ },
+
+ /**
+ * Focus on a new element - remove the style caches.
+ *
+ * @param {nsIDOMElement} aViewedElement the element the user has highlighted
+ * in the Inspector.
+ */
+ highlight: function CssLogic_highlight(aViewedElement)
+ {
+ if (!aViewedElement) {
+ this.viewedElement = null;
+ this.viewedDocument = null;
+ this._computedStyle = null;
+ this.reset();
+ return;
+ }
+
+ this.viewedElement = aViewedElement;
+
+ let doc = this.viewedElement.ownerDocument;
+ if (doc != this.viewedDocument) {
+ // New document: clear/rebuild the cache.
+ this.viewedDocument = doc;
+
+ // Hunt down top level stylesheets, and cache them.
+ this._cacheSheets();
+ } else {
+ // Clear cached data in the CssPropertyInfo objects.
+ this._propertyInfos = {};
+ }
+
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ let win = this.viewedDocument.defaultView;
+ this._computedStyle = win.getComputedStyle(this.viewedElement, "");
+ },
+
+ /**
+ * Get the source filter.
+ * @returns {string} The source filter being used.
+ */
+ get sourceFilter() {
+ return this._sourceFilter;
+ },
+
+ /**
+ * Source filter. Only display properties coming from the given source (web
+ * address). Note that in order to avoid information overload we DO NOT show
+ * unmatched system rules.
+ * @see CssLogic.FILTER.*
+ */
+ set sourceFilter(aValue) {
+ let oldValue = this._sourceFilter;
+ this._sourceFilter = aValue;
+
+ let ruleCount = 0;
+
+ // Update the CssSheet objects.
+ this.forEachSheet(function(aSheet) {
+ aSheet._sheetAllowed = -1;
+ if (aSheet.contentSheet && aSheet.sheetAllowed) {
+ ruleCount += aSheet.ruleCount;
+ }
+ }, this);
+
+ this._ruleCount = ruleCount;
+
+ // Full update is needed because the this.processMatchedSelectors() method
+ // skips UA stylesheets if the filter does not allow such sheets.
+ let needFullUpdate = (oldValue == CssLogic.FILTER.UA ||
+ aValue == CssLogic.FILTER.UA);
+
+ if (needFullUpdate) {
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ this._propertyInfos = {};
+ } else {
+ // Update the CssPropertyInfo objects.
+ for each (let propertyInfo in this._propertyInfos) {
+ propertyInfo.needRefilter = true;
+ }
+ }
+ },
+
+ /**
+ * Return a CssPropertyInfo data structure for the currently viewed element
+ * and the specified CSS property. If there is no currently viewed element we
+ * return an empty object.
+ *
+ * @param {string} aProperty The CSS property to look for.
+ * @return {CssPropertyInfo} a CssPropertyInfo structure for the given
+ * property.
+ */
+ getPropertyInfo: function CssLogic_getPropertyInfo(aProperty)
+ {
+ if (!this.viewedElement) {
+ return {};
+ }
+
+ let info = this._propertyInfos[aProperty];
+ if (!info) {
+ info = new CssPropertyInfo(this, aProperty);
+ this._propertyInfos[aProperty] = info;
+ }
+
+ return info;
+ },
+
+ /**
+ * Cache all the stylesheets in the inspected document
+ * @private
+ */
+ _cacheSheets: function CssLogic_cacheSheets()
+ {
+ this._passId++;
+ this.reset();
+
+ // styleSheets isn't an array, but forEach can work on it anyway
+ Array.prototype.forEach.call(this.viewedDocument.styleSheets,
+ this._cacheSheet, this);
+
+ this._sheetsCached = true;
+ },
+
+ /**
+ * Cache a stylesheet if it falls within the requirements: if it's enabled,
+ * and if the @media is allowed. This method also walks through the stylesheet
+ * cssRules to find @imported rules, to cache the stylesheets of those rules
+ * as well.
+ *
+ * @private
+ * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object to cache.
+ */
+ _cacheSheet: function CssLogic_cacheSheet(aDomSheet)
+ {
+ if (aDomSheet.disabled) {
+ return;
+ }
+
+ // Only work with stylesheets that have their media allowed.
+ if (!this.mediaMatches(aDomSheet)) {
+ return;
+ }
+
+ // Cache the sheet.
+ let cssSheet = this.getSheet(aDomSheet, this._sheetIndex++);
+ if (cssSheet._passId != this._passId) {
+ cssSheet._passId = this._passId;
+
+ // Find import rules.
+ Array.prototype.forEach.call(aDomSheet.cssRules, function(aDomRule) {
+ if (aDomRule.type == Ci.nsIDOMCSSRule.IMPORT_RULE && aDomRule.styleSheet &&
+ this.mediaMatches(aDomRule)) {
+ this._cacheSheet(aDomRule.styleSheet);
+ }
+ }, this);
+ }
+ },
+
+ /**
+ * Retrieve the list of stylesheets in the document.
+ *
+ * @return {array} the list of stylesheets in the document.
+ */
+ get sheets()
+ {
+ if (!this._sheetsCached) {
+ this._cacheSheets();
+ }
+
+ let sheets = [];
+ this.forEachSheet(function (aSheet) {
+ if (aSheet.contentSheet) {
+ sheets.push(aSheet);
+ }
+ }, this);
+
+ return sheets;
+ },
+
+ /**
+ * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the
+ * stylesheet is already cached, you get the existing CssSheet object,
+ * otherwise the new CSSStyleSheet object is cached.
+ *
+ * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object you want.
+ * @param {number} aIndex the index, within the document, of the stylesheet.
+ *
+ * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object.
+ */
+ getSheet: function CL_getSheet(aDomSheet, aIndex)
+ {
+ let cacheId = "";
+
+ if (aDomSheet.href) {
+ cacheId = aDomSheet.href;
+ } else if (aDomSheet.ownerNode && aDomSheet.ownerNode.ownerDocument) {
+ cacheId = aDomSheet.ownerNode.ownerDocument.location;
+ }
+
+ let sheet = null;
+ let sheetFound = false;
+
+ if (cacheId in this._sheets) {
+ for (let i = 0, numSheets = this._sheets[cacheId].length; i < numSheets; i++) {
+ sheet = this._sheets[cacheId][i];
+ if (sheet.domSheet === aDomSheet) {
+ if (aIndex != -1) {
+ sheet.index = aIndex;
+ }
+ sheetFound = true;
+ break;
+ }
+ }
+ }
+
+ if (!sheetFound) {
+ if (!(cacheId in this._sheets)) {
+ this._sheets[cacheId] = [];
+ }
+
+ sheet = new CssSheet(this, aDomSheet, aIndex);
+ if (sheet.sheetAllowed && sheet.contentSheet) {
+ this._ruleCount += sheet.ruleCount;
+ }
+
+ this._sheets[cacheId].push(sheet);
+ }
+
+ return sheet;
+ },
+
+ /**
+ * Process each cached stylesheet in the document using your callback.
+ *
+ * @param {function} aCallback the function you want executed for each of the
+ * CssSheet objects cached.
+ * @param {object} aScope the scope you want for the callback function. aScope
+ * will be the this object when aCallback executes.
+ */
+ forEachSheet: function CssLogic_forEachSheet(aCallback, aScope)
+ {
+ for each (let sheet in this._sheets) {
+ sheet.forEach(aCallback, aScope);
+ }
+ },
+
+ /**
+ * Process *some* cached stylesheets in the document using your callback. The
+ * callback function should return true in order to halt processing.
+ *
+ * @param {function} aCallback the function you want executed for some of the
+ * CssSheet objects cached.
+ * @param {object} aScope the scope you want for the callback function. aScope
+ * will be the this object when aCallback executes.
+ * @return {Boolean} true if aCallback returns true during any iteration,
+ * otherwise false is returned.
+ */
+ forSomeSheets: function CssLogic_forSomeSheets(aCallback, aScope)
+ {
+ for each (let sheets in this._sheets) {
+ if (sheets.some(aCallback, aScope)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Get the number nsIDOMCSSRule objects in the document, counted from all of
+ * the stylesheets. System sheets are excluded. If a filter is active, this
+ * tells only the number of nsIDOMCSSRule objects inside the selected
+ * CSSStyleSheet.
+ *
+ * WARNING: This only provides an estimate of the rule count, and the results
+ * could change at a later date. Todo remove this
+ *
+ * @return {number} the number of nsIDOMCSSRule (all rules).
+ */
+ get ruleCount()
+ {
+ if (!this._sheetsCached) {
+ this._cacheSheets();
+ }
+
+ return this._ruleCount;
+ },
+
+ /**
+ * Process the CssSelector objects that match the highlighted element and its
+ * parent elements. aScope.aCallback() is executed for each CssSelector
+ * object, being passed the CssSelector object and the match status.
+ *
+ * This method also includes all of the element.style properties, for each
+ * highlighted element parent and for the highlighted element itself.
+ *
+ * Note that the matched selectors are cached, such that next time your
+ * callback is invoked for the cached list of CssSelector objects.
+ *
+ * @param {function} aCallback the function you want to execute for each of
+ * the matched selectors.
+ * @param {object} aScope the scope you want for the callback function. aScope
+ * will be the this object when aCallback executes.
+ */
+ processMatchedSelectors: function CL_processMatchedSelectors(aCallback, aScope)
+ {
+ if (this._matchedSelectors) {
+ if (aCallback) {
+ this._passId++;
+ this._matchedSelectors.forEach(function(aValue) {
+ aCallback.call(aScope, aValue[0], aValue[1]);
+ aValue[0]._cssRule._passId = this._passId;
+ }, this);
+ }
+ return;
+ }
+
+ if (!this._matchedRules) {
+ this._buildMatchedRules();
+ }
+
+ this._matchedSelectors = [];
+ this._passId++;
+
+ for (let i = 0; i < this._matchedRules.length; i++) {
+ let rule = this._matchedRules[i][0];
+ let status = this._matchedRules[i][1];
+
+ rule.selectors.forEach(function (aSelector) {
+ if (aSelector._matchId !== this._matchId &&
+ (aSelector.elementStyle ||
+ this.selectorMatchesElement(rule._domRule, aSelector.selectorIndex))) {
+
+ aSelector._matchId = this._matchId;
+ this._matchedSelectors.push([ aSelector, status ]);
+ if (aCallback) {
+ aCallback.call(aScope, aSelector, status);
+ }
+ }
+ }, this);
+
+ rule._passId = this._passId;
+ }
+ },
+
+ /**
+ * Check if the given selector matches the highlighted element or any of its
+ * parents.
+ *
+ * @private
+ * @param {DOMRule} domRule
+ * The DOM Rule containing the selector.
+ * @param {Number} idx
+ * The index of the selector within the DOMRule.
+ * @return {boolean}
+ * true if the given selector matches the highlighted element or any
+ * of its parents, otherwise false is returned.
+ */
+ selectorMatchesElement: function CL_selectorMatchesElement2(domRule, idx)
+ {
+ let element = this.viewedElement;
+ do {
+ if (domUtils.selectorMatchesElement(element, domRule, idx)) {
+ return true;
+ }
+ } while ((element = element.parentNode) &&
+ element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE);
+
+ return false;
+ },
+
+ /**
+ * Check if the highlighted element or it's parents have matched selectors.
+ *
+ * @param {array} aProperties The list of properties you want to check if they
+ * have matched selectors or not.
+ * @return {object} An object that tells for each property if it has matched
+ * selectors or not. Object keys are property names and values are booleans.
+ */
+ hasMatchedSelectors: function CL_hasMatchedSelectors(aProperties)
+ {
+ if (!this._matchedRules) {
+ this._buildMatchedRules();
+ }
+
+ let result = {};
+
+ this._matchedRules.some(function(aValue) {
+ let rule = aValue[0];
+ let status = aValue[1];
+ aProperties = aProperties.filter(function(aProperty) {
+ // We just need to find if a rule has this property while it matches
+ // the viewedElement (or its parents).
+ if (rule.getPropertyValue(aProperty) &&
+ (status == CssLogic.STATUS.MATCHED ||
+ (status == CssLogic.STATUS.PARENT_MATCH &&
+ domUtils.isInheritedProperty(aProperty)))) {
+ result[aProperty] = true;
+ return false;
+ }
+ return true; // Keep the property for the next rule.
+ }.bind(this));
+ return aProperties.length == 0;
+ }, this);
+
+ return result;
+ },
+
+ /**
+ * Build the array of matched rules for the currently highlighted element.
+ * The array will hold rules that match the viewedElement and its parents.
+ *
+ * @private
+ */
+ _buildMatchedRules: function CL__buildMatchedRules()
+ {
+ let domRules;
+ let element = this.viewedElement;
+ let filter = this.sourceFilter;
+ let sheetIndex = 0;
+
+ this._matchId++;
+ this._passId++;
+ this._matchedRules = [];
+
+ if (!element) {
+ return;
+ }
+
+ do {
+ let status = this.viewedElement === element ?
+ CssLogic.STATUS.MATCHED : CssLogic.STATUS.PARENT_MATCH;
+
+ try {
+ domRules = domUtils.getCSSStyleRules(element);
+ } catch (ex) {
+ Services.console.
+ logStringMessage("CL__buildMatchedRules error: " + ex);
+ continue;
+ }
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let domRule = domRules.GetElementAt(i);
+ if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) {
+ continue;
+ }
+
+ let sheet = this.getSheet(domRule.parentStyleSheet, -1);
+ if (sheet._passId !== this._passId) {
+ sheet.index = sheetIndex++;
+ sheet._passId = this._passId;
+ }
+
+ if (filter === CssLogic.FILTER.ALL && !sheet.contentSheet) {
+ continue;
+ }
+
+ let rule = sheet.getRule(domRule);
+ if (rule._passId === this._passId) {
+ continue;
+ }
+
+ rule._matchId = this._matchId;
+ rule._passId = this._passId;
+ this._matchedRules.push([rule, status]);
+ }
+
+
+ // Add element.style information.
+ if (element.style.length > 0) {
+ let rule = new CssRule(null, { style: element.style }, element);
+ rule._matchId = this._matchId;
+ rule._passId = this._passId;
+ this._matchedRules.push([rule, status]);
+ }
+ } while ((element = element.parentNode) &&
+ element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE);
+ },
+
+ /**
+ * Tells if the given DOM CSS object matches the current view media.
+ *
+ * @param {object} aDomObject The DOM CSS object to check.
+ * @return {boolean} True if the DOM CSS object matches the current view
+ * media, or false otherwise.
+ */
+ mediaMatches: function CL_mediaMatches(aDomObject)
+ {
+ let mediaText = aDomObject.media.mediaText;
+ return !mediaText || this.viewedDocument.defaultView.
+ matchMedia(mediaText).matches;
+ },
+};
+
+/**
+ * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where
+ * n is the index of this element in its siblings.
+ * <p>A technically more 'correct' output from the no-id case might be:
+ * 'tagname:nth-of-type(n)' however this is unlikely to be more understood
+ * and it is longer.
+ *
+ * @param {nsIDOMElement} aElement the element for which you want the short name.
+ * @return {string} the string to be displayed for aElement.
+ */
+CssLogic.getShortName = function CssLogic_getShortName(aElement)
+{
+ if (!aElement) {
+ return "null";
+ }
+ if (aElement.id) {
+ return "#" + aElement.id;
+ }
+ let priorSiblings = 0;
+ let temp = aElement;
+ while (temp = temp.previousElementSibling) {
+ priorSiblings++;
+ }
+ return aElement.tagName + "[" + priorSiblings + "]";
+};
+
+/**
+ * Get an array of short names from the given element to document.body.
+ *
+ * @param {nsIDOMElement} aElement the element for which you want the array of
+ * short names.
+ * @return {array} The array of elements.
+ * <p>Each element is an object of the form:
+ * <ul>
+ * <li>{ display: "what to display for the given (parent) element",
+ * <li> element: referenceToTheElement }
+ * </ul>
+ */
+CssLogic.getShortNamePath = function CssLogic_getShortNamePath(aElement)
+{
+ let doc = aElement.ownerDocument;
+ let reply = [];
+
+ if (!aElement) {
+ return reply;
+ }
+
+ // We want to exclude nodes high up the tree (body/html) unless the user
+ // has selected that node, in which case we need to report something.
+ do {
+ reply.unshift({
+ display: CssLogic.getShortName(aElement),
+ element: aElement
+ });
+ aElement = aElement.parentNode;
+ } while (aElement && aElement != doc.body && aElement != doc.head && aElement != doc);
+
+ return reply;
+};
+
+/**
+ * Get a string list of selectors for a given DOMRule.
+ *
+ * @param {DOMRule} aDOMRule
+ * The DOMRule to parse.
+ * @return {Array}
+ * An array of string selectors.
+ */
+CssLogic.getSelectors = function CssLogic_getSelectors(aDOMRule)
+{
+ let selectors = [];
+
+ let len = domUtils.getSelectorCount(aDOMRule);
+ for (let i = 0; i < len; i++) {
+ let text = domUtils.getSelectorText(aDOMRule, i);
+ selectors.push(text);
+ }
+ return selectors;
+}
+
+/**
+ * Memonized lookup of a l10n string from a string bundle.
+ * @param {string} aName The key to lookup.
+ * @returns A localized version of the given key.
+ */
+CssLogic.l10n = function(aName) CssLogic._strings.GetStringFromName(aName);
+
+XPCOMUtils.defineLazyGetter(CssLogic, "_strings", function() Services.strings
+ .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
+
+/**
+ * Is the given property sheet a content stylesheet?
+ *
+ * @param {CSSStyleSheet} aSheet a stylesheet
+ * @return {boolean} true if the given stylesheet is a content stylesheet,
+ * false otherwise.
+ */
+CssLogic.isContentStylesheet = function CssLogic_isContentStylesheet(aSheet)
+{
+ // All sheets with owner nodes have been included by content.
+ if (aSheet.ownerNode) {
+ return true;
+ }
+
+ // If the sheet has a CSSImportRule we need to check the parent stylesheet.
+ if (aSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
+ return CssLogic.isContentStylesheet(aSheet.parentStyleSheet);
+ }
+
+ return false;
+};
+
+/**
+ * Get a source for a stylesheet, taking into account embedded stylesheets
+ * for which we need to use document.defaultView.location.href rather than
+ * sheet.href
+ *
+ * @param {CSSStyleSheet} aSheet the DOM object for the style sheet.
+ * @return {string} the address of the stylesheet.
+ */
+CssLogic.href = function CssLogic_href(aSheet)
+{
+ let href = aSheet.href;
+ if (!href) {
+ href = aSheet.ownerNode.ownerDocument.location;
+ }
+
+ return href;
+};
+
+/**
+ * Return a shortened version of a style sheet's source.
+ *
+ * @param {CSSStyleSheet} aSheet the DOM object for the style sheet.
+ */
+CssLogic.shortSource = function CssLogic_shortSource(aSheet)
+{
+ // Use a string like "inline" if there is no source href
+ if (!aSheet || !aSheet.href) {
+ return CssLogic.l10n("rule.sourceInline");
+ }
+
+ // We try, in turn, the filename, filePath, query string, whole thing
+ let url = {};
+ try {
+ url = Services.io.newURI(aSheet.href, null, null);
+ url = url.QueryInterface(Ci.nsIURL);
+ } catch (ex) {
+ // Some UA-provided stylesheets are not valid URLs.
+ }
+
+ if (url.fileName) {
+ return url.fileName;
+ }
+
+ if (url.filePath) {
+ return url.filePath;
+ }
+
+ if (url.query) {
+ return url.query;
+ }
+
+ let dataUrl = aSheet.href.match(/^(data:[^,]*),/);
+ return dataUrl ? dataUrl[1] : aSheet.href;
+}
+
+/**
+ * Find the position of [element] in [nodeList].
+ * @returns an index of the match, or -1 if there is no match
+ */
+function positionInNodeList(element, nodeList) {
+ for (var i = 0; i < nodeList.length; i++) {
+ if (element === nodeList[i]) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Find a unique CSS selector for a given element
+ * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
+ * and ele.ownerDocument.querySelectorAll(reply).length === 1
+ */
+CssLogic.findCssSelector = function CssLogic_findCssSelector(ele) {
+ var document = ele.ownerDocument;
+ if (ele.id && document.getElementById(ele.id) === ele) {
+ return '#' + ele.id;
+ }
+
+ // Inherently unique by tag name
+ var tagName = ele.tagName.toLowerCase();
+ if (tagName === 'html') {
+ return 'html';
+ }
+ if (tagName === 'head') {
+ return 'head';
+ }
+ if (tagName === 'body') {
+ return 'body';
+ }
+
+ if (ele.parentNode == null) {
+ console.log('danger: ' + tagName);
+ }
+
+ // We might be able to find a unique class name
+ var selector, index, matches;
+ if (ele.classList.length > 0) {
+ for (var i = 0; i < ele.classList.length; i++) {
+ // Is this className unique by itself?
+ selector = '.' + ele.classList.item(i);
+ matches = document.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique with a tag name?
+ selector = tagName + selector;
+ matches = document.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique using a tag name and nth-child
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = selector + ':nth-child(' + index + ')';
+ matches = document.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ }
+ }
+
+ // So we can be unique w.r.t. our parent, and use recursion
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = CssLogic_findCssSelector(ele.parentNode) + ' > ' +
+ tagName + ':nth-child(' + index + ')';
+
+ return selector;
+};
+
+/**
+ * A safe way to access cached bits of information about a stylesheet.
+ *
+ * @constructor
+ * @param {CssLogic} aCssLogic pointer to the CssLogic instance working with
+ * this CssSheet object.
+ * @param {CSSStyleSheet} aDomSheet reference to a DOM CSSStyleSheet object.
+ * @param {number} aIndex tells the index/position of the stylesheet within the
+ * main document.
+ */
+function CssSheet(aCssLogic, aDomSheet, aIndex)
+{
+ this._cssLogic = aCssLogic;
+ this.domSheet = aDomSheet;
+ this.index = this.contentSheet ? aIndex : -100 * aIndex;
+
+ // Cache of the sheets href. Cached by the getter.
+ this._href = null;
+ // Short version of href for use in select boxes etc. Cached by getter.
+ this._shortSource = null;
+
+ // null for uncached.
+ this._sheetAllowed = null;
+
+ // Cached CssRules from the given stylesheet.
+ this._rules = {};
+
+ this._ruleCount = -1;
+}
+
+CssSheet.prototype = {
+ _passId: null,
+ _contentSheet: null,
+ _mediaMatches: null,
+
+ /**
+ * Tells if the stylesheet is provided by the browser or not.
+ *
+ * @return {boolean} false if this is a browser-provided stylesheet, or true
+ * otherwise.
+ */
+ get contentSheet()
+ {
+ if (this._contentSheet === null) {
+ this._contentSheet = CssLogic.isContentStylesheet(this.domSheet);
+ }
+ return this._contentSheet;
+ },
+
+ /**
+ * Tells if the stylesheet is disabled or not.
+ * @return {boolean} true if this stylesheet is disabled, or false otherwise.
+ */
+ get disabled()
+ {
+ return this.domSheet.disabled;
+ },
+
+ /**
+ * Tells if the stylesheet matches the current browser view media.
+ * @return {boolean} true if this stylesheet matches the current browser view
+ * media, or false otherwise.
+ */
+ get mediaMatches()
+ {
+ if (this._mediaMatches === null) {
+ this._mediaMatches = this._cssLogic.mediaMatches(this.domSheet);
+ }
+ return this._mediaMatches;
+ },
+
+ /**
+ * Get a source for a stylesheet, using CssLogic.href
+ *
+ * @return {string} the address of the stylesheet.
+ */
+ get href()
+ {
+ if (this._href) {
+ return this._href;
+ }
+
+ this._href = CssLogic.href(this.domSheet);
+ return this._href;
+ },
+
+ /**
+ * Create a shorthand version of the href of a stylesheet.
+ *
+ * @return {string} the shorthand source of the stylesheet.
+ */
+ get shortSource()
+ {
+ if (this._shortSource) {
+ return this._shortSource;
+ }
+
+ this._shortSource = CssLogic.shortSource(this.domSheet);
+ return this._shortSource;
+ },
+
+ /**
+ * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or
+ * false otherwise.
+ */
+ get sheetAllowed()
+ {
+ if (this._sheetAllowed !== null) {
+ return this._sheetAllowed;
+ }
+
+ this._sheetAllowed = true;
+
+ let filter = this._cssLogic.sourceFilter;
+ if (filter === CssLogic.FILTER.ALL && !this.contentSheet) {
+ this._sheetAllowed = false;
+ }
+ if (filter !== CssLogic.FILTER.ALL && filter !== CssLogic.FILTER.UA) {
+ this._sheetAllowed = (filter === this.href);
+ }
+
+ return this._sheetAllowed;
+ },
+
+ /**
+ * Retrieve the number of rules in this stylesheet.
+ *
+ * @return {number} the number of nsIDOMCSSRule objects in this stylesheet.
+ */
+ get ruleCount()
+ {
+ return this._ruleCount > -1 ?
+ this._ruleCount :
+ this.domSheet.cssRules.length;
+ },
+
+ /**
+ * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is
+ * cached, such that subsequent retrievals return the same CssRule object for
+ * the same CSSStyleRule object.
+ *
+ * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a
+ * CssRule object.
+ * @return {CssRule} the cached CssRule object for the given CSSStyleRule
+ * object.
+ */
+ getRule: function CssSheet_getRule(aDomRule)
+ {
+ let cacheId = aDomRule.type + aDomRule.selectorText;
+
+ let rule = null;
+ let ruleFound = false;
+
+ if (cacheId in this._rules) {
+ for (let i = 0, rulesLen = this._rules[cacheId].length; i < rulesLen; i++) {
+ rule = this._rules[cacheId][i];
+ if (rule._domRule === aDomRule) {
+ ruleFound = true;
+ break;
+ }
+ }
+ }
+
+ if (!ruleFound) {
+ if (!(cacheId in this._rules)) {
+ this._rules[cacheId] = [];
+ }
+
+ rule = new CssRule(this, aDomRule);
+ this._rules[cacheId].push(rule);
+ }
+
+ return rule;
+ },
+
+ /**
+ * Process each rule in this stylesheet using your callback function. Your
+ * function receives one argument: the CssRule object for each CSSStyleRule
+ * inside the stylesheet.
+ *
+ * Note that this method also iterates through @media rules inside the
+ * stylesheet.
+ *
+ * @param {function} aCallback the function you want to execute for each of
+ * the style rules.
+ * @param {object} aScope the scope you want for the callback function. aScope
+ * will be the this object when aCallback executes.
+ */
+ forEachRule: function CssSheet_forEachRule(aCallback, aScope)
+ {
+ let ruleCount = 0;
+ let domRules = this.domSheet.cssRules;
+
+ function _iterator(aDomRule) {
+ if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) {
+ aCallback.call(aScope, this.getRule(aDomRule));
+ ruleCount++;
+ } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE &&
+ aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) {
+ Array.prototype.forEach.call(aDomRule.cssRules, _iterator, this);
+ }
+ }
+
+ Array.prototype.forEach.call(domRules, _iterator, this);
+
+ this._ruleCount = ruleCount;
+ },
+
+ /**
+ * Process *some* rules in this stylesheet using your callback function. Your
+ * function receives one argument: the CssRule object for each CSSStyleRule
+ * inside the stylesheet. In order to stop processing the callback function
+ * needs to return a value.
+ *
+ * Note that this method also iterates through @media rules inside the
+ * stylesheet.
+ *
+ * @param {function} aCallback the function you want to execute for each of
+ * the style rules.
+ * @param {object} aScope the scope you want for the callback function. aScope
+ * will be the this object when aCallback executes.
+ * @return {Boolean} true if aCallback returns true during any iteration,
+ * otherwise false is returned.
+ */
+ forSomeRules: function CssSheet_forSomeRules(aCallback, aScope)
+ {
+ let domRules = this.domSheet.cssRules;
+ function _iterator(aDomRule) {
+ if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) {
+ return aCallback.call(aScope, this.getRule(aDomRule));
+ } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE &&
+ aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) {
+ return Array.prototype.some.call(aDomRule.cssRules, _iterator, this);
+ }
+ }
+ return Array.prototype.some.call(domRules, _iterator, this);
+ },
+
+ toString: function CssSheet_toString()
+ {
+ return "CssSheet[" + this.shortSource + "]";
+ },
+};
+
+/**
+ * Information about a single CSSStyleRule.
+ *
+ * @param {CSSSheet|null} aCssSheet the CssSheet object of the stylesheet that
+ * holds the CSSStyleRule. If the rule comes from element.style, set this
+ * argument to null.
+ * @param {CSSStyleRule|object} aDomRule the DOM CSSStyleRule for which you want
+ * to cache data. If the rule comes from element.style, then provide
+ * an object of the form: {style: element.style}.
+ * @param {Element} [aElement] If the rule comes from element.style, then this
+ * argument must point to the element.
+ * @constructor
+ */
+function CssRule(aCssSheet, aDomRule, aElement)
+{
+ this._cssSheet = aCssSheet;
+ this._domRule = aDomRule;
+
+ let parentRule = aDomRule.parentRule;
+ if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
+ this.mediaText = parentRule.media.mediaText;
+ }
+
+ if (this._cssSheet) {
+ // parse _domRule.selectorText on call to this.selectors
+ this._selectors = null;
+ this.line = domUtils.getRuleLine(this._domRule);
+ this.source = this._cssSheet.shortSource + ":" + this.line;
+ if (this.mediaText) {
+ this.source += " @media " + this.mediaText;
+ }
+ this.href = this._cssSheet.href;
+ this.contentRule = this._cssSheet.contentSheet;
+ } else if (aElement) {
+ this._selectors = [ new CssSelector(this, "@element.style", 0) ];
+ this.line = -1;
+ this.source = CssLogic.l10n("rule.sourceElement");
+ this.href = "#";
+ this.contentRule = true;
+ this.sourceElement = aElement;
+ }
+}
+
+CssRule.prototype = {
+ _passId: null,
+
+ mediaText: "",
+
+ get isMediaRule()
+ {
+ return !!this.mediaText;
+ },
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed()
+ {
+ return this._cssSheet ? this._cssSheet.sheetAllowed : true;
+ },
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex()
+ {
+ return this._cssSheet ? this._cssSheet.index : 0;
+ },
+
+ /**
+ * Retrieve the style property value from the current CSSStyleRule.
+ *
+ * @param {string} aProperty the CSS property name for which you want the
+ * value.
+ * @return {string} the property value.
+ */
+ getPropertyValue: function(aProperty)
+ {
+ return this._domRule.style.getPropertyValue(aProperty);
+ },
+
+ /**
+ * Retrieve the style property priority from the current CSSStyleRule.
+ *
+ * @param {string} aProperty the CSS property name for which you want the
+ * priority.
+ * @return {string} the property priority.
+ */
+ getPropertyPriority: function(aProperty)
+ {
+ return this._domRule.style.getPropertyPriority(aProperty);
+ },
+
+ /**
+ * Retrieve the list of CssSelector objects for each of the parsed selectors
+ * of the current CSSStyleRule.
+ *
+ * @return {array} the array hold the CssSelector objects.
+ */
+ get selectors()
+ {
+ if (this._selectors) {
+ return this._selectors;
+ }
+
+ // Parse the CSSStyleRule.selectorText string.
+ this._selectors = [];
+
+ if (!this._domRule.selectorText) {
+ return this._selectors;
+ }
+
+ let selectors = CssLogic.getSelectors(this._domRule);
+
+ for (let i = 0, len = selectors.length; i < len; i++) {
+ this._selectors.push(new CssSelector(this, selectors[i], i));
+ }
+
+ return this._selectors;
+ },
+
+ toString: function CssRule_toString()
+ {
+ return "[CssRule " + this._domRule.selectorText + "]";
+ },
+};
+
+/**
+ * The CSS selector class allows us to document the ranking of various CSS
+ * selectors.
+ *
+ * @constructor
+ * @param {CssRule} aCssRule the CssRule instance from where the selector comes.
+ * @param {string} aSelector The selector that we wish to investigate.
+ * @param {Number} aIndex The index of the selector within it's rule.
+ */
+function CssSelector(aCssRule, aSelector, aIndex)
+{
+ this._cssRule = aCssRule;
+ this.text = aSelector;
+ this.elementStyle = this.text == "@element.style";
+ this._specificity = null;
+ this.selectorIndex = aIndex;
+}
+
+exports.CssSelector = CssSelector;
+
+CssSelector.prototype = {
+ _matchId: null,
+
+ /**
+ * Retrieve the CssSelector source, which is the source of the CssSheet owning
+ * the selector.
+ *
+ * @return {string} the selector source.
+ */
+ get source()
+ {
+ return this._cssRule.source;
+ },
+
+ /**
+ * Retrieve the CssSelector source element, which is the source of the CssRule
+ * owning the selector. This is only available when the CssSelector comes from
+ * an element.style.
+ *
+ * @return {string} the source element selector.
+ */
+ get sourceElement()
+ {
+ return this._cssRule.sourceElement;
+ },
+
+ /**
+ * Retrieve the address of the CssSelector. This points to the address of the
+ * CssSheet owning this selector.
+ *
+ * @return {string} the address of the CssSelector.
+ */
+ get href()
+ {
+ return this._cssRule.href;
+ },
+
+ /**
+ * Check if the selector comes from a browser-provided stylesheet.
+ *
+ * @return {boolean} true if the selector comes from a content-provided
+ * stylesheet, or false otherwise.
+ */
+ get contentRule()
+ {
+ return this._cssRule.contentRule;
+ },
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed()
+ {
+ return this._cssRule.sheetAllowed;
+ },
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex()
+ {
+ return this._cssRule.sheetIndex;
+ },
+
+ /**
+ * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the line of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleLine()
+ {
+ return this._cssRule.line;
+ },
+
+ /**
+ * Retrieve the pseudo-elements that we support. This list should match the
+ * elements specified in layout/style/nsCSSPseudoElementList.h
+ */
+ get pseudoElements()
+ {
+ if (!CssSelector._pseudoElements) {
+ let pseudos = CssSelector._pseudoElements = new Set();
+ pseudos.add("after");
+ pseudos.add("before");
+ pseudos.add("first-letter");
+ pseudos.add("first-line");
+ pseudos.add("selection");
+ pseudos.add("-moz-focus-inner");
+ pseudos.add("-moz-focus-outer");
+ pseudos.add("-moz-list-bullet");
+ pseudos.add("-moz-list-number");
+ pseudos.add("-moz-math-anonymous");
+ pseudos.add("-moz-math-stretchy");
+ pseudos.add("-moz-progress-bar");
+ pseudos.add("-moz-selection");
+ }
+ return CssSelector._pseudoElements;
+ },
+
+ /**
+ * Retrieve specificity information for the current selector.
+ *
+ * @see http://www.w3.org/TR/css3-selectors/#specificity
+ * @see http://www.w3.org/TR/CSS2/selector.html
+ *
+ * @return {Number} The selector's specificity.
+ */
+ get specificity()
+ {
+ if (this._specificity) {
+ return this._specificity;
+ }
+
+ this._specificity = domUtils.getSpecificity(this._cssRule._domRule,
+ this.selectorIndex);
+
+ return this._specificity;
+ },
+
+ toString: function CssSelector_toString()
+ {
+ return this.text;
+ },
+};
+
+/**
+ * A cache of information about the matched rules, selectors and values attached
+ * to a CSS property, for the highlighted element.
+ *
+ * The heart of the CssPropertyInfo object is the _findMatchedSelectors()
+ * method. This are invoked when the PropertyView tries to access the
+ * .matchedSelectors array.
+ * Results are cached, for later reuse.
+ *
+ * @param {CssLogic} aCssLogic Reference to the parent CssLogic instance
+ * @param {string} aProperty The CSS property we are gathering information for
+ * @constructor
+ */
+function CssPropertyInfo(aCssLogic, aProperty)
+{
+ this._cssLogic = aCssLogic;
+ this.property = aProperty;
+ this._value = "";
+
+ // The number of matched rules holding the this.property style property.
+ // Additionally, only rules that come from allowed stylesheets are counted.
+ this._matchedRuleCount = 0;
+
+ // An array holding CssSelectorInfo objects for each of the matched selectors
+ // that are inside a CSS rule. Only rules that hold the this.property are
+ // counted. This includes rules that come from filtered stylesheets (those
+ // that have sheetAllowed = false).
+ this._matchedSelectors = null;
+}
+
+CssPropertyInfo.prototype = {
+ /**
+ * Retrieve the computed style value for the current property, for the
+ * highlighted element.
+ *
+ * @return {string} the computed style value for the current property, for the
+ * highlighted element.
+ */
+ get value()
+ {
+ if (!this._value && this._cssLogic._computedStyle) {
+ try {
+ this._value = this._cssLogic._computedStyle.getPropertyValue(this.property);
+ } catch (ex) {
+ Services.console.logStringMessage('Error reading computed style for ' +
+ this.property);
+ Services.console.logStringMessage(ex);
+ }
+ }
+
+ return this._value;
+ },
+
+ /**
+ * Retrieve the number of matched rules holding the this.property style
+ * property. Only rules that come from allowed stylesheets are counted.
+ *
+ * @return {number} the number of matched rules.
+ */
+ get matchedRuleCount()
+ {
+ if (!this._matchedSelectors) {
+ this._findMatchedSelectors();
+ } else if (this.needRefilter) {
+ this._refilterSelectors();
+ }
+
+ return this._matchedRuleCount;
+ },
+
+ /**
+ * Retrieve the array holding CssSelectorInfo objects for each of the matched
+ * selectors, from each of the matched rules. Only selectors coming from
+ * allowed stylesheets are included in the array.
+ *
+ * @return {array} the list of CssSelectorInfo objects of selectors that match
+ * the highlighted element and its parents.
+ */
+ get matchedSelectors()
+ {
+ if (!this._matchedSelectors) {
+ this._findMatchedSelectors();
+ } else if (this.needRefilter) {
+ this._refilterSelectors();
+ }
+
+ return this._matchedSelectors;
+ },
+
+ /**
+ * Find the selectors that match the highlighted element and its parents.
+ * Uses CssLogic.processMatchedSelectors() to find the matched selectors,
+ * passing in a reference to CssPropertyInfo._processMatchedSelector() to
+ * create CssSelectorInfo objects, which we then sort
+ * @private
+ */
+ _findMatchedSelectors: function CssPropertyInfo_findMatchedSelectors()
+ {
+ this._matchedSelectors = [];
+ this._matchedRuleCount = 0;
+ this.needRefilter = false;
+
+ this._cssLogic.processMatchedSelectors(this._processMatchedSelector, this);
+
+ // Sort the selectors by how well they match the given element.
+ this._matchedSelectors.sort(function(aSelectorInfo1, aSelectorInfo2) {
+ if (aSelectorInfo1.status > aSelectorInfo2.status) {
+ return -1;
+ } else if (aSelectorInfo2.status > aSelectorInfo1.status) {
+ return 1;
+ } else {
+ return aSelectorInfo1.compareTo(aSelectorInfo2);
+ }
+ });
+
+ // Now we know which of the matches is best, we can mark it BEST_MATCH.
+ if (this._matchedSelectors.length > 0 &&
+ this._matchedSelectors[0].status > CssLogic.STATUS.UNMATCHED) {
+ this._matchedSelectors[0].status = CssLogic.STATUS.BEST;
+ }
+ },
+
+ /**
+ * Process a matched CssSelector object.
+ *
+ * @private
+ * @param {CssSelector} aSelector the matched CssSelector object.
+ * @param {CssLogic.STATUS} aStatus the CssSelector match status.
+ */
+ _processMatchedSelector: function CssPropertyInfo_processMatchedSelector(aSelector, aStatus)
+ {
+ let cssRule = aSelector._cssRule;
+ let value = cssRule.getPropertyValue(this.property);
+ if (value &&
+ (aStatus == CssLogic.STATUS.MATCHED ||
+ (aStatus == CssLogic.STATUS.PARENT_MATCH &&
+ domUtils.isInheritedProperty(this.property)))) {
+ let selectorInfo = new CssSelectorInfo(aSelector, this.property, value,
+ aStatus);
+ this._matchedSelectors.push(selectorInfo);
+ if (this._cssLogic._passId !== cssRule._passId && cssRule.sheetAllowed) {
+ this._matchedRuleCount++;
+ }
+ }
+ },
+
+ /**
+ * Refilter the matched selectors array when the CssLogic.sourceFilter
+ * changes. This allows for quick filter changes.
+ * @private
+ */
+ _refilterSelectors: function CssPropertyInfo_refilterSelectors()
+ {
+ let passId = ++this._cssLogic._passId;
+ let ruleCount = 0;
+
+ let iterator = function(aSelectorInfo) {
+ let cssRule = aSelectorInfo.selector._cssRule;
+ if (cssRule._passId != passId) {
+ if (cssRule.sheetAllowed) {
+ ruleCount++;
+ }
+ cssRule._passId = passId;
+ }
+ };
+
+ if (this._matchedSelectors) {
+ this._matchedSelectors.forEach(iterator);
+ this._matchedRuleCount = ruleCount;
+ }
+
+ this.needRefilter = false;
+ },
+
+ toString: function CssPropertyInfo_toString()
+ {
+ return "CssPropertyInfo[" + this.property + "]";
+ },
+};
+
+/**
+ * A class that holds information about a given CssSelector object.
+ *
+ * Instances of this class are given to CssHtmlTree in the array of matched
+ * selectors. Each such object represents a displayable row in the PropertyView
+ * objects. The information given by this object blends data coming from the
+ * CssSheet, CssRule and from the CssSelector that own this object.
+ *
+ * @param {CssSelector} aSelector The CssSelector object for which to present information.
+ * @param {string} aProperty The property for which information should be retrieved.
+ * @param {string} aValue The property value from the CssRule that owns the selector.
+ * @param {CssLogic.STATUS} aStatus The selector match status.
+ * @constructor
+ */
+function CssSelectorInfo(aSelector, aProperty, aValue, aStatus)
+{
+ this.selector = aSelector;
+ this.property = aProperty;
+ this.value = aValue;
+ this.status = aStatus;
+
+ let priority = this.selector._cssRule.getPropertyPriority(this.property);
+ this.important = (priority === "important");
+}
+
+CssSelectorInfo.prototype = {
+ /**
+ * Retrieve the CssSelector source, which is the source of the CssSheet owning
+ * the selector.
+ *
+ * @return {string} the selector source.
+ */
+ get source()
+ {
+ return this.selector.source;
+ },
+
+ /**
+ * Retrieve the CssSelector source element, which is the source of the CssRule
+ * owning the selector. This is only available when the CssSelector comes from
+ * an element.style.
+ *
+ * @return {string} the source element selector.
+ */
+ get sourceElement()
+ {
+ return this.selector.sourceElement;
+ },
+
+ /**
+ * Retrieve the address of the CssSelector. This points to the address of the
+ * CssSheet owning this selector.
+ *
+ * @return {string} the address of the CssSelector.
+ */
+ get href()
+ {
+ return this.selector.href;
+ },
+
+ /**
+ * Check if the CssSelector comes from element.style or not.
+ *
+ * @return {boolean} true if the CssSelector comes from element.style, or
+ * false otherwise.
+ */
+ get elementStyle()
+ {
+ return this.selector.elementStyle;
+ },
+
+ /**
+ * Retrieve specificity information for the current selector.
+ *
+ * @return {object} an object holding specificity information for the current
+ * selector.
+ */
+ get specificity()
+ {
+ return this.selector.specificity;
+ },
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex()
+ {
+ return this.selector.sheetIndex;
+ },
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed()
+ {
+ return this.selector.sheetAllowed;
+ },
+
+ /**
+ * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the line of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleLine()
+ {
+ return this.selector.ruleLine;
+ },
+
+ /**
+ * Check if the selector comes from a browser-provided stylesheet.
+ *
+ * @return {boolean} true if the selector comes from a browser-provided
+ * stylesheet, or false otherwise.
+ */
+ get contentRule()
+ {
+ return this.selector.contentRule;
+ },
+
+ /**
+ * Compare the current CssSelectorInfo instance to another instance, based on
+ * specificity information.
+ *
+ * @param {CssSelectorInfo} aThat The instance to compare ourselves against.
+ * @return number -1, 0, 1 depending on how aThat compares with this.
+ */
+ compareTo: function CssSelectorInfo_compareTo(aThat)
+ {
+ if (!this.contentRule && aThat.contentRule) return 1;
+ if (this.contentRule && !aThat.contentRule) return -1;
+
+ if (this.elementStyle && !aThat.elementStyle) {
+ if (!this.important && aThat.important) return 1;
+ else return -1;
+ }
+
+ if (!this.elementStyle && aThat.elementStyle) {
+ if (this.important && !aThat.important) return -1;
+ else return 1;
+ }
+
+ if (this.important && !aThat.important) return -1;
+ if (aThat.important && !this.important) return 1;
+
+ if (this.specificity > aThat.specificity) return -1;
+ if (aThat.specificity > this.specificity) return 1;
+
+ if (this.sheetIndex > aThat.sheetIndex) return -1;
+ if (aThat.sheetIndex > this.sheetIndex) return 1;
+
+ if (this.ruleLine > aThat.ruleLine) return -1;
+ if (aThat.ruleLine > this.ruleLine) return 1;
+
+ return 0;
+ },
+
+ toString: function CssSelectorInfo_toString()
+ {
+ return this.selector + " -> " + this.value;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
diff --git a/browser/devtools/styleinspector/cssruleview.xhtml b/browser/devtools/styleinspector/cssruleview.xhtml
new file mode 100644
index 000000000..fa69cf533
--- /dev/null
+++ b/browser/devtools/styleinspector/cssruleview.xhtml
@@ -0,0 +1,38 @@
+<?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 % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd">
+ %inspectorDTD;
+]>
+
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-body">
+
+ <head>
+ <title>&ruleViewTitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/content/devtools/ruleview.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/ruleview.css" type="text/css"/>
+
+ <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8">
+ window.setPanel = function(panel, iframe) {
+ let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let inspector = devtools.require("devtools/styleinspector/style-inspector");
+ this.ruleview = new inspector.RuleViewTool(panel, window, iframe);
+ }
+ window.onunload = function() {
+ if (this.ruleview) {
+ this.ruleview.destroy();
+ }
+ }
+ </script>
+ </head>
+</html>
diff --git a/browser/devtools/styleinspector/moz.build b/browser/devtools/styleinspector/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/styleinspector/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js
new file mode 100644
index 000000000..2983e6ae3
--- /dev/null
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -0,0 +1,1875 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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 {Cc, Ci, Cu} = require("chrome");
+
+let {CssLogic} = require("devtools/styleinspector/css-logic");
+let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * These regular expressions are adapted from firebug's css.js, and are
+ * used to parse CSSStyleDeclaration's cssText attribute.
+ */
+
+// Used to split on css line separators
+const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
+
+// Used to parse a single property line.
+const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
+
+// Used to parse an external resource from a property value
+const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
+
+const IOService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+/**
+ * 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 cssText 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.
+ */
+
+/**
+ * ElementStyle maintains a list of Rule objects for a given element.
+ *
+ * @param {Element} aElement
+ * The element whose style we are viewing.
+ * @param {object} aStore
+ * The ElementStyle can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ *
+ * @constructor
+ */
+function ElementStyle(aElement, aStore)
+{
+ this.element = aElement;
+ this.store = aStore || {};
+
+ // 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();
+ }
+
+ let doc = aElement.ownerDocument;
+
+ // 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.
+ this.dummyElement = doc.createElementNS(this.element.namespaceURI,
+ this.element.tagName);
+ this.populate();
+}
+// We're exporting _ElementStyle for unit tests.
+exports._ElementStyle = ElementStyle;
+
+ElementStyle.prototype = {
+
+ // The element we're looking at.
+ element: null,
+
+ // Empty, unconnected element of the same type as this node, used
+ // to figure out how shorthand properties will be parsed.
+ dummyElement: null,
+
+ /**
+ * Called by the Rule object when it has been changed through the
+ * setProperty* methods.
+ */
+ _changed: function ElementStyle_changed()
+ {
+ 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.
+ */
+ populate: function ElementStyle_populate()
+ {
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ this._refreshRules = this.rules;
+
+ this.rules = [];
+
+ let element = this.element;
+ do {
+ this._addElementRules(element);
+ } while ((element = element.parentNode) &&
+ element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE);
+
+ // Mark overridden computed styles.
+ this.markOverridden();
+
+ // We're done with the previous list of rules.
+ delete this._refreshRules;
+ },
+
+ _addElementRules: function ElementStyle_addElementRules(aElement)
+ {
+ let inherited = aElement !== this.element ? aElement : null;
+
+ // Include the element's style first.
+ this._maybeAddRule({
+ style: aElement.style,
+ selectorText: CssLogic.l10n("rule.sourceElement"),
+ inherited: inherited
+ });
+
+ // Get the styles that apply to the element.
+ var domRules = domUtils.getCSSStyleRules(aElement);
+
+ // getCSStyleRules returns ordered from least-specific to
+ // most-specific.
+ for (let i = domRules.Count() - 1; i >= 0; i--) {
+ let domRule = domRules.GetElementAt(i);
+
+ // XXX: Optionally provide access to system sheets.
+ let contentSheet = CssLogic.isContentStylesheet(domRule.parentStyleSheet);
+ if (!contentSheet) {
+ continue;
+ }
+
+ if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) {
+ continue;
+ }
+
+ this._maybeAddRule({
+ domRule: domRule,
+ inherited: inherited
+ });
+ }
+ },
+
+ /**
+ * Add a rule if it's one we care about. Filters out duplicates and
+ * inherited styles with no inherited properties.
+ *
+ * @param {object} aOptions
+ * Options for creating the Rule, see the Rule constructor.
+ *
+ * @return {bool} true if we added the rule.
+ */
+ _maybeAddRule: function ElementStyle_maybeAddRule(aOptions)
+ {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (aOptions.domRule &&
+ this.rules.some(function(rule) rule.domRule === aOptions.domRule)) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ for (let r of (this._refreshRules || [])) {
+ if (r.matches(aOptions)) {
+ rule = r;
+ rule.refresh();
+ break;
+ }
+ }
+
+ // If this is a new rule, create its Rule object.
+ if (!rule) {
+ rule = new Rule(this, aOptions);
+ }
+
+ // Ignore inherited rules with no properties.
+ if (aOptions.inherited && rule.textProps.length == 0) {
+ return false;
+ }
+
+ this.rules.push(rule);
+ },
+
+ /**
+ * Mark the properties listed in this.rules with an overridden flag
+ * if an earlier property overrides it.
+ */
+ markOverridden: function ElementStyle_markOverridden()
+ {
+ // Gather all the text properties applied by these rules, ordered
+ // from more- to less-specific.
+ let textProps = [];
+ for each (let rule in this.rules) {
+ textProps = textProps.concat(rule.textProps.slice(0).reverse());
+ }
+
+ // Gather all the computed properties applied by those text
+ // properties.
+ let computedProps = [];
+ for each (let textProp in 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 each (let computedProp in computedProps) {
+ let earlier = taken[computedProp.name];
+ let overridden;
+ if (earlier
+ && computedProp.priority === "important"
+ && earlier.priority !== "important") {
+ // 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 each (let textProp in 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} aProp
+ * The text property to update.
+ *
+ * @return {bool} true if the TextProperty's overridden state (or any of its
+ * computed properties overridden state) changed.
+ */
+ _updatePropertyOverridden: function ElementStyle_updatePropertyOverridden(aProp)
+ {
+ let overridden = true;
+ let dirty = false;
+ for each (let computedProp in aProp.computed) {
+ if (!computedProp.overridden) {
+ overridden = false;
+ }
+ dirty = computedProp._overriddenDirty || dirty;
+ delete computedProp._overriddenDirty;
+ }
+
+ dirty = (!!aProp.overridden != overridden) || dirty;
+ aProp.overridden = overridden;
+ return dirty;
+ }
+};
+
+/**
+ * A single style rule or declaration.
+ *
+ * @param {ElementStyle} aElementStyle
+ * The ElementStyle to which this rule belongs.
+ * @param {object} aOptions
+ * The information used to construct this rule. Properties include:
+ * domRule: the nsIDOMCSSStyleRule to view, if any.
+ * style: the nsIDOMCSSStyleDeclaration to view. If omitted,
+ * the domRule's style will be used.
+ * selectorText: selector text to display. If omitted, the domRule's
+ * selectorText will be used.
+ * inherited: An element this rule was inherited from. If omitted,
+ * the rule applies directly to the current element.
+ * @constructor
+ */
+function Rule(aElementStyle, aOptions)
+{
+ this.elementStyle = aElementStyle;
+ this.domRule = aOptions.domRule || null;
+ this.style = aOptions.style || this.domRule.style;
+ this.selectorText = aOptions.selectorText || this.domRule.selectorText;
+ this.inherited = aOptions.inherited || null;
+
+ if (this.domRule) {
+ let parentRule = this.domRule.parentRule;
+ if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
+ this.mediaText = parentRule.media.mediaText;
+ }
+ }
+
+ // Populate the text properties with the style's current cssText
+ // 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()
+ {
+ if (this._title) {
+ return this._title;
+ }
+ this._title = CssLogic.shortSource(this.sheet);
+ if (this.domRule) {
+ this._title += ":" + this.ruleLine;
+ }
+
+ return this._title + (this.mediaText ? " @media " + this.mediaText : "");
+ },
+
+ get inheritedSource()
+ {
+ if (this._inheritedSource) {
+ return this._inheritedSource;
+ }
+ this._inheritedSource = "";
+ if (this.inherited) {
+ let eltText = this.inherited.tagName.toLowerCase();
+ if (this.inherited.id) {
+ eltText += "#" + this.inherited.id;
+ }
+ this._inheritedSource =
+ CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1);
+ }
+ return this._inheritedSource;
+ },
+
+ /**
+ * The rule's stylesheet.
+ */
+ get sheet()
+ {
+ return this.domRule ? this.domRule.parentStyleSheet : null;
+ },
+
+ /**
+ * The rule's line within a stylesheet
+ */
+ get ruleLine()
+ {
+ if (!this.sheet) {
+ // No stylesheet, no ruleLine
+ return null;
+ }
+ return domUtils.getRuleLine(this.domRule);
+ },
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {object} aOptions
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches: function Rule_matches(aOptions)
+ {
+ return (this.style === (aOptions.style || aOptions.domRule.style));
+ },
+
+ /**
+ * Create a new TextProperty to include in the rule.
+ *
+ * @param {string} aName
+ * The text property name (such as "background" or "border-top").
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ */
+ createProperty: function Rule_createProperty(aName, aValue, aPriority)
+ {
+ let prop = new TextProperty(this, aName, aValue, aPriority);
+ this.textProps.push(prop);
+ this.applyProperties();
+ return prop;
+ },
+
+ /**
+ * Reapply all the properties in this rule, and update their
+ * computed styles. Store disabled properties in the element
+ * style's store. Will re-mark overridden properties.
+ *
+ * @param {string} [aName]
+ * A text property name (such as "background" or "border-top") used
+ * when calling from setPropertyValue & setPropertyName to signify
+ * that the property should be saved in store.userProperties.
+ */
+ applyProperties: function Rule_applyProperties(aName)
+ {
+ let disabledProps = [];
+ let store = this.elementStyle.store;
+
+ for each (let prop in this.textProps) {
+ if (!prop.enabled) {
+ disabledProps.push({
+ name: prop.name,
+ value: prop.value,
+ priority: prop.priority
+ });
+ continue;
+ }
+
+ this.style.setProperty(prop.name, prop.value, prop.priority);
+
+ if (aName && prop.name == aName) {
+ store.userProperties.setProperty(
+ this.style, prop.name,
+ this.style.getPropertyValue(prop.name),
+ prop.value);
+ }
+
+ // Refresh the property's priority from the style, to reflect
+ // any changes made during parsing.
+ prop.priority = this.style.getPropertyPriority(prop.name);
+ prop.updateComputed();
+ }
+ this.elementStyle._changed();
+
+ // 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);
+ }
+
+ this.elementStyle.markOverridden();
+ },
+
+ /**
+ * Renames a property.
+ *
+ * @param {TextProperty} aProperty
+ * The property to rename.
+ * @param {string} aName
+ * The new property name (such as "background" or "border-top").
+ */
+ setPropertyName: function Rule_setPropertyName(aProperty, aName)
+ {
+ if (aName === aProperty.name) {
+ return;
+ }
+ this.style.removeProperty(aProperty.name);
+ aProperty.name = aName;
+ this.applyProperties(aName);
+ },
+
+ /**
+ * Sets the value and priority of a property.
+ *
+ * @param {TextProperty} aProperty
+ * The property to manipulate.
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ */
+ setPropertyValue: function Rule_setPropertyValue(aProperty, aValue, aPriority)
+ {
+ if (aValue === aProperty.value && aPriority === aProperty.priority) {
+ return;
+ }
+ aProperty.value = aValue;
+ aProperty.priority = aPriority;
+ this.applyProperties(aProperty.name);
+ },
+
+ /**
+ * Disables or enables given TextProperty.
+ */
+ setPropertyEnabled: function Rule_enableProperty(aProperty, aValue)
+ {
+ aProperty.enabled = !!aValue;
+ if (!aProperty.enabled) {
+ this.style.removeProperty(aProperty.name);
+ }
+ this.applyProperties();
+ },
+
+ /**
+ * Remove a given TextProperty from the rule and update the rule
+ * accordingly.
+ */
+ removeProperty: function Rule_removeProperty(aProperty)
+ {
+ this.textProps = this.textProps.filter(function(prop) prop != aProperty);
+ this.style.removeProperty(aProperty);
+ // Need to re-apply properties in case removing this TextProperty
+ // exposes another one.
+ this.applyProperties();
+ },
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's cssText.
+ */
+ _getTextProperties: function Rule_getTextProperties()
+ {
+ let textProps = [];
+ let store = this.elementStyle.store;
+ let lines = this.style.cssText.match(CSS_LINE_RE);
+ for each (let line in lines) {
+ let matches = CSS_PROP_RE.exec(line);
+ if (!matches || !matches[2])
+ continue;
+
+ let name = matches[1];
+ if (this.inherited && !domUtils.isInheritedProperty(name)) {
+ continue;
+ }
+ let value = store.userProperties.getProperty(this.style, name, matches[2]);
+ let prop = new TextProperty(this, name, value, matches[3] || "");
+ textProps.push(prop);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Return the list of disabled properties from the store for this rule.
+ */
+ _getDisabledProperties: function Rule_getDisabledProperties()
+ {
+ 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 each (let prop in 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 Rule_refresh()
+ {
+ 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 cssText. 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} aNewProp
+ * The current version of the property, as parsed from the
+ * cssText in Rule._getTextProperties().
+ *
+ * @return {bool} true if a property was updated, false if no properties
+ * were updated.
+ */
+ _updateTextProperty: function Rule__updateTextProperty(aNewProp) {
+ let match = { rank: 0, prop: null };
+
+ for each (let prop in this.textProps) {
+ if (prop.name != aNewProp.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 === aNewProp.value) {
+ rank += 2;
+ if (prop.priority === aNewProp.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(aNewProp);
+ return true;
+ }
+
+ return false;
+ },
+};
+
+/**
+ * A single property in a rule's cssText.
+ *
+ * @param {Rule} aRule
+ * The rule this TextProperty came from.
+ * @param {string} aName
+ * The text property name (such as "background" or "border-top").
+ * @param {string} aValue
+ * The property's value (not including priority).
+ * @param {string} aPriority
+ * The property's priority (either "important" or an empty string).
+ *
+ */
+function TextProperty(aRule, aName, aValue, aPriority)
+{
+ this.rule = aRule;
+ this.name = aName;
+ this.value = aValue;
+ this.priority = aPriority;
+ this.enabled = true;
+ this.updateComputed();
+}
+
+TextProperty.prototype = {
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor: function TextProperty_updateEditor()
+ {
+ if (this.editor) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed: function TextProperty_updateComputed()
+ {
+ 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.dummyElement;
+ let dummyStyle = dummyElement.style;
+ dummyStyle.cssText = "";
+ dummyStyle.setProperty(this.name, this.value, this.priority);
+
+ this.computed = [];
+ for (let i = 0, n = dummyStyle.length; i < n; i++) {
+ let prop = dummyStyle.item(i);
+ 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} aOther
+ * The other TextProperty instance.
+ */
+ set: function TextProperty_set(aOther)
+ {
+ let changed = false;
+ for (let item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] != aOther[item]) {
+ this[item] = aOther[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateEditor();
+ }
+ },
+
+ setValue: function TextProperty_setValue(aValue, aPriority)
+ {
+ this.rule.setPropertyValue(this, aValue, aPriority);
+ this.updateEditor();
+ },
+
+ setName: function TextProperty_setName(aName)
+ {
+ this.rule.setPropertyName(this, aName);
+ this.updateEditor();
+ },
+
+ setEnabled: function TextProperty_setEnabled(aValue)
+ {
+ this.rule.setPropertyEnabled(this, aValue);
+ this.updateEditor();
+ },
+
+ remove: function TextProperty_remove()
+ {
+ this.rule.removeProperty(this);
+ }
+};
+
+
+/**
+ * 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 {Document} aDoc
+ * The document that will contain the rule view.
+ * @param {object} aStore
+ * 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 {<iframe>} aOuterIFrame
+ * The iframe containing the ruleview.
+ * @constructor
+ */
+function CssRuleView(aDoc, aStore)
+{
+ this.doc = aDoc;
+ this.store = aStore;
+ this.element = this.doc.createElementNS(HTML_NS, "div");
+ this.element.className = "ruleview devtools-monospace";
+ this.element.flex = 1;
+
+ this._boundCopy = this._onCopy.bind(this);
+ this.element.addEventListener("copy", this._boundCopy);
+
+ this._showEmpty();
+}
+
+exports.CssRuleView = CssRuleView;
+
+CssRuleView.prototype = {
+ // The element that we're inspecting.
+ _viewedElement: null,
+
+ /**
+ * Return {bool} true if the rule view currently has an input editor visible.
+ */
+ get isEditing() {
+ return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0;
+ },
+
+ destroy: function CssRuleView_destroy()
+ {
+ this.clear();
+
+ this.element.removeEventListener("copy", this._boundCopy);
+ delete this._boundCopy;
+
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ },
+
+ /**
+ * Update the highlighted element.
+ *
+ * @param {nsIDOMElement} aElement
+ * The node whose style rules we'll inspect.
+ */
+ highlight: function CssRuleView_highlight(aElement)
+ {
+ if (this._viewedElement === aElement) {
+ return;
+ }
+
+ this.clear();
+
+ if (this._elementStyle) {
+ delete this._elementStyle;
+ }
+
+ this._viewedElement = aElement;
+ if (!this._viewedElement) {
+ this._showEmpty();
+ return;
+ }
+
+ this._elementStyle = new ElementStyle(aElement, this.store);
+ this._elementStyle.onChanged = function() {
+ this._changed();
+ }.bind(this);
+
+ this._createEditors();
+ },
+
+ /**
+ * Update the rules for the currently highlighted element.
+ */
+ nodeChanged: function CssRuleView_nodeChanged()
+ {
+ // Ignore refreshes during editing or when no element is selected.
+ if (this.isEditing || !this._elementStyle) {
+ return;
+ }
+
+ this._clearRules();
+
+ // Repopulate the element style.
+ this._elementStyle.populate();
+
+ // Refresh the rule editors.
+ this._createEditors();
+
+ // Notify anyone that cares that we refreshed.
+ var evt = this.doc.createEvent("Events");
+ evt.initEvent("CssRuleViewRefreshed", true, false);
+ this.element.dispatchEvent(evt);
+ },
+
+ /**
+ * Show the user that the rule view has no node selected.
+ */
+ _showEmpty: function CssRuleView_showEmpty()
+ {
+ if (this.doc.getElementById("noResults") > 0) {
+ return;
+ }
+
+ createChild(this.element, "div", {
+ id: "noResults",
+ textContent: CssLogic.l10n("rule.empty")
+ });
+ },
+
+ /**
+ * Clear the rules.
+ */
+ _clearRules: function CssRuleView_clearRules()
+ {
+ while (this.element.hasChildNodes()) {
+ this.element.removeChild(this.element.lastChild);
+ }
+ },
+
+ /**
+ * Clear the rule view.
+ */
+ clear: function CssRuleView_clear()
+ {
+ this._clearRules();
+ this._viewedElement = null;
+ this._elementStyle = null;
+ },
+
+ /**
+ * Called when the user has made changes to the ElementStyle.
+ * Emits an event that clients can listen to.
+ */
+ _changed: function CssRuleView_changed()
+ {
+ var evt = this.doc.createEvent("Events");
+ evt.initEvent("CssRuleViewChanged", true, false);
+ this.element.dispatchEvent(evt);
+ },
+
+ /**
+ * Creates editor UI for each of the rules in _elementStyle.
+ */
+ _createEditors: function CssRuleView_createEditors()
+ {
+ // Run through the current list of rules, attaching
+ // their editors in order. Create editors if needed.
+ let lastInheritedSource = "";
+ for each (let rule in this._elementStyle.rules) {
+
+ let inheritedSource = rule.inheritedSource;
+ if (inheritedSource != lastInheritedSource) {
+ let h2 = this.doc.createElementNS(HTML_NS, "div");
+ h2.className = "ruleview-rule-inheritance theme-gutter";
+ h2.textContent = inheritedSource;
+ lastInheritedSource = inheritedSource;
+ this.element.appendChild(h2);
+ }
+
+ if (!rule.editor) {
+ new RuleEditor(this, rule);
+ }
+
+ this.element.appendChild(rule.editor.element);
+ }
+ },
+
+ /**
+ * Copy selected text from the rule view.
+ *
+ * @param {Event} aEvent
+ * The event object.
+ */
+ _onCopy: function CssRuleView_onCopy(aEvent)
+ {
+ let target = aEvent.target;
+
+ let text;
+
+ if (target.nodeName == "input") {
+ 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 {
+ let win = this.doc.defaultView;
+ text = win.getSelection().toString();
+
+ // Remove any double newlines.
+ text = text.replace(/(\r?\n)\r?\n/g, "$1");
+
+ // Remove "inline"
+ let inline = _strings.GetStringFromName("rule.sourceInline");
+ let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
+ text = text.replace(rx, "");
+ }
+
+ clipboardHelper.copyString(text, this.doc);
+
+ aEvent.preventDefault();
+ },
+
+};
+
+/**
+ * Create a RuleEditor.
+ *
+ * @param {CssRuleView} aRuleView
+ * The CssRuleView containg the document holding this rule editor.
+ * @param {Rule} aRule
+ * The Rule object we're editing.
+ * @constructor
+ */
+function RuleEditor(aRuleView, aRule)
+{
+ this.ruleView = aRuleView;
+ this.doc = this.ruleView.doc;
+ this.rule = aRule;
+ this.rule.editor = this;
+
+ this._onNewProperty = this._onNewProperty.bind(this);
+ this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ _create: function RuleEditor_create()
+ {
+ this.element = this.doc.createElementNS(HTML_NS, "div");
+ this.element.className = "ruleview-rule theme-separator";
+ 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.
+ let source = createChild(this.element, "div", {
+ class: "ruleview-rule-source theme-link"
+ });
+ source.addEventListener("click", function() {
+ let rule = this.rule;
+ let evt = this.doc.createEvent("CustomEvent");
+ evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, {
+ rule: rule,
+ });
+ this.element.dispatchEvent(evt);
+ }.bind(this));
+ let sourceLabel = this.doc.createElementNS(XUL_NS, "label");
+ sourceLabel.setAttribute("crop", "center");
+ sourceLabel.setAttribute("value", this.rule.title);
+ sourceLabel.setAttribute("tooltiptext", this.rule.title);
+ source.appendChild(sourceLabel);
+
+ let code = createChild(this.element, "div", {
+ class: "ruleview-code"
+ });
+
+ let header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selector theme-fg-color3"
+ });
+
+ this.openBrace = createChild(header, "span", {
+ class: "ruleview-ruleopen",
+ textContent: " {"
+ });
+
+ code.addEventListener("click", function() {
+ let selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed) {
+ this.newProperty();
+ }
+ }.bind(this), false);
+
+ this.element.addEventListener("mousedown", function() {
+ this.doc.defaultView.focus();
+
+ let editorNodes =
+ this.doc.querySelectorAll(".styleinspector-propertyeditor");
+
+ if (editorNodes) {
+ for (let node of editorNodes) {
+ if (node.inplaceEditor) {
+ node.inplaceEditor._clear();
+ }
+ }
+ }
+ }.bind(this), false);
+
+ this.propertyList = createChild(code, "ul", {
+ class: "ruleview-propertylist"
+ });
+
+ this.populate();
+
+ this.closeBrace = createChild(code, "div", {
+ class: "ruleview-ruleclose",
+ tabindex: "0",
+ textContent: "}"
+ });
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, function(aElement) {
+ this.newProperty();
+ }.bind(this));
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ */
+ populate: function RuleEditor_populate()
+ {
+ // 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 && this.rule.domRule.selectorText) {
+ let selectors = CssLogic.getSelectors(this.rule.domRule);
+ let element = this.rule.inherited || this.ruleView._viewedElement;
+ for (let i = 0; i < selectors.length; i++) {
+ let selector = selectors[i];
+ if (i != 0) {
+ createChild(this.selectorText, "span", {
+ class: "ruleview-selector-separator",
+ textContent: ", "
+ });
+ }
+ let cls;
+ if (domUtils.selectorMatchesElement(element, this.rule.domRule, i)) {
+ cls = "ruleview-selector-matched";
+ } else {
+ cls = "ruleview-selector-unmatched";
+ }
+ createChild(this.selectorText, "span", {
+ class: cls,
+ textContent: selector
+ });
+ }
+ } else {
+ this.selectorText.textContent = this.rule.selectorText;
+ }
+
+ for (let prop of this.rule.textProps) {
+ if (!prop.editor) {
+ new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(prop.editor.element);
+ }
+ }
+ },
+
+ /**
+ * Programatically add a new property to the rule.
+ *
+ * @param {string} aName
+ * Property name.
+ * @param {string} aValue
+ * Property value.
+ * @param {string} aPriority
+ * Property priority.
+ */
+ addProperty: function RuleEditor_addProperty(aName, aValue, aPriority)
+ {
+ let prop = this.rule.createProperty(aName, aValue, aPriority);
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ },
+
+ /**
+ * 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 RuleEditor_newProperty()
+ {
+ // 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"
+ });
+
+ new InplaceEditor({
+ element: this.newPropSpan,
+ done: this._onNewProperty,
+ destroy: this._newPropertyDestroy,
+ advanceChars: ":"
+ });
+ },
+
+ /**
+ * Called when the new property input has been dismissed.
+ * Will create a new TextProperty if necessary.
+ *
+ * @param {string} aValue
+ * The value in the editor.
+ * @param {bool} aCommit
+ * True if the value should be committed.
+ */
+ _onNewProperty: function RuleEditor__onNewProperty(aValue, aCommit)
+ {
+ if (!aValue || !aCommit) {
+ return;
+ }
+
+ // Create an empty-valued property and start editing it.
+ let prop = this.rule.createProperty(aValue, "", "");
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ editor.valueSpan.click();
+ },
+
+ /**
+ * Called when the new property editor is destroyed.
+ */
+ _newPropertyDestroy: function RuleEditor__newPropertyDestroy()
+ {
+ // 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;
+ }
+};
+
+/**
+ * Create a TextPropertyEditor.
+ *
+ * @param {RuleEditor} aRuleEditor
+ * The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} aProperty
+ * The text property to edit.
+ * @constructor
+ */
+function TextPropertyEditor(aRuleEditor, aProperty)
+{
+ this.doc = aRuleEditor.doc;
+ this.prop = aProperty;
+ this.prop.editor = this;
+ this.browserWindow = this.doc.defaultView.top;
+
+ let sheet = this.prop.rule.sheet;
+ let href = sheet ? CssLogic.href(sheet) : null;
+ if (href) {
+ this.sheetURI = IOService.newURI(href, null, null);
+ }
+
+ 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._create();
+ this.update();
+}
+
+TextPropertyEditor.prototype = {
+ get editing() {
+ return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor);
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create: function TextPropertyEditor_create()
+ {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+
+ // The enable checkbox will disable or enable the rule.
+ this.enable = createChild(this.element, "div", {
+ class: "ruleview-enableproperty theme-checkbox",
+ tabindex: "-1"
+ });
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+
+ // Click to expand the computed properties of the text property.
+ this.expander = createChild(this.element, "span", {
+ class: "ruleview-expander theme-twisty"
+ });
+ this.expander.addEventListener("click", this._onExpandClicked, true);
+
+ this.nameContainer = createChild(this.element, "span", {
+ class: "ruleview-namecontainer"
+ });
+ this.nameContainer.addEventListener("click", function(aEvent) {
+ // Clicks within the name shouldn't propagate any further.
+ aEvent.stopPropagation();
+ if (aEvent.target === propertyContainer) {
+ this.nameSpan.click();
+ }
+ }.bind(this), false);
+
+ // 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: "0",
+ });
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.nameSpan,
+ done: this._onNameDone,
+ advanceChars: ':'
+ });
+
+ 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.
+ let propertyContainer = createChild(this.element, "span", {
+ class: "ruleview-propertycontainer"
+ });
+ propertyContainer.addEventListener("click", function(aEvent) {
+ // Clicks within the value shouldn't propagate any further.
+ aEvent.stopPropagation();
+ if (aEvent.target === propertyContainer) {
+ this.valueSpan.click();
+ }
+ }.bind(this), false);
+
+ // 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(propertyContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ tabindex: "0",
+ });
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ this.committed = { name: this.prop.name,
+ value: this.prop.value,
+ priority: this.prop.priority };
+
+ appendText(propertyContainer, ";");
+
+ this.warning = createChild(this.element, "div", {
+ hidden: "",
+ class: "ruleview-warning",
+ title: CssLogic.l10n("rule.warning.title"),
+ });
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.valueSpan,
+ done: this._onValueDone,
+ validate: this._validate.bind(this),
+ warning: this.warning,
+ advanceChars: ';'
+ });
+ },
+
+ /**
+ * Resolve a URI based on the rule stylesheet
+ * @param {string} relativePath the path to resolve
+ * @return {string} the resolved path.
+ */
+ resolveURI: function(relativePath)
+ {
+ if (this.sheetURI) {
+ relativePath = this.sheetURI.resolve(relativePath);
+ }
+ return relativePath;
+ },
+
+ /**
+ * Check the property value to find an external resource (if any).
+ * @return {string} the URI in the property value, or null if there is no match.
+ */
+ getResourceURI: function()
+ {
+ let val = this.prop.value;
+ let uriMatch = CSS_RESOURCE_RE.exec(val);
+ let uri = null;
+
+ if (uriMatch && uriMatch[1]) {
+ uri = uriMatch[1];
+ }
+
+ return uri;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ update: function TextPropertyEditor_update()
+ {
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ this.enable.setAttribute("checked", "");
+ } else {
+ this.enable.style.visibility = "visible";
+ this.enable.removeAttribute("checked");
+ }
+
+ if (this.prop.overridden && !this.editing) {
+ this.element.classList.add("ruleview-overridden");
+ } else {
+ this.element.classList.remove("ruleview-overridden");
+ }
+
+ let name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ let val = this.prop.value;
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ // Treat URLs differently than other properties.
+ // Allow the user to click a link to the resource and open it.
+ let resourceURI = this.getResourceURI();
+ if (resourceURI) {
+ this.valueSpan.textContent = "";
+
+ appendText(this.valueSpan, val.split(resourceURI)[0]);
+
+ let a = createChild(this.valueSpan, "a", {
+ target: "_blank",
+ class: "theme-link",
+ textContent: resourceURI,
+ href: this.resolveURI(resourceURI)
+ });
+
+ a.addEventListener("click", (aEvent) => {
+
+ // Clicks within the link shouldn't trigger editing.
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+
+ this.browserWindow.openUILinkIn(aEvent.target.href, "tab");
+
+ }, false);
+
+ appendText(this.valueSpan, val.split(resourceURI)[1]);
+ } else {
+ this.valueSpan.textContent = val;
+ }
+
+ this.warning.hidden = this._validate();
+
+ let store = this.prop.rule.elementStyle.store;
+ let propDirty = store.userProperties.contains(this.prop.rule.style, name);
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ // Populate the computed styles.
+ this._updateComputed();
+ },
+
+ _onStartEditing: function TextPropertyEditor_onStartEditing()
+ {
+ this.element.classList.remove("ruleview-overridden");
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _updateComputed: function TextPropertyEditor_updateComputed()
+ {
+ // Clear out existing viewers.
+ while (this.computed.hasChildNodes()) {
+ this.computed.removeChild(this.computed.lastChild);
+ }
+
+ let showExpander = false;
+ for each (let computed in this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ showExpander = true;
+
+ 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, ": ");
+
+ createChild(li, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ textContent: computed.value
+ });
+ appendText(li, ";");
+ }
+
+ // Show or hide the expander as needed.
+ if (showExpander) {
+ this.expander.style.visibility = "visible";
+ } else {
+ this.expander.style.visibility = "hidden";
+ }
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableClicked: function TextPropertyEditor_onEnableClicked(aEvent)
+ {
+ let checked = this.enable.hasAttribute("checked");
+ if (checked) {
+ this.enable.removeAttribute("checked");
+ } else {
+ this.enable.setAttribute("checked", "");
+ }
+ this.prop.setEnabled(!checked);
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the computed property expander.
+ */
+ _onExpandClicked: function TextPropertyEditor_onExpandClicked(aEvent)
+ {
+ this.computed.classList.toggle("styleinspector-open");
+ if (this.computed.classList.contains("styleinspector-open")) {
+ this.expander.setAttribute("open", "true");
+ } else {
+ this.expander.removeAttribute("open");
+ }
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Called when the property name's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {string} aValue
+ * The value contained in the editor.
+ * @param {boolean} aCommit
+ * True if the change should be applied.
+ */
+ _onNameDone: function TextPropertyEditor_onNameDone(aValue, aCommit)
+ {
+ if (!aCommit) {
+ if (this.prop.overridden) {
+ this.element.classList.add("ruleview-overridden");
+ }
+
+ return;
+ }
+ if (!aValue) {
+ this.prop.remove();
+ this.element.parentNode.removeChild(this.element);
+ return;
+ }
+ this.prop.setName(aValue);
+ },
+
+ /**
+ * Pull priority (!important) out of the value provided by a
+ * value editor.
+ *
+ * @param {string} aValue
+ * The value from the text editor.
+ * @return {object} an object with 'value' and 'priority' properties.
+ */
+ _parseValue: function TextPropertyEditor_parseValue(aValue)
+ {
+ let pieces = aValue.split("!", 2);
+ return {
+ value: pieces[0].trim(),
+ priority: (pieces.length > 1 ? pieces[1].trim() : "")
+ };
+ },
+
+ /**
+ * Called when a value editor closes. If the user pressed escape,
+ * revert to the value this property had before editing.
+ *
+ * @param {string} aValue
+ * The value contained in the editor.
+ * @param {bool} aCommit
+ * True if the change should be applied.
+ */
+ _onValueDone: function PropertyEditor_onValueDone(aValue, aCommit)
+ {
+ if (aCommit) {
+ let val = this._parseValue(aValue);
+ this.prop.setValue(val.value, val.priority);
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+ if (this.prop.overridden) {
+ this.element.classList.add("ruleview-overridden");
+ }
+ } else {
+ this.prop.setValue(this.committed.value, this.committed.priority);
+ }
+ },
+
+ /**
+ * Validate this property.
+ *
+ * @param {string} [aValue]
+ * Override the actual property value used for validation without
+ * applying property values e.g. validate as you type.
+ *
+ * @return {bool} true if the property value is valid, false otherwise.
+ */
+ _validate: function TextPropertyEditor_validate(aValue)
+ {
+ let name = this.prop.name;
+ let value = typeof aValue == "undefined" ? this.prop.value : aValue;
+ let val = this._parseValue(value);
+ let style = this.doc.createElementNS(HTML_NS, "div").style;
+ let prefs = Services.prefs;
+
+ // We toggle output of errors whilst the user is typing a property value.
+ let prefVal = Services.prefs.getBoolPref("layout.css.report_errors");
+ prefs.setBoolPref("layout.css.report_errors", false);
+
+ try {
+ style.setProperty(name, val.value, val.priority);
+ } finally {
+ prefs.setBoolPref("layout.css.report_errors", prefVal);
+ }
+ return !!style.getPropertyValue(name);
+ },
+};
+
+/**
+ * Store of CSSStyleDeclarations mapped to properties that have been changed by
+ * the user.
+ */
+function UserProperties()
+{
+ this.weakMap = new WeakMap();
+}
+
+UserProperties.prototype = {
+ /**
+ * Get a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property is mapped.
+ * @param {string} aName
+ * The name of the property to get.
+ * @param {string} aComputedValue
+ * The computed value of the property. The user value will only be
+ * returned if the computed value hasn't changed since, and this will
+ * be returned as the default if no user value is available.
+ * @return {string}
+ * The property value if it has previously been set by the user, null
+ * otherwise.
+ */
+ getProperty: function UP_getProperty(aStyle, aName, aComputedValue) {
+ let entry = this.weakMap.get(aStyle, null);
+
+ if (entry && aName in entry) {
+ let item = entry[aName];
+ if (item.computed != aComputedValue) {
+ delete entry[aName];
+ return aComputedValue;
+ }
+
+ return item.user;
+ }
+ return aComputedValue;
+
+ },
+
+ /**
+ * Set a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property is to be mapped.
+ * @param {String} aName
+ * The name of the property to set.
+ * @param {String} aComputedValue
+ * The computed property value. The user value will not be used if the
+ * computed value changes.
+ * @param {String} aUserValue
+ * The value of the property to set.
+ */
+ setProperty: function UP_setProperty(aStyle, aName, aComputedValue, aUserValue) {
+ let entry = this.weakMap.get(aStyle, null);
+ if (entry) {
+ entry[aName] = { computed: aComputedValue, user: aUserValue };
+ } else {
+ let props = {};
+ props[aName] = { computed: aComputedValue, user: aUserValue };
+ this.weakMap.set(aStyle, props);
+ }
+ },
+
+ /**
+ * Check whether a named property for a given CSSStyleDeclaration is stored.
+ *
+ * @param {CSSStyleDeclaration} aStyle
+ * The CSSStyleDeclaration against which the property would be mapped.
+ * @param {String} aName
+ * The name of the property to check.
+ */
+ contains: function UP_contains(aStyle, aName) {
+ let entry = this.weakMap.get(aStyle, null);
+ return !!entry && aName in entry;
+ },
+};
+
+/**
+ * Helper functions
+ */
+
+/**
+ * Create a child element with a set of attributes.
+ *
+ * @param {Element} aParent
+ * The parent node.
+ * @param {string} aTag
+ * The tag name.
+ * @param {object} aAttributes
+ * A set of attributes to set on the node.
+ */
+function createChild(aParent, aTag, aAttributes)
+{
+ let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
+ for (let attr in aAttributes) {
+ if (aAttributes.hasOwnProperty(attr)) {
+ if (attr === "textContent") {
+ elt.textContent = aAttributes[attr];
+ } else {
+ elt.setAttribute(attr, aAttributes[attr]);
+ }
+ }
+ }
+ aParent.appendChild(elt);
+ return elt;
+}
+
+function createMenuItem(aMenu, aAttributes)
+{
+ let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
+ item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
+ item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
+ item.addEventListener("command", aAttributes.command);
+
+ aMenu.appendChild(item);
+
+ return item;
+}
+
+/**
+ * Append a text node to an element.
+ */
+function appendText(aParent, aText)
+{
+ aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
+}
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+});
+
+XPCOMUtils.defineLazyGetter(this, "_strings", function() {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/styleinspector.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
diff --git a/browser/devtools/styleinspector/ruleview.css b/browser/devtools/styleinspector/ruleview.css
new file mode 100644
index 000000000..086e506fc
--- /dev/null
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -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/. */
+
+#root {
+ display: -moz-box;
+}
+
+.ruleview {
+ overflow: auto;
+ -moz-user-select: text;
+}
+
+.ruleview-code {
+ direction: ltr;
+}
+
+.ruleview-property:not(:hover) > .ruleview-enableproperty {
+ pointer-events: none;
+}
+
+.ruleview-namecontainer {
+ cursor: text;
+}
+
+.ruleview-propertycontainer {
+ cursor: text;
+ padding-right: 15px;
+}
+
+.ruleview-propertycontainer a {
+ cursor: pointer;
+}
+
+.ruleview-computedlist:not(.styleinspector-open),
+.ruleview-warning[hidden] {
+ display: none;
+}
diff --git a/browser/devtools/styleinspector/style-inspector.js b/browser/devtools/styleinspector/style-inspector.js
new file mode 100644
index 000000000..b318d9ea6
--- /dev/null
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -0,0 +1,238 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+const {Cc, Cu, Ci} = require("chrome");
+
+let ToolDefinitions = require("main").Tools;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+loader.lazyGetter(this, "gDevTools", () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
+loader.lazyGetter(this, "RuleView", () => require("devtools/styleinspector/rule-view"));
+loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/computed-view"));
+loader.lazyGetter(this, "_strings", () => Services.strings
+ .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
+loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
+
+// This module doesn't currently export any symbols directly, it only
+// registers inspector tools.
+
+function RuleViewTool(aInspector, aWindow, aIFrame)
+{
+ this.inspector = aInspector;
+ this.doc = aWindow.document;
+ this.outerIFrame = aIFrame;
+
+ this.view = new RuleView.CssRuleView(this.doc, null);
+ this.doc.documentElement.appendChild(this.view.element);
+
+ this._changeHandler = function() {
+ this.inspector.markDirty();
+ }.bind(this);
+
+ this.view.element.addEventListener("CssRuleViewChanged", this._changeHandler)
+
+ this._cssLinkHandler = function(aEvent) {
+ let contentDoc = this.inspector.selection.document;
+ let rule = aEvent.detail.rule;
+ let line = rule.ruleLine || 0;
+ let styleSheet = rule.sheet;
+ let styleSheets = contentDoc.styleSheets;
+ let contentSheet = false;
+
+ // 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.
+ //
+ // Array.prototype.indexOf always returns -1 here so we loop through
+ // the styleSheets object instead.
+ for each (let sheet in styleSheets) {
+ if (sheet == styleSheet) {
+ contentSheet = true;
+ break;
+ }
+ }
+
+ if (contentSheet) {
+ let target = this.inspector.target;
+
+ if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
+ gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
+ toolbox.getCurrentPanel().selectStyleSheet(styleSheet.href, line);
+ });
+ }
+ } else {
+ let href = styleSheet ? styleSheet.href : "";
+ if (rule.elementStyle.element) {
+ href = rule.elementStyle.element.ownerDocument.location.href;
+ }
+ let viewSourceUtils = this.inspector.viewSourceUtils;
+ viewSourceUtils.viewSource(href, null, contentDoc, line);
+ }
+ }.bind(this);
+
+ this.view.element.addEventListener("CssRuleViewCSSLinkClicked",
+ this._cssLinkHandler);
+
+ this._onSelect = this.onSelect.bind(this);
+ this.inspector.selection.on("detached", this._onSelect);
+ this.inspector.selection.on("new-node", this._onSelect);
+ this.refresh = this.refresh.bind(this);
+ this.inspector.on("layout-change", this.refresh);
+ this.inspector.sidebar.on("ruleview-selected", this.refresh);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.on("locked", this._onSelect);
+ }
+
+ this.onSelect();
+}
+
+exports.RuleViewTool = RuleViewTool;
+
+RuleViewTool.prototype = {
+ onSelect: function RVT_onSelect(aEvent) {
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.view.highlight(null);
+ return;
+ }
+
+ if (!aEvent || aEvent == "new-node") {
+ if (this.inspector.selection.reason == "highlighter") {
+ this.view.highlight(null);
+ } else {
+ this.view.highlight(this.inspector.selection.node);
+ }
+ }
+
+ if (aEvent == "locked") {
+ this.view.highlight(this.inspector.selection.node);
+ }
+ },
+
+ isActive: function RVT_isActive() {
+ return this.inspector.sidebar.getCurrentTabID() == "ruleview";
+ },
+
+ refresh: function RVT_refresh() {
+ if (this.isActive()) {
+ this.view.nodeChanged();
+ }
+ },
+
+ destroy: function RVT_destroy() {
+ this.inspector.off("layout-change", this.refresh);
+ this.inspector.sidebar.off("ruleview-selected", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node", this._onSelect);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.off("locked", this._onSelect);
+ }
+
+ this.view.element.removeEventListener("CssRuleViewCSSLinkClicked",
+ this._cssLinkHandler);
+
+ this.view.element.removeEventListener("CssRuleViewChanged",
+ this._changeHandler);
+
+ this.doc.documentElement.removeChild(this.view.element);
+
+ this.view.destroy();
+
+ delete this.outerIFrame;
+ delete this.view;
+ delete this.doc;
+ delete this.inspector;
+ }
+}
+
+function ComputedViewTool(aInspector, aWindow, aIFrame)
+{
+ this.inspector = aInspector;
+ this.window = aWindow;
+ this.document = aWindow.document;
+ this.outerIFrame = aIFrame;
+ this.cssLogic = new CssLogic();
+ this.view = new ComputedView.CssHtmlTree(this);
+
+ this._onSelect = this.onSelect.bind(this);
+ this.inspector.selection.on("detached", this._onSelect);
+ this.inspector.selection.on("new-node", this._onSelect);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.on("locked", this._onSelect);
+ }
+ this.refresh = this.refresh.bind(this);
+ this.inspector.on("layout-change", this.refresh);
+ this.inspector.sidebar.on("computedview-selected", this.refresh);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+
+ this.cssLogic.highlight(null);
+ this.view.highlight(null);
+
+ this.onSelect();
+}
+
+exports.ComputedViewTool = ComputedViewTool;
+
+ComputedViewTool.prototype = {
+ onSelect: function CVT_onSelect(aEvent)
+ {
+ if (!this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ this.view.highlight(null);
+ return;
+ }
+
+ if (!aEvent || aEvent == "new-node") {
+ if (this.inspector.selection.reason == "highlighter") {
+ // FIXME: We should hide view's content
+ } else {
+ this.cssLogic.highlight(this.inspector.selection.node);
+ this.view.highlight(this.inspector.selection.node);
+ }
+ }
+
+ if (aEvent == "locked") {
+ this.cssLogic.highlight(this.inspector.selection.node);
+ this.view.highlight(this.inspector.selection.node);
+ }
+ },
+
+ isActive: function CVT_isActive() {
+ return this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ refresh: function CVT_refresh() {
+ if (this.isActive()) {
+ this.cssLogic.highlight(this.inspector.selection.node);
+ this.view.refreshPanel();
+ }
+ },
+
+ destroy: function CVT_destroy(aContext)
+ {
+ this.inspector.off("layout-change", this.refresh);
+ this.inspector.sidebar.off("computedview-selected", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node", this._onSelect);
+ if (this.inspector.highlighter) {
+ this.inspector.highlighter.off("locked", this._onSelect);
+ }
+
+ this.view.destroy();
+ delete this.view;
+
+ delete this.outerIFrame;
+ delete this.cssLogic;
+ delete this.cssHtmlTree;
+ delete this.window;
+ delete this.document;
+ delete this.inspector;
+ }
+}
diff --git a/browser/devtools/styleinspector/test/Makefile.in b/browser/devtools/styleinspector/test/Makefile.in
new file mode 100644
index 000000000..7d68cf613
--- /dev/null
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_styleinspector.js \
+ browser_bug683672.js \
+ browser_styleinspector_bug_672746_default_styles.js \
+ browser_styleinspector_bug_672744_search_filter.js \
+ $(filter awaiting-promise-based-init, browser_bug589375_keybindings.js) \
+ browser_styleinspector_bug_689759_no_results_placeholder.js \
+ browser_bug_692400_element_style.js \
+ browser_csslogic_inherited.js \
+ browser_ruleview_734259_style_editor_link.js \
+ browser_ruleview_editor.js \
+ browser_ruleview_editor_changedvalues.js \
+ browser_ruleview_copy.js \
+ browser_ruleview_focus.js \
+ browser_ruleview_inherit.js \
+ browser_ruleview_manipulation.js \
+ browser_ruleview_override.js \
+ browser_ruleview_ui.js \
+ browser_ruleview_update.js \
+ browser_bug705707_is_content_stylesheet.js \
+ browser_bug722196_property_view_media_queries.js \
+ browser_bug722196_rule_view_media_queries.js \
+ browser_bug_592743_specificity.js \
+ browser_bug722691_rule_view_increment.js \
+ browser_computedview_734259_style_editor_link.js \
+ browser_computedview_copy.js\
+ browser_styleinspector_bug_677930_urls_clickable.js \
+ head.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_FILES += \
+ browser_bug683672.html \
+ browser_bug705707_is_content_stylesheet.html \
+ browser_bug705707_is_content_stylesheet_imported.css \
+ browser_bug705707_is_content_stylesheet_imported2.css \
+ browser_bug705707_is_content_stylesheet_linked.css \
+ browser_bug705707_is_content_stylesheet_script.css \
+ browser_bug705707_is_content_stylesheet.xul \
+ browser_bug705707_is_content_stylesheet_xul.css \
+ browser_bug722196_identify_media_queries.html \
+ browser_styleinspector_bug_677930_urls_clickable.html \
+ browser_styleinspector_bug_677930_urls_clickable \
+ browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css \
+ test-image.png \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/styleinspector/test/browser_bug589375_keybindings.js b/browser/devtools/styleinspector/test/browser_bug589375_keybindings.js
new file mode 100644
index 000000000..37e5a042e
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug589375_keybindings.js
@@ -0,0 +1,141 @@
+/* vim: set 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 key bindings work properly.
+
+let doc;
+let inspector;
+let computedView;
+let iframe;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span class="matches">Some styled text</span>' +
+ '</div>';
+ doc.title = "Style Inspector key binding test";
+
+ openInspector(openComputedView);
+}
+
+function openComputedView(aInspector)
+{
+ inspector = aInspector;
+ iframe = inspector._toolbox.frame;
+
+ Services.obs.addObserver(runTests, "StyleInspector-populated", false);
+
+ inspector.sidebar.select("computedview");
+}
+
+function runTests()
+{
+ Services.obs.removeObserver(runTests, "StyleInspector-populated");
+ computedView = getComputedView(inspector);
+
+ var span = doc.querySelector(".matches");
+ ok(span, "captain, we have the matches span");
+
+ inspector.selection.setNode(span);
+
+ is(span, computedView.viewedElement,
+ "style inspector node matches the selected node");
+ is(computedView.viewedElement, computedView.cssLogic.viewedElement,
+ "cssLogic node matches the cssHtmlTree node");
+
+ info("checking keybindings");
+
+ let searchbar = computedView.searchField;
+ let propView = getFirstVisiblePropertyView();
+ let rulesTable = propView.matchedSelectorsContainer;
+ let matchedExpander = propView.matchedExpander;
+
+ info("Adding focus event handler to search filter");
+ searchbar.addEventListener("focus", function searchbarFocused() {
+ searchbar.removeEventListener("focus", searchbarFocused);
+ info("search filter is focused");
+ info("tabbing to property expander node");
+ EventUtils.synthesizeKey("VK_TAB", {}, iframe.contentWindow);
+ });
+
+ info("Adding focus event handler to property expander");
+ matchedExpander.addEventListener("focus", function expanderFocused() {
+ matchedExpander.removeEventListener("focus", expanderFocused);
+ info("property expander is focused");
+ info("checking expand / collapse");
+ testKey(iframe.contentWindow, "VK_SPACE", rulesTable);
+ testKey(iframe.contentWindow, "VK_RETURN", rulesTable);
+
+ checkHelpLinkKeybinding();
+ computedView.destroy();
+ finishUp();
+ });
+
+ info("Making sure that the style inspector panel is focused");
+ SimpleTest.waitForFocus(function windowFocused() {
+ info("window is focused");
+ info("focusing search filter");
+ searchbar.focus();
+ }, iframe.contentWindow);
+}
+
+function getFirstVisiblePropertyView()
+{
+ let propView = null;
+ computedView.propertyViews.some(function(aPropView) {
+ if (aPropView.visible) {
+ propView = aPropView;
+ return true;
+ }
+ return false;
+ });
+
+ return propView;
+}
+
+function testKey(aContext, aVirtKey, aRulesTable)
+{
+ info("testing " + aVirtKey + " key");
+ info("expanding rules table");
+ EventUtils.synthesizeKey(aVirtKey, {}, aContext);
+ isnot(aRulesTable.innerHTML, "", "rules Table is populated");
+ info("collapsing rules table");
+ EventUtils.synthesizeKey(aVirtKey, {}, aContext);
+ is(aRulesTable.innerHTML, "", "rules Table is not populated");
+}
+
+function checkHelpLinkKeybinding()
+{
+ info("checking help link keybinding");
+ let propView = getFirstVisiblePropertyView();
+
+ info("check that MDN link is opened on \"F1\"");
+ let linkClicked = false;
+ propView.mdnLinkClick = function(aEvent) {
+ linkClicked = true;
+ };
+ EventUtils.synthesizeKey("VK_F1", {}, iframe.contentWindow);
+ is(linkClicked, true, "MDN link will be shown");
+}
+
+function finishUp()
+{
+ doc = inspector = iframe = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,default styles test";
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug683672.html b/browser/devtools/styleinspector/test/browser_bug683672.html
new file mode 100644
index 000000000..8fe007409
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug683672.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/browser/devtools/styleinspector/test/browser_bug683672.js b/browser/devtools/styleinspector/test/browser_bug683672.js
new file mode 100644
index 000000000..0189e848f
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug683672.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/ */
+
+// Tests that the style inspector works properly
+
+let doc;
+let inspector;
+let div;
+let computedView;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/test/browser_bug683672.html";
+
+let tempScope = {};
+let {CssHtmlTree, PropertyView} = devtools.require("devtools/styleinspector/computed-view");
+
+function test()
+{
+ waitForExplicitFinish();
+ addTab(TEST_URI);
+ browser.addEventListener("load", tabLoaded, true);
+}
+
+function tabLoaded()
+{
+ browser.removeEventListener("load", tabLoaded, true);
+ doc = content.document;
+ openInspector(selectNode);
+}
+
+function selectNode(aInspector)
+{
+ inspector = aInspector;
+
+ div = content.document.getElementById("test");
+ ok(div, "captain, we have the div");
+
+ inspector.selection.setNode(div);
+
+ inspector.sidebar.once("computedview-ready", function() {
+ computedView = getComputedView(inspector);
+
+ inspector.sidebar.select("computedview");
+ runTests();
+ });
+}
+
+function runTests()
+{
+ testMatchedSelectors();
+
+ info("finishing up");
+ finishUp();
+}
+
+function testMatchedSelectors()
+{
+ info("checking selector counts, matched rules and titles");
+
+ is(div, computedView.viewedElement,
+ "style inspector node matches the selected node");
+
+ let propertyView = new PropertyView(computedView, "color");
+ let numMatchedSelectors = propertyView.propertyInfo.matchedSelectors.length;
+
+ is(numMatchedSelectors, 6,
+ "CssLogic returns the correct number of matched selectors for div");
+
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+}
+
+function finishUp()
+{
+ doc = inspector = div = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.html b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.html
new file mode 100644
index 000000000..96ef5b6e9
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.html
@@ -0,0 +1,33 @@
+<html>
+<head>
+ <title>test</title>
+
+ <link href="./browser_bug705707_is_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
+
+ <script>
+ // Load script.css
+ function loadCSS() {
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = "./browser_bug705707_is_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/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.js b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.js
new file mode 100644
index 000000000..13de2a1ff
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.js
@@ -0,0 +1,102 @@
+/* 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 correct stylesheets origins are identified in HTML & XUL
+// stylesheets
+
+let doc;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/" +
+ "test/browser_bug705707_is_content_stylesheet.html";
+const TEST_URI2 = "http://example.com/browser/browser/devtools/styleinspector/" +
+ "test/browser_bug705707_is_content_stylesheet.xul";
+const XUL_URI = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(TEST_URI2, null, null);
+const XUL_PRINCIPAL = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager)
+ .getNoAppCodebasePrincipal(XUL_URI);
+
+
+let {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+
+function test()
+{
+ waitForExplicitFinish();
+ addTab(TEST_URI);
+ browser.addEventListener("load", htmlLoaded, true);
+}
+
+function htmlLoaded()
+{
+ browser.removeEventListener("load", htmlLoaded, true);
+ doc = content.document;
+ testFromHTML()
+}
+
+function testFromHTML()
+{
+ let target = doc.querySelector("#target");
+
+ executeSoon(function() {
+ checkSheets(target);
+ gBrowser.removeCurrentTab();
+ openXUL();
+ });
+}
+
+function openXUL()
+{
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.ALLOW_ACTION);
+ addTab(TEST_URI2);
+ browser.addEventListener("load", xulLoaded, true);
+}
+
+function xulLoaded()
+{
+ browser.removeEventListener("load", xulLoaded, true);
+ doc = content.document;
+ testFromXUL()
+}
+
+function testFromXUL()
+{
+ let target = doc.querySelector("#target");
+
+ executeSoon(function() {
+ checkSheets(target);
+ finishUp();
+ });
+}
+
+function checkSheets(aTarget)
+{
+ let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+ .getService(Ci.inIDOMUtils);
+ let domRules = domUtils.getCSSStyleRules(aTarget);
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let domRule = domRules.GetElementAt(i);
+ let sheet = domRule.parentStyleSheet;
+ let isContentSheet = CssLogic.isContentStylesheet(sheet);
+
+ if (!sheet.href ||
+ /browser_bug705707_is_content_stylesheet_/.test(sheet.href)) {
+ ok(isContentSheet, sheet.href + " identified as content stylesheet");
+ } else {
+ ok(!isContentSheet, sheet.href + " identified as non-content stylesheet");
+ }
+ }
+}
+
+function finishUp()
+{
+ info("finishing up");
+ Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager)
+ .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.DENY_ACTION);
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.xul b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet.xul
new file mode 100644
index 000000000..abbd03030
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_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="./browser_bug705707_is_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/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported.css b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported.css
new file mode 100644
index 000000000..4092afeea
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported.css
@@ -0,0 +1,5 @@
+@import url("./browser_bug705707_is_content_stylesheet_imported2.css");
+
+#target {
+ text-decoration: underline;
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported2.css b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported2.css
new file mode 100644
index 000000000..77c73299e
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_imported2.css
@@ -0,0 +1,3 @@
+#target {
+ text-decoration: underline;
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_linked.css b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_linked.css
new file mode 100644
index 000000000..712ba78fb
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_linked.css
@@ -0,0 +1,3 @@
+table {
+ border-collapse: collapse;
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_script.css b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_script.css
new file mode 100644
index 000000000..bf4fc8ddc
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_script.css
@@ -0,0 +1,5 @@
+@import url("./browser_bug705707_is_content_stylesheet_imported.css");
+
+table {
+ opacity: 1;
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_xul.css b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_xul.css
new file mode 100644
index 000000000..a14ae7f6f
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug705707_is_content_stylesheet_xul.css
@@ -0,0 +1,3 @@
+#target {
+ font-size: 200px;
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug722196_identify_media_queries.html b/browser/devtools/styleinspector/test/browser_bug722196_identify_media_queries.html
new file mode 100644
index 000000000..1adb8bc7a
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug722196_identify_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/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js b/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js
new file mode 100644
index 000000000..9fe1b464a
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.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/ */
+
+// Tests that we correctly display appropriate media query titles in the
+// property view.
+
+let doc;
+let computedView;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/" +
+ "test/browser_bug722196_identify_media_queries.html";
+
+function test()
+{
+ waitForExplicitFinish();
+ addTab(TEST_URI);
+ browser.addEventListener("load", docLoaded, true);
+}
+
+function docLoaded()
+{
+ browser.removeEventListener("load", docLoaded, true);
+ doc = content.document;
+
+ openInspector(selectNode);
+}
+
+function selectNode(aInspector)
+{
+ var div = doc.querySelector("div");
+ ok(div, "captain, we have the div");
+
+ aInspector.selection.setNode(div);
+
+ aInspector.sidebar.once("computedview-ready", function() {
+ aInspector.sidebar.select("computedview");
+ computedView = getComputedView(aInspector);
+ checkSheets();
+ });
+}
+
+function checkSheets()
+{
+ let cssLogic = computedView.cssLogic;
+ cssLogic.processMatchedSelectors();
+
+ let _strings = Services.strings
+ .createBundle("chrome://browser/locale/devtools/styleinspector.properties");
+
+ let inline = _strings.GetStringFromName("rule.sourceInline");
+
+ let source1 = inline + ":8";
+ let source2 = inline + ":15 @media screen and (min-width: 1px)";
+ is(cssLogic._matchedRules[0][0].source, source1,
+ "rule.source gives correct output for rule 1");
+ is(cssLogic._matchedRules[1][0].source, source2,
+ "rule.source gives correct output for rule 2");
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug722196_rule_view_media_queries.js b/browser/devtools/styleinspector/test/browser_bug722196_rule_view_media_queries.js
new file mode 100644
index 000000000..93d5b9625
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug722196_rule_view_media_queries.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/ */
+
+// Tests that we correctly display appropriate media query titles in the
+// rule view.
+
+let doc;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/" +
+ "test/browser_bug722196_identify_media_queries.html";
+
+function test()
+{
+ waitForExplicitFinish();
+ addTab(TEST_URI);
+ browser.addEventListener("load", docLoaded, true);
+}
+
+function docLoaded()
+{
+ browser.removeEventListener("load", docLoaded, true);
+ doc = content.document;
+ checkSheets();
+}
+
+function checkSheets()
+{
+ var div = doc.querySelector("div");
+ ok(div, "captain, we have the div");
+
+ let elementStyle = new _ElementStyle(div);
+ is(elementStyle.rules.length, 3, "Should have 3 rules.");
+
+ let _strings = Services.strings
+ .createBundle("chrome://browser/locale/devtools/styleinspector.properties");
+
+ let inline = _strings.GetStringFromName("rule.sourceInline");
+
+ is(elementStyle.rules[0].title, inline, "check rule 0 title");
+ is(elementStyle.rules[1].title, inline +
+ ":15 @media screen and (min-width: 1px)", "check rule 1 title");
+ is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title");
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
new file mode 100644
index 000000000..e74b842a1
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
@@ -0,0 +1,198 @@
+/* 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 increasing/decreasing values in rule view using
+// arrow keys works correctly.
+
+let doc;
+let ruleDialog;
+let ruleView;
+
+function setUpTests()
+{
+ doc.body.innerHTML = '<div id="test" style="' +
+ 'margin-top:0px;' +
+ 'padding-top: 0px;' +
+ 'color:#000000;' +
+ 'background-color: #000000; >"'+
+ '</div>';
+ let testElement = doc.getElementById("test");
+ ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xhtml",
+ "cssruleviewtest",
+ "width=350,height=350");
+ ruleDialog.addEventListener("load", function onLoad(evt) {
+ ruleDialog.removeEventListener("load", onLoad, true);
+ let doc = ruleDialog.document;
+ ruleView = new CssRuleView(doc);
+ doc.documentElement.appendChild(ruleView.element);
+ ruleView.highlight(testElement);
+ waitForFocus(runTests, ruleDialog);
+ }, true);
+}
+
+function runTests()
+{
+ let idRuleEditor = ruleView.element.children[0]._ruleEditor;
+ let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+ let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+ let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+ let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor;
+
+ (function() {
+ info("INCREMENTS");
+ newTest( marginPropEditor, {
+ 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,
+ nextTest: test2 }
+ });
+ EventUtils.synthesizeMouse(marginPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ })();
+
+ function test2() {
+ info("UNITS");
+ newTest( paddingPropEditor, {
+ 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,
+ nextTest: test3 }
+ });
+ EventUtils.synthesizeMouse(paddingPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ };
+
+ function test3() {
+ info("HEX COLORS");
+ newTest( hexColorPropEditor, {
+ 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,
+ nextTest: test4 }
+ });
+ EventUtils.synthesizeMouse(hexColorPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ };
+
+ function test4() {
+ info("RGB COLORS");
+ newTest( rgbColorPropEditor, {
+ 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],
+ nextTest: test5 }
+ });
+ EventUtils.synthesizeMouse(rgbColorPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ };
+
+ function test5() {
+ info("SHORTHAND");
+ newTest( paddingPropEditor, {
+ 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,
+ nextTest: test6 }
+ });
+ EventUtils.synthesizeMouse(paddingPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ };
+
+ function test6() {
+ info("ODD CASES");
+ newTest( marginPropEditor, {
+ 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: "1" },
+ 4: { down: true, start: "0", end: "-1" },
+ 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],
+ endTest: true }
+ });
+ EventUtils.synthesizeMouse(marginPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+ };
+}
+
+function newTest( propEditor, tests )
+{
+ waitForEditorFocus(propEditor.element, function onElementFocus(aEditor) {
+ for( test in tests) {
+ testIncrement( aEditor, tests[test] );
+ }
+ }, false);
+}
+
+function testIncrement( aEditor, aOptions )
+{
+ aEditor.input.value = aOptions.start;
+ let input = aEditor.input;
+ if ( aOptions.selectAll ) {
+ input.select();
+ } else if ( aOptions.selection ) {
+ input.setSelectionRange(aOptions.selection[0], aOptions.selection[1]);
+ }
+ is(input.value, aOptions.start, "Value initialized at " + aOptions.start);
+ input.addEventListener("keyup", function onIncrementUp() {
+ input.removeEventListener("keyup", onIncrementUp, false);
+ input = aEditor.input;
+ is(input.value, aOptions.end, "Value changed to " + aOptions.end);
+ if( aOptions.nextTest) {
+ aOptions.nextTest();
+ }
+ else if( aOptions.endTest ) {
+ finishTest();
+ }
+ }, false);
+ let key;
+ key = ( aOptions.down ) ? "VK_DOWN" : "VK_UP";
+ key = ( aOptions.pageDown ) ? "VK_PAGE_DOWN" : ( aOptions.pageUp ) ? "VK_PAGE_UP" : key;
+ EventUtils.synthesizeKey(key,
+ {altKey: aOptions.alt, shiftKey: aOptions.shift},
+ ruleDialog);
+}
+
+function finishTest()
+{
+ ruleView.clear();
+ ruleDialog.close();
+ ruleDialog = ruleView = null;
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onload, true);
+ doc = content.document;
+ waitForFocus(setUpTests, content);
+ }, true);
+ content.location = "data:text/html,sample document for bug 722691";
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug_592743_specificity.js b/browser/devtools/styleinspector/test/browser_bug_592743_specificity.js
new file mode 100644
index 000000000..237d45b1e
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug_592743_specificity.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/ */
+
+// Tests that CSS specificity is properly calculated.
+
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+ .getService(Ci.inIDOMUtils);
+
+function createDocument()
+{
+ let doc = content.document;
+ doc.body.innerHTML = getStylesheetText();
+ doc.title = "Computed view specificity test";
+ runTests(doc);
+}
+
+function runTests(doc) {
+ let cssLogic = new CssLogic();
+ cssLogic.highlight(doc.body);
+
+ let tests = getTests();
+ let cssSheet = cssLogic.sheets[0];
+ let cssRule = cssSheet.domSheet.cssRules[0];
+ let selectors = CssLogic.getSelectors(cssRule);
+
+ for (let i = 0; i < selectors.length; i++) {
+ let selectorText = selectors[i];
+ let selector = new CssSelector(cssRule, selectorText, i);
+ let expected = getExpectedSpecificity(selectorText);
+ let specificity = DOMUtils.getSpecificity(selector._cssRule,
+ selector.selectorIndex)
+ is(specificity, expected,
+ 'selector "' + selectorText + '" has a specificity of ' + expected);
+ }
+ finishUp();
+}
+
+function getExpectedSpecificity(selectorText) {
+ let tests = getTests();
+
+ for (let test of tests) {
+ if (test.text == selectorText) {
+ return test.expected;
+ }
+ }
+}
+
+function getTests() {
+ return [
+ {text: "*", expected: 0},
+ {text: "LI", expected: 1},
+ {text: "UL LI", expected: 2},
+ {text: "UL OL + LI", expected: 3},
+ {text: "H1 + [REL=\"up\"]", expected: 257},
+ {text: "UL OL LI.red", expected: 259},
+ {text: "LI.red.level", expected: 513},
+ {text: ".red .level", expected: 512},
+ {text: "#x34y", expected: 65536},
+ {text: "#s12:not(FOO)", expected: 65537},
+ {text: "body#home div#warning p.message", expected: 131331},
+ {text: "* body#home div#warning p.message", expected: 131331},
+ {text: "#footer :not(nav) li", expected: 65538},
+ {text: "bar:nth-child(n)", expected: 257},
+ {text: "li::-moz-list-number", expected: 1},
+ {text: "a:hover", expected: 257},
+ ];
+}
+
+function getStylesheetText() {
+ let tests = getTests();
+ let text = "";
+
+ tests.forEach(function(test) {
+ if (text.length > 0) {
+ text += ",";
+ }
+ text += test.text;
+ });
+ return '<style type="text/css">' + text + " {color:red;}</style>";
+}
+
+function finishUp()
+{
+ CssLogic = CssSelector = tempScope = null;
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,Computed view specificity test";
+}
diff --git a/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js b/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js
new file mode 100644
index 000000000..484ea28af
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug_692400_element_style.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/ */
+
+// Tests for selector text errors.
+
+let doc;
+let computedView;
+
+function createDocument()
+{
+ doc.body.innerHTML = "<div style='color:blue;'></div>";
+
+ doc.title = "Style Inspector Selector Text Test";
+
+ openInspector(openComputedView);
+}
+
+
+function openComputedView(aInspector)
+{
+ let div = doc.querySelector("div");
+ ok(div, "captain, we have the test div");
+
+ aInspector.selection.setNode(div);
+
+ aInspector.sidebar.once("computedview-ready", function() {
+ aInspector.sidebar.select("computedview");
+ computedView = getComputedView(aInspector);
+
+ Services.obs.addObserver(SI_checkText, "StyleInspector-populated", false);
+ });
+}
+
+function SI_checkText()
+{
+ Services.obs.removeObserver(SI_checkText, "StyleInspector-populated");
+
+ let propertyView = null;
+ computedView.propertyViews.some(function(aView) {
+ if (aView.name == "color") {
+ propertyView = aView;
+ return true;
+ }
+ return false;
+ });
+
+ ok(propertyView, "found PropertyView for color");
+
+ is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
+
+ propertyView.matchedExpanded = true;
+ 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");
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,selector text test, bug 692400";
+}
diff --git a/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js b/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js
new file mode 100644
index 000000000..a3091aa06
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js
@@ -0,0 +1,175 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let inspector;
+let computedView;
+
+const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
+ [".highlight {",
+ "color: blue",
+ "}"].join("\n"));
+
+const DOCUMENT_URL = "data:text/html,"+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>',
+ '<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>'].join("\n"));
+
+
+
+function selectNode(aInspector)
+{
+ inspector = aInspector;
+
+ let span = doc.querySelector("span");
+ ok(span, "captain, we have the span");
+
+ aInspector.selection.setNode(span);
+
+ aInspector.sidebar.once("computedview-ready", function() {
+ aInspector.sidebar.select("computedview");
+
+ computedView = getComputedView(aInspector);
+
+ Services.obs.addObserver(testInlineStyle, "StyleInspector-populated", false);
+ });
+}
+
+function testInlineStyle()
+{
+ Services.obs.removeObserver(testInlineStyle, "StyleInspector-populated");
+
+ info("expanding property");
+ expandProperty(0, function propertyExpanded() {
+ Services.ww.registerNotification(function onWindow(aSubject, aTopic) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+ info("window opened");
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function windowLoad() {
+ win.removeEventListener("load", windowLoad);
+ info("window load completed");
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ is(windowType, "navigator:view-source", "view source window is open");
+ info("closing window");
+ win.close();
+ Services.ww.unregisterNotification(onWindow);
+ executeSoon(() => {
+ testInlineStyleSheet();
+ });
+ });
+ });
+ let link = getLinkByIndex(0);
+ link.click();
+ });
+}
+
+function testInlineStyleSheet()
+{
+ info("clicking an inline stylesheet");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.once("styleeditor-selected", () => {
+ let panel = toolbox.getCurrentPanel();
+
+ panel.UI.once("editor-selected", (event, editor) => {
+ validateStyleEditorSheet(editor, 0);
+ executeSoon(() => {
+ testExternalStyleSheet(toolbox);
+ });
+ });
+ });
+
+ let link = getLinkByIndex(2);
+ link.click();
+}
+
+function testExternalStyleSheet(toolbox) {
+ info ("clicking an external stylesheet");
+
+ let panel = toolbox.getCurrentPanel();
+ panel.UI.once("editor-selected", (event, editor) => {
+ is(toolbox.currentToolId, "styleeditor", "style editor selected");
+ validateStyleEditorSheet(editor, 1);
+ finishUp();
+ });
+
+ toolbox.selectTool("inspector").then(function () {
+ info("inspector selected");
+ let link = getLinkByIndex(1);
+ link.click();
+ });
+}
+
+function validateStyleEditorSheet(aEditor, aExpectedSheetIndex)
+{
+ info("validating style editor stylesheet");
+ let sheet = doc.styleSheets[aExpectedSheetIndex];
+ is(aEditor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet");
+}
+
+function expandProperty(aIndex, aCallback)
+{
+ let contentDoc = computedView.styleDocument;
+ let expando = contentDoc.querySelectorAll(".expandable")[aIndex];
+ expando.click();
+
+ // We use executeSoon to give the property time to expand.
+ executeSoon(aCallback);
+}
+
+function getLinkByIndex(aIndex)
+{
+ let contentDoc = computedView.styleDocument;
+ let links = contentDoc.querySelectorAll(".rule-link .link");
+ return links[aIndex];
+}
+
+function finishUp()
+{
+ doc = inspector = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
+ true);
+ doc = content.document;
+ waitForFocus(function () { openInspector(selectNode); }, content);
+ }, true);
+
+ content.location = DOCUMENT_URL;
+}
diff --git a/browser/devtools/styleinspector/test/browser_computedview_copy.js b/browser/devtools/styleinspector/test/browser_computedview_copy.js
new file mode 100644
index 000000000..67337a8e6
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_copy.js
@@ -0,0 +1,148 @@
+/* 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 style inspector works properly
+
+let doc;
+let win;
+let computedView;
+
+XPCOMUtils.defineLazyGetter(this, "osString", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+});
+
+function createDocument()
+{
+ doc.body.innerHTML = '<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">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<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>\n' +
+ '<p id="closing">more text</p>\n' +
+ '<p>even more text</p>' +
+ '</div>';
+ doc.title = "Computed view context menu test";
+
+ openInspector(selectNode)
+}
+
+function selectNode(aInspector)
+{
+ let span = doc.querySelector("span");
+ ok(span, "captain, we have the span");
+
+ aInspector.selection.setNode(span);
+
+ aInspector.sidebar.once("computedview-ready", function() {
+ aInspector.sidebar.select("computedview");
+
+ computedView = getComputedView(aInspector);
+ win = aInspector.sidebar.getWindowForTab("computedview");
+
+ Services.obs.addObserver(runStyleInspectorTests,
+ "StyleInspector-populated", false);
+ });
+}
+
+
+function runStyleInspectorTests()
+{
+ Services.obs.removeObserver(runStyleInspectorTests,
+ "StyleInspector-populated", false);
+
+ let contentDocument = computedView.styleDocument;
+ let prop = contentDocument.querySelector(".property-view");
+ ok(prop, "captain, we have the property-view node");
+
+ checkCopySelection();
+}
+
+function checkCopySelection()
+{
+ let contentDocument = computedView.styleDocument;
+ let props = contentDocument.querySelectorAll(".property-view");
+ ok(props, "captain, we have the property-view nodes");
+
+ let range = document.createRange();
+ range.setStart(props[0], 0);
+ range.setEnd(props[3], 3);
+ win.getSelection().addRange(range);
+
+ info("Checking that cssHtmlTree.siBoundCopy() " +
+ " returns the correct clipboard value");
+
+ let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" +
+ "font-family: helvetica,sans-serif;[\\r\\n]+" +
+ "font-size: 16px;[\\r\\n]+" +
+ "font-variant: small-caps;[\\r\\n]*";
+
+ SimpleTest.waitForClipboard(function CS_boundCopyCheck() {
+ return checkClipboardData(expectedPattern);
+ },
+ function() {fireCopyEvent(props[0])}, closeStyleInspector, function() {
+ failedClipboard(expectedPattern, closeStyleInspector);
+ });
+}
+
+function checkClipboardData(aExpectedPattern)
+{
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(aExpectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(aExpectedPattern, aCallback)
+{
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ aExpectedPattern = aExpectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ aExpectedPattern = aExpectedPattern.replace(/\\\(/g, "(");
+ aExpectedPattern = aExpectedPattern.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.
+ aExpectedPattern = aExpectedPattern.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(aExpectedPattern));
+ aCallback();
+}
+
+function closeStyleInspector()
+{
+ finishUp();
+}
+
+function finishUp()
+{
+ computedView = doc = win = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,computed view context menu test";
+}
diff --git a/browser/devtools/styleinspector/test/browser_csslogic_inherited.js b/browser/devtools/styleinspector/test/browser_csslogic_inherited.js
new file mode 100644
index 000000000..e56c869d0
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_csslogic_inherited.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/ */
+
+// Test that inherited properties are treated correctly.
+
+let doc;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<div style="margin-left:10px; font-size: 5px"><div id="innerdiv">Inner div</div></div>';
+ doc.title = "Style Inspector Inheritance Test";
+
+ let cssLogic = new CssLogic();
+ cssLogic.highlight(doc.getElementById("innerdiv"));
+
+ let marginProp = cssLogic.getPropertyInfo("margin-left");
+ is(marginProp.matchedRuleCount, 0, "margin-left should not be included in matched selectors.");
+
+ let fontSizeProp = cssLogic.getPropertyInfo("font-size");
+ is(fontSizeProp.matchedRuleCount, 1, "font-size should be included in matched selectors.");
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,selector text test, bug 692400";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js b/browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js
new file mode 100644
index 000000000..9dbcfed11
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js
@@ -0,0 +1,175 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let win;
+let doc;
+let contentWindow;
+let inspector;
+let toolbox;
+
+let tempScope = {};
+Cu.import("resource://gre/modules/Services.jsm", tempScope);
+let Services = tempScope.Services;
+
+const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
+ ["#first {",
+ "color: blue",
+ "}"].join("\n"));
+
+const DOCUMENT_URL = "data:text/html,"+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>',
+ '<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>'].join("\n"));
+
+function openToolbox() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(aToolbox) {
+ toolbox = aToolbox;
+ inspector = toolbox.getCurrentPanel();
+ inspector.sidebar.select("ruleview");
+ highlightNode();
+ });
+}
+
+function highlightNode()
+{
+ // Highlight a node.
+ let div = content.document.getElementsByTagName("div")[0];
+
+ inspector.selection.once("new-node", function() {
+ is(inspector.selection.node, div, "selection matches the div element");
+ testInlineStyle();
+ });
+ executeSoon(function() {
+ inspector.selection.setNode(div);
+ });
+}
+
+function testInlineStyle()
+{
+ executeSoon(function() {
+ info("clicking an inline style");
+
+ Services.ww.registerNotification(function onWindow(aSubject, aTopic) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function windowLoad() {
+ win.removeEventListener("load", windowLoad);
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ is(windowType, "navigator:view-source", "view source window is open");
+ win.close();
+ Services.ww.unregisterNotification(onWindow);
+ executeSoon(() => {
+ testInlineStyleSheet();
+ });
+ });
+ });
+
+ let link = getLinkByIndex(0);
+ link.scrollIntoView();
+ link.click();
+ });
+}
+
+function testInlineStyleSheet()
+{
+ info("clicking an inline stylesheet");
+
+ toolbox.once("styleeditor-ready", function(id, aToolbox) {
+ let panel = toolbox.getCurrentPanel();
+
+ panel.UI.once("editor-selected", (event, editor) => {
+ validateStyleEditorSheet(editor, 0);
+ executeSoon(() => {
+ testExternalStyleSheet(toolbox);
+ });
+ });
+ });
+
+ let link = getLinkByIndex(2);
+ link.scrollIntoView();
+ link.click();
+}
+
+function testExternalStyleSheet(toolbox) {
+ info ("clicking an external stylesheet");
+
+ let panel = toolbox.getCurrentPanel();
+ panel.UI.once("editor-selected", (event, editor) => {
+ is(toolbox.currentToolId, "styleeditor", "style editor tool selected");
+ validateStyleEditorSheet(editor, 1);
+ finishUp();
+ });
+
+ toolbox.selectTool("inspector").then(function () {
+ let link = getLinkByIndex(1);
+ link.scrollIntoView();
+ link.click();
+ });
+}
+
+function validateStyleEditorSheet(aEditor, aExpectedSheetIndex)
+{
+ info("validating style editor stylesheet");
+ let sheet = doc.styleSheets[aExpectedSheetIndex];
+ is(aEditor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet");
+}
+
+function getLinkByIndex(aIndex)
+{
+ let contentDoc = ruleView().doc;
+ contentWindow = contentDoc.defaultView;
+ let links = contentDoc.querySelectorAll(".ruleview-rule-source");
+ return links[aIndex];
+}
+
+function finishUp()
+{
+ gBrowser.removeCurrentTab();
+ contentWindow = doc = inspector = toolbox = win = null;
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
+ true);
+ doc = content.document;
+ waitForFocus(openToolbox, content);
+ }, true);
+
+ content.location = DOCUMENT_URL;
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_copy.js b/browser/devtools/styleinspector/test/browser_ruleview_copy.js
new file mode 100644
index 000000000..add61957b
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_copy.js
@@ -0,0 +1,145 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let inspector;
+let win;
+
+XPCOMUtils.defineLazyGetter(this, "osString", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+});
+
+function createDocument()
+{
+ doc.body.innerHTML = '<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">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<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>\n' +
+ '<p id="closing">more text</p>\n' +
+ '<p>even more text</p>' +
+ '</div>';
+ doc.title = "Rule view context menu test";
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ inspector = toolbox.getCurrentPanel();
+ inspector.sidebar.select("ruleview");
+ win = inspector.sidebar.getWindowForTab("ruleview");
+ highlightNode();
+ });
+}
+
+function highlightNode()
+{
+ // Highlight a node.
+ let div = content.document.getElementsByTagName("div")[0];
+
+ inspector.selection.once("new-node", function() {
+ is(inspector.selection.node, div, "selection matches the div element");
+ executeSoon(checkCopySelection);
+ });
+ executeSoon(function() {
+ inspector.selection.setNode(div);
+ });
+}
+
+function checkCopySelection()
+{
+ let contentDoc = win.document;
+ let props = contentDoc.querySelectorAll(".ruleview-property");
+ let values = contentDoc.querySelectorAll(".ruleview-propertycontainer");
+
+ let range = document.createRange();
+ range.setStart(props[0], 0);
+ range.setEnd(values[4], 2);
+
+ let selection = win.getSelection();
+ selection.addRange(range);
+
+ info("Checking that _boundCopy() returns the correct " +
+ "clipboard value");
+ let expectedPattern = " margin: 10em;[\\r\\n]+" +
+ " font-size: 14pt;[\\r\\n]+" +
+ " font-family: helvetica,sans-serif;[\\r\\n]+" +
+ " color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
+ "}[\\r\\n]+" +
+ "html {[\\r\\n]+" +
+ " color: rgb\\(0, 0, 0\\);[\\r\\n]*";
+
+ SimpleTest.waitForClipboard(function IUI_boundCopyCheck() {
+ return checkClipboardData(expectedPattern);
+ },function() {fireCopyEvent(props[0])}, finishup, function() {
+ failedClipboard(expectedPattern, finishup);
+ });
+}
+
+function selectNode(aNode) {
+ let doc = aNode.ownerDocument;
+ let win = doc.defaultView;
+ let range = doc.createRange();
+
+ range.selectNode(aNode);
+ win.getSelection().addRange(range);
+}
+
+function checkClipboardData(aExpectedPattern)
+{
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ let expectedRegExp = new RegExp(aExpectedPattern, "g");
+ return expectedRegExp.test(actual);
+}
+
+function failedClipboard(aExpectedPattern, aCallback)
+{
+ // Format expected text for comparison
+ let terminator = osString == "WINNT" ? "\r\n" : "\n";
+ aExpectedPattern = aExpectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
+ aExpectedPattern = aExpectedPattern.replace(/\\\(/g, "(");
+ aExpectedPattern = aExpectedPattern.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.
+ aExpectedPattern = aExpectedPattern.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(aExpectedPattern));
+ aCallback();
+}
+
+function finishup()
+{
+ gBrowser.removeCurrentTab();
+ doc = inspector = null;
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
+ true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,<p>rule view context menu test</p>";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_editor.js b/browser/devtools/styleinspector/test/browser_ruleview_editor.js
new file mode 100644
index 000000000..f4a891dac
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor.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/ */
+
+let doc = content.document;
+
+function expectDone(aValue, aCommit, aNext)
+{
+ return function(aDoneValue, aDoneCommit) {
+ dump("aDoneValue: " + aDoneValue + " commit: " + aDoneCommit + "\n");
+
+ is(aDoneValue, aValue, "Should get expected value");
+ is(aDoneCommit, aDoneCommit, "Should get expected commit value");
+ aNext();
+ }
+}
+
+function clearBody()
+{
+ doc.body.innerHTML = "";
+}
+
+function createSpan()
+{
+ let span = doc.createElement("span");
+ span.setAttribute("tabindex", "0");
+ span.textContent = "Edit Me!";
+ doc.body.appendChild(span);
+ return span;
+}
+
+function testReturnCommit()
+{
+ clearBody();
+ let span = createSpan();
+ editableField({
+ element: span,
+ initial: "explicit initial",
+ start: function() {
+ is(inplaceEditor(span).input.value, "explicit initial", "Explicit initial value should be used.");
+ inplaceEditor(span).input.value = "Test Value";
+ EventUtils.sendKey("return");
+ },
+ done: expectDone("Test Value", true, testBlurCommit)
+ });
+ span.click();
+}
+
+function testBlurCommit()
+{
+ clearBody();
+ let span = createSpan();
+ editableField({
+ element: span,
+ start: function() {
+ is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used.");
+ inplaceEditor(span).input.value = "Test Value";
+ inplaceEditor(span).input.blur();
+ },
+ done: expectDone("Test Value", true, testAdvanceCharCommit)
+ });
+ span.click();
+}
+
+function testAdvanceCharCommit()
+{
+ clearBody();
+ let span = createSpan();
+ editableField({
+ element: span,
+ advanceChars: ":",
+ start: function() {
+ let input = inplaceEditor(span).input;
+ for each (let ch in "Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: expectDone("Test", true, testEscapeCancel)
+ });
+ span.click();
+}
+
+function testEscapeCancel()
+{
+ clearBody();
+ let span = createSpan();
+ editableField({
+ element: span,
+ initial: "initial text",
+ start: function() {
+ inplaceEditor(span).input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: expectDone("initial text", false, finishTest)
+ });
+ span.click();
+}
+
+
+function finishTest()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(testReturnCommit, content);
+ }, true);
+
+ content.location = "data:text/html,inline editor tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
new file mode 100644
index 000000000..28ccdf0a9
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
@@ -0,0 +1,158 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let ruleDialog;
+let ruleView;
+
+var gRuleViewChanged = false;
+function ruleViewChanged()
+{
+ gRuleViewChanged = true;
+}
+
+function expectChange()
+{
+ ok(gRuleViewChanged, "Rule view should have fired a change event.");
+ gRuleViewChanged = false;
+}
+
+function startTest()
+{
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ let testElement = doc.getElementById("testid");
+
+ ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xhtml",
+ "cssruleviewtest",
+ "width=200,height=350");
+ ruleDialog.addEventListener("load", function onLoad(evt) {
+ ruleDialog.removeEventListener("load", onLoad, true);
+ let doc = ruleDialog.document;
+ ruleView = new CssRuleView(doc);
+ doc.documentElement.appendChild(ruleView.element);
+ ruleView.element.addEventListener("CssRuleViewChanged", ruleViewChanged, false);
+ ruleView.highlight(testElement);
+ waitForFocus(testCancelNew, ruleDialog);
+ }, true);
+}
+
+function testCancelNew()
+{
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, but leave it empty.
+ let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+ waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
+ let input = aEditor.input;
+ waitForEditorBlur(aEditor, function () {
+ ok(!gRuleViewChanged, "Shouldn't get a change event after a cancel.");
+ is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+ testCreateNew();
+ });
+ aEditor.input.blur();
+ });
+ EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+ { },
+ ruleDialog);
+}
+
+function testCreateNew()
+{
+ // Create a new property.
+ let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+ waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
+ let input = aEditor.input;
+ input.value = "background-color";
+
+ waitForEditorFocus(elementRuleEditor.element, function onNewValue(aEditor) {
+ expectChange();
+ is(elementRuleEditor.rule.textProps.length, 1, "Should have created a new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1, "Should have created a property editor.");
+ let textProp = elementRuleEditor.rule.textProps[0];
+ is(aEditor, inplaceEditor(textProp.editor.valueSpan), "Should be editing the value span now.");
+ aEditor.input.value = "#XYZ";
+ waitForEditorBlur(aEditor, function() {
+ expectChange();
+ is(textProp.value, "#XYZ", "Text prop should have been changed.");
+ is(textProp.editor._validate(), false, "#XYZ should not be a valid entry");
+ testEditProperty();
+ });
+ aEditor.input.blur();
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, ruleDialog);
+ });
+
+ EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+ { },
+ ruleDialog);
+}
+
+function testEditProperty()
+{
+ let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+ waitForEditorFocus(propEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(propEditor.nameSpan), aEditor, "Next focused editor should be the name editor.");
+ let input = aEditor.input;
+ waitForEditorFocus(propEditor.element, function onNewName(aEditor) {
+ expectChange();
+ input = aEditor.input;
+ is(inplaceEditor(propEditor.valueSpan), aEditor, "Focus should have moved to the value.");
+
+ waitForEditorBlur(aEditor, function() {
+ expectChange();
+ let value = idRuleEditor.rule.style.getPropertyValue("border-color");
+ is(value, "red", "border-color should have been set.");
+ is(propEditor._validate(), true, "red should be a valid entry");
+ finishTest();
+ });
+
+ for each (let ch in "red;") {
+ EventUtils.sendChar(ch, ruleDialog);
+ }
+ });
+ for each (let ch in "border-color:") {
+ EventUtils.sendChar(ch, ruleDialog);
+ }
+ });
+
+ EventUtils.synthesizeMouse(propEditor.nameSpan, 32, 1,
+ { },
+ ruleDialog);
+}
+
+function finishTest()
+{
+ ruleView.element.removeEventListener("CssRuleViewChanged", ruleViewChanged, false);
+ ruleView.clear();
+ ruleDialog.close();
+ ruleDialog = ruleView = null;
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function changedValues_load(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, changedValues_load, true);
+ doc = content.document;
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,test rule view user changes";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_focus.js b/browser/devtools/styleinspector/test/browser_ruleview_focus.js
new file mode 100644
index 000000000..da45169ae
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.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/ */
+
+// Test that focus doesn't leave the style editor when adding a property
+// (bug 719916)
+
+let doc;
+let inspector;
+let stylePanel;
+
+function openRuleView()
+{
+ var target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ inspector = toolbox.getCurrentPanel();
+ inspector.sidebar.select("ruleview");
+
+ // Highlight a node.
+ let node = content.document.getElementsByTagName("h1")[0];
+
+ inspector.sidebar.once("ruleview-ready", testFocus);
+ });
+}
+
+function testFocus()
+{
+ let win = inspector.sidebar.getWindowForTab("ruleview");
+ let brace = win.document.querySelectorAll(".ruleview-ruleclose")[0];
+
+ waitForEditorFocus(brace.parentNode, function onNewElement(aEditor) {
+ aEditor.input.value = "color";
+ waitForEditorFocus(brace.parentNode, function onEditingValue(aEditor) {
+ // If we actually get this focus we're ok.
+ ok(true, "We got focus.");
+ aEditor.input.value = "green";
+
+ // If we've retained focus, pressing return will start a new editor.
+ // If not, we'll wait here until we time out.
+ waitForEditorFocus(brace.parentNode, function onNewEditor(aEditor) {
+ aEditor.input.blur();
+ finishUp();
+ });
+ EventUtils.sendKey("return");
+ });
+ EventUtils.sendKey("return");
+ });
+
+ brace.click();
+}
+
+function finishUp()
+{
+ doc = inspector = stylePanel = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ doc.title = "Rule View Test";
+ waitForFocus(openRuleView, content);
+ }, true);
+
+ content.location = "data:text/html,<h1>Some header text</h1>";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
new file mode 100644
index 000000000..472e9cfa5
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.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/ */
+
+let doc;
+
+function simpleInherit()
+{
+ let style = '' +
+ '#test2 {' +
+ ' background-color: green;' +
+ ' color: purple;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("test1"));
+
+ 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, 1, "Should only display one inherited style");
+ let inheritProp = inheritRule.textProps[0];
+ is(inheritProp.name, "color", "color should have been inherited.");
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ emptyInherit();
+}
+
+function emptyInherit()
+{
+ // No inheritable styles, this rule shouldn't show up.
+ let style = '' +
+ '#test2 {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("test1"));
+
+ 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.");
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ elementStyleInherit();
+}
+
+function elementStyleInherit()
+{
+ doc.body.innerHTML = '<div id="test2" style="color: red"><div id="test1">Styled Node</div></div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("test1"));
+
+ 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];
+ ok(!inheritRule.domRule, "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.");
+
+ finishTest();
+}
+
+function finishTest()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(simpleInherit, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
new file mode 100644
index 000000000..b077c5400
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.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/ */
+
+let doc;
+
+function simpleOverride()
+{
+ doc.body.innerHTML = '<div id="testid">Styled Node</div>';
+ let element = doc.getElementById("testid");
+ let elementStyle = new _ElementStyle(element);
+
+ let elementRule = elementStyle.rules[0];
+ let firstProp = elementRule.createProperty("background-color", "green", "");
+ let secondProp = elementRule.createProperty("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.");
+
+ is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used.");
+
+ secondProp.remove();
+ is(element.style.getPropertyValue("background-color"), "green", "After deleting second property, first should be used.");
+
+ secondProp = elementRule.createProperty("background-color", "blue", "");
+ is(element.style.getPropertyValue("background-color"), "blue", "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.");
+
+ secondProp.setEnabled(false);
+ is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used");
+
+ firstProp.setEnabled(false);
+ is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty.");
+
+ secondProp.setEnabled(true);
+ is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling");
+
+ firstProp.setEnabled(true);
+ is(element.style.getPropertyValue("background-color"), "blue", "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.");
+
+ firstProp.setValue("purple", "");
+ is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property.");
+
+ finishTest();
+}
+
+function finishTest()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(simpleOverride, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_override.js b/browser/devtools/styleinspector/test/browser_ruleview_override.js
new file mode 100644
index 000000000..54e5233aa
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_override.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/ */
+
+let doc;
+
+function simpleOverride()
+{
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+ is(idProp.name, "background-color", "First ID prop should be background-color");
+ ok(!idProp.overridden, "ID prop should not be overridden.");
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ is(classProp.name, "background-color", "First class prop should be background-color");
+ ok(classProp.overridden, "Class property should be overridden.");
+
+ // Override background-color by changing the element style.
+ let elementRule = elementStyle.rules[0];
+ elementRule.createProperty("background-color", "purple", "");
+ let elementProp = elementRule.textProps[0];
+ is(classProp.name, "background-color", "First element prop should now be background-color");
+
+ ok(!elementProp.overridden, "Element style property should not be overridden");
+ ok(idProp.overridden, "ID property should be overridden");
+ ok(classProp.overridden, "Class property should be overridden");
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ partialOverride();
+}
+
+function partialOverride()
+{
+ let style = '' +
+ // Margin shorthand property...
+ '.testclass {' +
+ ' margin: 2px;' +
+ '}' +
+ // ... will be partially overridden.
+ '#testid {' +
+ ' margin-left: 1px;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+ 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 each (let computed in 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.");
+ }
+ }
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ importantOverride();
+}
+
+function importantOverride()
+{
+ let style = '' +
+ // Margin shorthand property...
+ '.testclass {' +
+ ' background-color: green !important;' +
+ '}' +
+ // ... will be partially overridden.
+ '#testid {' +
+ ' background-color: blue;' +
+ '}';
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+ ok(idProp.overridden, "Not-important rule should be overridden.");
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Important rule should not be overridden.");
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ let elementRule = elementStyle.rules[0];
+ let elementProp = elementRule.createProperty("background-color", "purple", "important");
+ ok(classProp.overridden, "New important prop should override class property.");
+ ok(!elementProp.overridden, "New important prop should not be overriden.");
+
+ disableOverride();
+}
+
+function disableOverride()
+{
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '}' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}';
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+ let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+ let idRule = elementStyle.rules[1];
+ let idProp = idRule.textProps[0];
+ idProp.setEnabled(false);
+
+ let classRule = elementStyle.rules[2];
+ let classProp = classRule.textProps[0];
+ ok(!classProp.overridden, "Class prop should not be overridden after id prop was disabled.");
+
+ styleNode.parentNode.removeChild(styleNode);
+
+ finishTest();
+}
+
+function finishTest()
+{
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(simpleOverride, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_ui.js b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
new file mode 100644
index 000000000..b7b69f85b
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
@@ -0,0 +1,218 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let ruleDialog;
+let ruleView;
+
+var gRuleViewChanged = false;
+function ruleViewChanged()
+{
+ gRuleViewChanged = true;
+}
+
+function expectChange()
+{
+ ok(gRuleViewChanged, "Rule view should have fired a change event.");
+ gRuleViewChanged = false;
+}
+
+function startTest()
+{
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass, .unmatched {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ let testElement = doc.getElementById("testid");
+
+ ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xhtml",
+ "cssruleviewtest",
+ "width=200,height=350");
+ ruleDialog.addEventListener("load", function onLoad(evt) {
+ ruleDialog.removeEventListener("load", onLoad);
+ let doc = ruleDialog.document;
+ ruleView = new CssRuleView(doc);
+ doc.documentElement.appendChild(ruleView.element);
+ ruleView.element.addEventListener("CssRuleViewChanged", ruleViewChanged, false);
+ is(ruleView.element.querySelectorAll("#noResults").length, 1, "Has a no-results element.");
+ ruleView.highlight(testElement);
+ is(ruleView.element.querySelectorAll("#noResults").length, 0, "After a highlight, no longer has a no-results element.");
+ ruleView.highlight(null);
+ is(ruleView.element.querySelectorAll("#noResults").length, 1, "After highlighting null, has a no-results element again.");
+ ruleView.highlight(testElement);
+
+ let classEditor = ruleView.element.children[2]._ruleEditor;
+ is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent, ".testclass", ".textclass should be matched.");
+ is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent, ".unmatched", ".unmatched should not be matched.");
+
+ waitForFocus(testCancelNew, ruleDialog);
+ }, true);
+}
+
+function testCancelNew()
+{
+ // Start at the beginning: start to add a rule to the element's style
+ // declaration, but leave it empty.
+
+ let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+ waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
+ let input = aEditor.input;
+ waitForEditorBlur(aEditor, function () {
+ ok(!gRuleViewChanged, "Shouldn't get a change event after a cancel.");
+ is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property.");
+ ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+ testCreateNew();
+ });
+ aEditor.input.blur();
+ });
+
+ EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+ { },
+ ruleDialog);
+}
+
+function testCreateNew()
+{
+ // Create a new property.
+ let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+ waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
+
+ let input = aEditor.input;
+
+ 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, { }, ruleDialog);
+ input.select();
+
+ input.value = "background-color";
+
+ waitForEditorFocus(elementRuleEditor.element, function onNewValue(aEditor) {
+ expectChange();
+ is(elementRuleEditor.rule.textProps.length, 1, "Should have created a new text property.");
+ is(elementRuleEditor.propertyList.children.length, 1, "Should have created a property editor.");
+ let textProp = elementRuleEditor.rule.textProps[0];
+ is(aEditor, inplaceEditor(textProp.editor.valueSpan), "Should be editing the value span now.");
+
+ aEditor.input.value = "purple";
+ waitForEditorBlur(aEditor, function() {
+ expectChange();
+ is(textProp.value, "purple", "Text prop should have been changed.");
+ testEditProperty();
+ });
+
+ aEditor.input.blur();
+ });
+ EventUtils.sendKey("return", ruleDialog);
+ });
+
+ EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+ { },
+ ruleDialog);
+}
+
+function testEditProperty()
+{
+ let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+ waitForEditorFocus(propEditor.element, function onNewElement(aEditor) {
+ is(inplaceEditor(propEditor.nameSpan), aEditor, "Next focused editor should be the name editor.");
+
+ let input = aEditor.input;
+
+ dump("SELECTION END IS: " + input.selectionEnd + "\n");
+ 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, { }, ruleDialog);
+ input.select();
+
+ waitForEditorFocus(propEditor.element, function onNewName(aEditor) {
+ expectChange();
+ is(inplaceEditor(propEditor.valueSpan), aEditor, "Focus should have moved to the value.");
+
+ input = aEditor.input;
+
+ 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, { }, ruleDialog);
+ input.select();
+
+ waitForEditorBlur(aEditor, function() {
+ expectChange();
+ is(idRuleEditor.rule.style.getPropertyValue("border-color"), "red",
+ "border-color should have been set.");
+
+ 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");
+ }
+ testDisableProperty();
+ });
+
+ for each (let ch in "red;") {
+ EventUtils.sendChar(ch, ruleDialog);
+ is(propEditor.warning.hidden, ch == "d" || ch == ";",
+ "warning triangle is hidden or shown as appropriate");
+ }
+ });
+ for each (let ch in "border-color:") {
+ EventUtils.sendChar(ch, ruleDialog);
+ }
+ });
+
+ EventUtils.synthesizeMouse(propEditor.nameSpan, 32, 1,
+ { },
+ ruleDialog);
+}
+
+function testDisableProperty()
+{
+ let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+ let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+ propEditor.enable.click();
+ is(idRuleEditor.rule.style.getPropertyValue("border-color"), "", "Border-color should have been unset.");
+ expectChange();
+
+ propEditor.enable.click();
+ is(idRuleEditor.rule.style.getPropertyValue("border-color"), "red",
+ "Border-color should have been reset.");
+ expectChange();
+
+ finishTest();
+}
+
+function finishTest()
+{
+ ruleView.clear();
+ ruleDialog.close();
+ ruleDialog = ruleView = null;
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_update.js b/browser/devtools/styleinspector/test/browser_ruleview_update.js
new file mode 100644
index 000000000..de6f613ed
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_update.js
@@ -0,0 +1,153 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+let ruleDialog;
+let ruleView;
+let testElement;
+
+function startTest()
+{
+ let style = '' +
+ '#testid {' +
+ ' background-color: blue;' +
+ '} ' +
+ '.testclass {' +
+ ' background-color: green;' +
+ '}';
+
+ let styleNode = addStyle(doc, style);
+ doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+ testElement = doc.getElementById("testid");
+
+ let elementStyle = 'margin-top: 1px; padding-top: 5px;'
+ testElement.setAttribute("style", elementStyle);
+
+ ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xhtml",
+ "cssruleviewtest",
+ "width=200,height=350");
+ ruleDialog.addEventListener("load", function onLoad(evt) {
+ ruleDialog.removeEventListener("load", onLoad);
+ let doc = ruleDialog.document;
+ ruleView = new CssRuleView(doc);
+ doc.documentElement.appendChild(ruleView.element);
+ ruleView.highlight(testElement);
+ waitForFocus(testRuleChanges, ruleDialog);
+ }, true);
+}
+
+function testRuleChanges()
+{
+ let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
+ is(selectors.length, 3, "Three rules visible.");
+ is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
+ is(selectors[1].textContent.indexOf("#testid"), 0, "Second item is id rule.");
+ is(selectors[2].textContent.indexOf(".testclass"), 0, "Third item is class rule.");
+
+ // Change the id and refresh.
+ testElement.setAttribute("id", "differentid");
+ ruleView.nodeChanged();
+
+ let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
+ is(selectors.length, 2, "Three rules visible.");
+ is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
+ is(selectors[1].textContent.indexOf(".testclass"), 0, "Second item is class rule.");
+
+ testElement.setAttribute("id", "testid");
+ ruleView.nodeChanged();
+
+ // Put the id back.
+ let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
+ is(selectors.length, 3, "Three rules visible.");
+ is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
+ is(selectors[1].textContent.indexOf("#testid"), 0, "Second item is id rule.");
+ is(selectors[2].textContent.indexOf(".testclass"), 0, "Third item is class rule.");
+
+ testPropertyChanges();
+}
+
+function validateTextProp(aProp, aEnabled, aName, aValue, aDesc)
+{
+ is(aProp.enabled, aEnabled, aDesc + ": enabled.");
+ is(aProp.name, aName, aDesc + ": name.");
+ is(aProp.value, aValue, aDesc + ": value.");
+
+ is(aProp.editor.enable.hasAttribute("checked"), aEnabled, aDesc + ": enabled checkbox.");
+ is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span.");
+ is(aProp.editor.valueSpan.textContent, aValue, aDesc + ": value span.");
+}
+
+function testPropertyChanges()
+{
+ // Add a second margin-top value, just to make things interesting.
+ let ruleEditor = ruleView._elementStyle.rules[0].editor;
+ ruleEditor.addProperty("margin-top", "5px", "");
+
+ let rule = ruleView._elementStyle.rules[0];
+
+ // Set the element style back to a 1px margin-top.
+ testElement.setAttribute("style", "margin-top: 1px; padding-top: 5px");
+ ruleView.nodeChanged();
+ 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");
+
+ // Now set it back to 5px, the 5px value should be re-enabled.
+ testElement.setAttribute("style", "margin-top: 5px; padding-top: 5px;");
+ ruleView.nodeChanged();
+ 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");
+
+ // Set the margin property to a value that doesn't exist in the editor.
+ // Should reuse the currently-enabled element (the second one.)
+ testElement.setAttribute("style", "margin-top: 15px; padding-top: 5px;");
+ ruleView.nodeChanged();
+ 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");
+
+ // Remove the padding-top attribute. Should disable the padding property but not remove it.
+ testElement.setAttribute("style", "margin-top: 5px;");
+ ruleView.nodeChanged();
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[1], false, "padding-top", "5px", "Padding property disabled");
+
+ // Put the padding-top attribute back in, should re-enable the padding property.
+ testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px");
+ ruleView.nodeChanged();
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
+ validateTextProp(rule.textProps[1], true, "padding-top", "25px", "Padding property enabled");
+
+ // Add an entirely new property.
+ testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px; padding-left: 20px;");
+ ruleView.nodeChanged();
+ is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, "Added a property");
+ validateTextProp(rule.textProps[3], true, "padding-left", "20px", "Padding property enabled");
+
+ finishTest();
+}
+
+function finishTest()
+{
+ ruleView.clear();
+ ruleDialog.close();
+ ruleDialog = ruleView = null;
+ doc = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(startTest, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector.js b/browser/devtools/styleinspector/test/browser_styleinspector.js
new file mode 100644
index 000000000..78c67ef6a
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector.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/ */
+
+// Tests that the style inspector works properly
+
+let doc;
+let inspector;
+let computedView;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<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">\n' +
+ '<h1>Some header text</h1>\n' +
+ '<p id="salutation" style="font-size: 12pt">hi.</p>\n' +
+ '<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">Maybe more inspector test-cases!</span></p>\n' +
+ '<p id="closing">end transmission</p>\n' +
+ '<p>Inspect using inspectstyle(document.querySelectorAll("span")[0])</p>' +
+ '</div>';
+ doc.title = "Style Inspector Test";
+
+ openInspector(openComputedView);
+}
+
+function openComputedView(aInspector)
+{
+ inspector = aInspector;
+
+ inspector.sidebar.once("computedview-ready", function() {
+ computedView = getComputedView(inspector);
+
+ inspector.sidebar.select("computedview");
+ runStyleInspectorTests();
+ });
+}
+
+function runStyleInspectorTests()
+{
+ var spans = doc.querySelectorAll("span");
+ ok(spans, "captain, we have the spans");
+
+ for (var i = 0, numSpans = spans.length; i < numSpans; i++) {
+ inspector.selection.setNode(spans[i]);
+
+ is(spans[i], computedView.viewedElement,
+ "style inspector node matches the selected node");
+ is(computedView.viewedElement, computedView.cssLogic.viewedElement,
+ "cssLogic node matches the cssHtmlTree node");
+ }
+
+ SI_CheckProperty();
+ finishUp();
+}
+
+function SI_CheckProperty()
+{
+ let cssLogic = computedView.cssLogic;
+ let propertyInfo = cssLogic.getPropertyInfo("color");
+ ok(propertyInfo.matchedRuleCount > 0, "color property has matching rules");
+}
+
+function finishUp()
+{
+ doc = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,basic style inspector tests";
+}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
new file mode 100644
index 000000000..16e3aecbf
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
@@ -0,0 +1,116 @@
+/* vim: set 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 search filter works properly.
+
+let doc;
+let inspector;
+let computedView;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>' +
+ '</div>';
+ doc.title = "Style Inspector Search Filter Test";
+
+ openInspector(openComputedView);
+}
+
+function openComputedView(aInspector)
+{
+ inspector = aInspector;
+
+ inspector.sidebar.once("computedview-ready", function() {
+ inspector.sidebar.select("computedview");
+ computedView = getComputedView(inspector);
+
+ runStyleInspectorTests();
+ });
+}
+
+function runStyleInspectorTests()
+{
+ Services.obs.addObserver(SI_toggleDefaultStyles, "StyleInspector-populated", false);
+ SI_inspectNode();
+}
+
+function SI_inspectNode()
+{
+ var span = doc.querySelector("#matches");
+ ok(span, "captain, we have the matches span");
+
+ inspector.selection.setNode(span);
+
+ is(span, computedView.viewedElement,
+ "style inspector node matches the selected node");
+ is(computedView.viewedElement, computedView.cssLogic.viewedElement,
+ "cssLogic node matches the cssHtmlTree node");
+}
+
+function SI_toggleDefaultStyles()
+{
+ Services.obs.removeObserver(SI_toggleDefaultStyles, "StyleInspector-populated");
+
+ info("checking \"Browser styles\" checkbox");
+
+ let doc = computedView.styleDocument;
+ let checkbox = doc.querySelector(".includebrowserstyles");
+ Services.obs.addObserver(SI_AddFilterText, "StyleInspector-populated", false);
+ checkbox.click();
+}
+
+function SI_AddFilterText()
+{
+ Services.obs.removeObserver(SI_AddFilterText, "StyleInspector-populated");
+
+ let doc = computedView.styleDocument;
+ let searchbar = doc.querySelector(".devtools-searchinput");
+ Services.obs.addObserver(SI_checkFilter, "StyleInspector-populated", false);
+ info("setting filter text to \"color\"");
+ searchbar.focus();
+
+ let win =computedView.styleWindow;
+ EventUtils.synthesizeKey("c", {}, win);
+ EventUtils.synthesizeKey("o", {}, win);
+ EventUtils.synthesizeKey("l", {}, win);
+ EventUtils.synthesizeKey("o", {}, win);
+ EventUtils.synthesizeKey("r", {}, win);
+}
+
+function SI_checkFilter()
+{
+ Services.obs.removeObserver(SI_checkFilter, "StyleInspector-populated");
+ let propertyViews = computedView.propertyViews;
+
+ info("check that the correct properties are visible");
+ propertyViews.forEach(function(propView) {
+ let name = propView.name;
+ is(propView.visible, name.indexOf("color") > -1,
+ "span " + name + " property visibility check");
+ });
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = inspector = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,default styles test";
+}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js
new file mode 100644
index 000000000..860fc7d4d
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js
@@ -0,0 +1,116 @@
+/* vim: set 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 checkbox to include browser styles works properly.
+
+let doc;
+let inspector;
+let computedView;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>' +
+ '</div>';
+ doc.title = "Style Inspector Default Styles Test";
+
+ openInspector(openComputedView);
+}
+
+function openComputedView(aInspector)
+{
+ inspector = aInspector;
+
+ inspector.sidebar.once("computedview-ready", function() {
+ inspector.sidebar.select("computedview");
+ computedView = getComputedView(inspector);
+
+ runStyleInspectorTests();
+ });
+}
+
+function runStyleInspectorTests()
+{
+ Services.obs.addObserver(SI_check, "StyleInspector-populated", false);
+ SI_inspectNode();
+}
+
+function SI_inspectNode()
+{
+ let span = doc.querySelector("#matches");
+ ok(span, "captain, we have the matches span");
+
+ inspector.selection.setNode(span);
+
+ is(span, computedView.viewedElement,
+ "style inspector node matches the selected node");
+ is(computedView.viewedElement, computedView.cssLogic.viewedElement,
+ "cssLogic node matches the cssHtmlTree node");
+}
+
+function SI_check()
+{
+ Services.obs.removeObserver(SI_check, "StyleInspector-populated");
+ is(propertyVisible("color"), true,
+ "span #matches color property is visible");
+ is(propertyVisible("background-color"), false,
+ "span #matches background-color property is hidden");
+
+ SI_toggleDefaultStyles();
+}
+
+function SI_toggleDefaultStyles()
+{
+ // Click on the checkbox.
+ let doc = computedView.styleDocument;
+ let checkbox = doc.querySelector(".includebrowserstyles");
+ Services.obs.addObserver(SI_checkDefaultStyles, "StyleInspector-populated", false);
+
+ checkbox.click();
+}
+
+function SI_checkDefaultStyles()
+{
+ Services.obs.removeObserver(SI_checkDefaultStyles, "StyleInspector-populated");
+ // Check that the default styles are now applied.
+ is(propertyVisible("color"), true,
+ "span color property is visible");
+ is(propertyVisible("background-color"), true,
+ "span background-color property is visible");
+
+ finishUp();
+}
+
+function propertyVisible(aName)
+{
+ info("Checking property visibility for " + aName);
+ let propertyViews = computedView.propertyViews;
+ for each (let propView in propertyViews) {
+ if (propView.name == aName) {
+ return propView.visible;
+ }
+ }
+ return false;
+}
+
+function finishUp()
+{
+ doc = inspector = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,default styles test";
+}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html
new file mode 100644
index 000000000..44b5f0e3d
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+ <head>
+
+ <link href="./browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css" rel="stylesheet" type="text/css">
+
+ </head>
+ <body>
+
+ <div class="relative">Background image with relative path (loaded from external css)</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(test-image.png);">Background image with relative path (loaded from style attribute)</div>';
+
+ <div class="noimage">No background image :(</div>
+ </body>
+</html>
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js
new file mode 100644
index 000000000..215313081
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js
@@ -0,0 +1,97 @@
+/* vim: set 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 make sure that URLs are clickable in the rule view
+
+let doc;
+let computedView;
+let inspector;
+
+const BASE_URL = "http://example.com/browser/browser/" +
+ "devtools/styleinspector/test/";
+const TEST_URI = BASE_URL +
+ "browser_styleinspector_bug_677930_urls_clickable.html";
+const TEST_IMAGE = BASE_URL + "test-image.png";
+const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+function createDocument()
+{
+ doc.title = "Style Inspector URL Clickable test";
+
+ openInspector(function(aInspector) {
+ inspector = aInspector;
+ executeSoon(selectNode);
+ });
+}
+
+
+function selectNode(aInspector)
+{
+ let sidebar = inspector.sidebar;
+ let iframe = sidebar._tabbox.querySelector(".iframe-ruleview");
+ let contentDoc = iframe.contentWindow.document;
+
+ let relative = doc.querySelector(".relative");
+ let absolute = doc.querySelector(".absolute");
+ let inline = doc.querySelector(".inline");
+ let base64 = doc.querySelector(".base64");
+ let noimage = doc.querySelector(".noimage");
+
+ ok(relative, "captain, we have the relative div");
+ ok(absolute, "captain, we have the absolute div");
+ ok(inline, "captain, we have the inline div");
+ ok(base64, "captain, we have the base64 div");
+ ok(noimage, "captain, we have the noimage div");
+
+ inspector.selection.setNode(relative);
+ is(inspector.selection.node, relative, "selection matches the relative element");
+ let relativeLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+ ok (relativeLink, "Link exists for relative node");
+ ok (relativeLink.getAttribute("href"), TEST_IMAGE);
+
+ inspector.selection.setNode(absolute);
+ is(inspector.selection.node, absolute, "selection matches the absolute element");
+ let absoluteLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+ ok (absoluteLink, "Link exists for absolute node");
+ ok (absoluteLink.getAttribute("href"), TEST_IMAGE);
+
+ inspector.selection.setNode(inline);
+ is(inspector.selection.node, inline, "selection matches the inline element");
+ let inlineLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+ ok (inlineLink, "Link exists for inline node");
+ ok (inlineLink.getAttribute("href"), TEST_IMAGE);
+
+ inspector.selection.setNode(base64);
+ is(inspector.selection.node, base64, "selection matches the base64 element");
+ let base64Link = contentDoc.querySelector(".ruleview-propertycontainer a");
+ ok (base64Link, "Link exists for base64 node");
+ ok (base64Link.getAttribute("href"), BASE_64_URL);
+
+ inspector.selection.setNode(noimage);
+ is(inspector.selection.node, noimage, "selection matches the inline element");
+ let noimageLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+ ok (!noimageLink, "There is no link for the node with no background image");
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = computedView = inspector = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css
new file mode 100644
index 000000000..f55c54a89
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css
@@ -0,0 +1,9 @@
+.relative {
+ background-image: url(../test-image.png);
+}
+.absolute {
+ background: url("http://example.com/browser/browser/devtools/styleinspector/test/test-image.png");
+}
+.base64 {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
+} \ No newline at end of file
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js
new file mode 100644
index 000000000..0bfea2947
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.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/ */
+
+// Tests that the no results placeholder works properly.
+
+let doc;
+let inspector;
+let computedView;
+
+function createDocument()
+{
+ doc.body.innerHTML = '<style type="text/css"> ' +
+ '.matches {color: #F00;}</style>' +
+ '<span id="matches" class="matches">Some styled text</span>';
+ doc.title = "Tests that the no results placeholder works properly";
+
+ openInspector(openComputedView);
+}
+
+function openComputedView(aInspector)
+{
+ inspector = aInspector;
+
+ inspector.sidebar.once("computedview-ready", function() {
+ inspector.sidebar.select("computedview");
+ computedView = getComputedView(inspector);
+
+ runStyleInspectorTests();
+ });
+}
+
+
+function runStyleInspectorTests()
+{
+ Services.obs.addObserver(SI_AddFilterText, "StyleInspector-populated", false);
+
+ let span = doc.querySelector("#matches");
+ ok(span, "captain, we have the matches span");
+
+ inspector.selection.setNode(span);
+
+ is(span, computedView.viewedElement,
+ "style inspector node matches the selected node");
+ is(computedView.viewedElement, computedView.cssLogic.viewedElement,
+ "cssLogic node matches the cssHtmlTree node");
+}
+
+function SI_AddFilterText()
+{
+ Services.obs.removeObserver(SI_AddFilterText, "StyleInspector-populated");
+
+ let searchbar = computedView.searchField;
+ let searchTerm = "xxxxx";
+
+ Services.obs.addObserver(SI_checkPlaceholderVisible, "StyleInspector-populated", false);
+ info("setting filter text to \"" + searchTerm + "\"");
+ searchbar.focus();
+ for each (let c in searchTerm) {
+ EventUtils.synthesizeKey(c, {}, computedView.styleWindow);
+ }
+}
+
+function SI_checkPlaceholderVisible()
+{
+ Services.obs.removeObserver(SI_checkPlaceholderVisible, "StyleInspector-populated");
+ info("SI_checkPlaceholderVisible called");
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+
+ is(display, "block", "placeholder is visible");
+
+ SI_ClearFilterText();
+}
+
+function SI_ClearFilterText()
+{
+ let searchbar = computedView.searchField;
+
+ Services.obs.addObserver(SI_checkPlaceholderHidden, "StyleInspector-populated", false);
+ info("clearing filter text");
+ searchbar.focus();
+ searchbar.value = "";
+ EventUtils.synthesizeKey("c", {}, computedView.styleWindow);
+}
+
+function SI_checkPlaceholderHidden()
+{
+ Services.obs.removeObserver(SI_checkPlaceholderHidden, "StyleInspector-populated");
+ let placeholder = computedView.noResults;
+ let win = computedView.styleWindow;
+ let display = win.getComputedStyle(placeholder).display;
+
+ is(display, "none", "placeholder is hidden");
+
+ finishUp();
+}
+
+function finishUp()
+{
+ doc = inspector = computedView = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ doc = content.document;
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html,no results placeholder test";
+}
diff --git a/browser/devtools/styleinspector/test/head.js b/browser/devtools/styleinspector/test/head.js
new file mode 100644
index 000000000..76358fd39
--- /dev/null
+++ b/browser/devtools/styleinspector/test/head.js
@@ -0,0 +1,126 @@
+/* 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/. */
+
+let tempScope = {};
+Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
+let ConsoleUtils = tempScope.ConsoleUtils;
+let gDevTools = tempScope.gDevTools;
+
+Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+let devtools = tempScope.devtools;
+
+let TargetFactory = devtools.TargetFactory;
+let {CssHtmlTree} = devtools.require("devtools/styleinspector/computed-view");
+let {CssRuleView, _ElementStyle} = devtools.require("devtools/styleinspector/rule-view");
+let {CssLogic, CssSelector} = devtools.require("devtools/styleinspector/css-logic");
+
+let {
+ editableField,
+ getInplaceEditorForSpan: inplaceEditor
+} = devtools.require("devtools/shared/inplace-editor");
+Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+let console = tempScope.console;
+
+let browser, hudId, hud, hudBox, filterBox, outputNode, cs;
+
+function addTab(aURL)
+{
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = aURL;
+ browser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+}
+
+function openInspector(callback)
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ callback(toolbox.getCurrentPanel());
+ });
+}
+
+function addStyle(aDocument, aString)
+{
+ let node = aDocument.createElement('style');
+ node.setAttribute("type", "text/css");
+ node.textContent = aString;
+ aDocument.getElementsByTagName("head")[0].appendChild(node);
+ return node;
+}
+
+function finishTest()
+{
+ finish();
+}
+
+function tearDown()
+{
+ try {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ }
+ catch (ex) {
+ dump(ex);
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ browser = hudId = hud = filterBox = outputNode = cs = null;
+}
+
+function getComputedView(inspector) {
+ return inspector.sidebar.getWindowForTab("computedview").computedview.view;
+}
+
+function ruleView()
+{
+ return inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
+}
+
+function waitForEditorFocus(aParent, aCallback)
+{
+ aParent.addEventListener("focus", function onFocus(evt) {
+ if (inplaceEditor(evt.target) && evt.target.tagName == "input") {
+ aParent.removeEventListener("focus", onFocus, true);
+ let editor = inplaceEditor(evt.target);
+ executeSoon(function() {
+ aCallback(editor);
+ });
+ }
+ }, true);
+}
+
+function waitForEditorBlur(aEditor, aCallback)
+{
+ let input = aEditor.input;
+ input.addEventListener("blur", function onBlur() {
+ input.removeEventListener("blur", onBlur, false);
+ executeSoon(function() {
+ aCallback();
+ });
+ }, false);
+}
+
+function fireCopyEvent(element) {
+ let evt = element.ownerDocument.createEvent("Event");
+ evt.initEvent("copy", true, true);
+ element.dispatchEvent(evt);
+}
+
+function contextMenuClick(element) {
+ var evt = element.ownerDocument.createEvent('MouseEvents');
+
+ var button = 2; // right click
+
+ evt.initMouseEvent('contextmenu', true, true,
+ element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false,
+ false, false, false, button, null);
+
+ element.dispatchEvent(evt);
+}
+
+registerCleanupFunction(tearDown);
+
+waitForExplicitFinish();
+
diff --git a/browser/devtools/styleinspector/test/moz.build b/browser/devtools/styleinspector/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/styleinspector/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/styleinspector/test/test-image.png b/browser/devtools/styleinspector/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/devtools/styleinspector/test/test-image.png
Binary files differ
diff --git a/browser/devtools/tilt/CmdTilt.jsm b/browser/devtools/tilt/CmdTilt.jsm
new file mode 100644
index 000000000..cc2d9b240
--- /dev/null
+++ b/browser/devtools/tilt/CmdTilt.jsm
@@ -0,0 +1,216 @@
+/* -*- 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";
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+
+// Fetch TiltManager using the current loader, but don't save a
+// reference to it, because it might change with a tool reload.
+// We can clean this up once the command line is loadered.
+Object.defineProperty(this, "TiltManager", {
+ get: function() {
+ return devtools.require("devtools/tilt/tilt").TiltManager;
+ },
+ enumerable: true
+});
+
+/**
+ * 'tilt' command
+ */
+gcli.addCommand({
+ name: 'tilt',
+ description: gcli.lookup("tiltDesc"),
+ manual: gcli.lookup("tiltManual")
+});
+
+
+/**
+ * 'tilt open' command
+ */
+gcli.addCommand({
+ name: 'tilt open',
+ description: gcli.lookup("tiltOpenDesc"),
+ manual: gcli.lookup("tiltOpenManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (!Tilt.currentInstance) {
+ Tilt.toggle();
+ }
+ }
+});
+
+
+/**
+ * 'tilt toggle' command
+ */
+gcli.addCommand({
+ name: "tilt toggle",
+ buttonId: "command-button-tilt",
+ buttonClass: "command-button",
+ tooltipText: gcli.lookup("tiltToggleTooltip"),
+ hidden: true,
+ state: {
+ isChecked: function(aTarget) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ return !!TiltManager.getTiltForBrowser(browserWindow).currentInstance;
+ },
+ onChange: function(aTarget, aChangeHandler) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let tilt = TiltManager.getTiltForBrowser(browserWindow);
+ tilt.on("change", aChangeHandler);
+ },
+ offChange: function(aTarget, aChangeHandler) {
+ if (aTarget.tab) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let tilt = TiltManager.getTiltForBrowser(browserWindow);
+ tilt.off("change", aChangeHandler);
+ }
+ },
+ },
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ Tilt.toggle();
+ }
+});
+
+
+/**
+ * 'tilt translate' command
+ */
+gcli.addCommand({
+ name: 'tilt translate',
+ description: gcli.lookup("tiltTranslateDesc"),
+ manual: gcli.lookup("tiltTranslateManual"),
+ params: [
+ {
+ name: "x",
+ type: "number",
+ defaultValue: 0,
+ description: gcli.lookup("tiltTranslateXDesc"),
+ manual: gcli.lookup("tiltTranslateXManual")
+ },
+ {
+ name: "y",
+ type: "number",
+ defaultValue: 0,
+ description: gcli.lookup("tiltTranslateYDesc"),
+ manual: gcli.lookup("tiltTranslateYManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.translate([args.x, args.y]);
+ }
+ }
+});
+
+
+/**
+ * 'tilt rotate' command
+ */
+gcli.addCommand({
+ name: 'tilt rotate',
+ description: gcli.lookup("tiltRotateDesc"),
+ manual: gcli.lookup("tiltRotateManual"),
+ params: [
+ {
+ name: "x",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateXDesc"),
+ manual: gcli.lookup("tiltRotateXManual")
+ },
+ {
+ name: "y",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateYDesc"),
+ manual: gcli.lookup("tiltRotateYManual")
+ },
+ {
+ name: "z",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateZDesc"),
+ manual: gcli.lookup("tiltRotateZManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.rotate([args.x, args.y, args.z]);
+ }
+ }
+});
+
+
+/**
+ * 'tilt zoom' command
+ */
+gcli.addCommand({
+ name: 'tilt zoom',
+ description: gcli.lookup("tiltZoomDesc"),
+ manual: gcli.lookup("tiltZoomManual"),
+ params: [
+ {
+ name: "zoom",
+ type: { name: 'number' },
+ description: gcli.lookup("tiltZoomAmountDesc"),
+ manual: gcli.lookup("tiltZoomAmountManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.zoom(-args.zoom);
+ }
+ }
+});
+
+
+/**
+ * 'tilt reset' command
+ */
+gcli.addCommand({
+ name: 'tilt reset',
+ description: gcli.lookup("tiltResetDesc"),
+ manual: gcli.lookup("tiltResetManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.reset();
+ }
+ }
+});
+
+
+/**
+ * 'tilt close' command
+ */
+gcli.addCommand({
+ name: 'tilt close',
+ description: gcli.lookup("tiltCloseDesc"),
+ manual: gcli.lookup("tiltCloseManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+});
diff --git a/browser/devtools/tilt/Makefile.in b/browser/devtools/tilt/Makefile.in
new file mode 100644
index 000000000..e4584f380
--- /dev/null
+++ b/browser/devtools/tilt/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/tilt
diff --git a/browser/devtools/tilt/TiltWorkerCrafter.js b/browser/devtools/tilt/TiltWorkerCrafter.js
new file mode 100644
index 000000000..9884d059a
--- /dev/null
+++ b/browser/devtools/tilt/TiltWorkerCrafter.js
@@ -0,0 +1,280 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
+
+/**
+ * Given the initialization data (sizes and information about
+ * each DOM node) this worker sends back the arrays representing
+ * vertices, texture coords, colors, indices and all the needed data for
+ * rendering the DOM visualization mesh.
+ *
+ * Used in the TiltVisualization.Presenter object.
+ */
+self.onmessage = function TWC_onMessage(event)
+{
+ let data = event.data;
+ let maxGroupNodes = parseInt(data.maxGroupNodes);
+ let style = data.style;
+ let texWidth = data.texWidth;
+ let texHeight = data.texHeight;
+ let nodesInfo = data.nodesInfo;
+
+ let mesh = {
+ allVertices: [],
+ groups: [],
+ width: 0,
+ height: 0
+ };
+
+ let vertices;
+ let texCoord;
+ let color;
+ let stacksIndices;
+ let wireframeIndices;
+ let index;
+
+ // seed the random function to get the same values each time
+ // we're doing this to avoid ugly z-fighting with overlapping nodes
+ self.random.seed(0);
+
+ // go through all the dom nodes and compute the verts, texcoord etc.
+ for (let n = 0, len = nodesInfo.length; n < len; n++) {
+
+ // check if we need to start creating a new group
+ if (n % maxGroupNodes === 0) {
+ vertices = []; // recreate the arrays used to construct the 3D mesh data
+ texCoord = [];
+ color = [];
+ stacksIndices = [];
+ wireframeIndices = [];
+ index = 0;
+ }
+
+ let info = nodesInfo[n];
+ let coord = info.coord;
+
+ // calculate the stack x, y, z, width and height coordinates
+ let z = coord.depth + coord.thickness;
+ let y = coord.top;
+ let x = coord.left;
+ let w = coord.width;
+ let h = coord.height;
+
+ // the maximum texture size slices the visualization mesh where needed
+ if (x + w > texWidth) {
+ w = texWidth - x;
+ }
+ if (y + h > texHeight) {
+ h = texHeight - y;
+ }
+
+ x += self.random.next();
+ y += self.random.next();
+ w -= self.random.next() * 0.1;
+ h -= self.random.next() * 0.1;
+
+ let xpw = x + w;
+ let yph = y + h;
+ let zmt = coord.depth;
+
+ let xotw = x / texWidth;
+ let yoth = y / texHeight;
+ let xpwotw = xpw / texWidth;
+ let yphoth = yph / texHeight;
+
+ // calculate the margin fill color
+ let fill = style[info.name] || style.highlight.defaultFill;
+
+ let r = fill[0];
+ let g = fill[1];
+ let b = fill[2];
+ let g10 = r * 1.1;
+ let g11 = g * 1.1;
+ let g12 = b * 1.1;
+ let g20 = r * 0.6;
+ let g21 = g * 0.6;
+ let g22 = b * 0.6;
+
+ // compute the vertices
+ vertices.push(x, y, z, /* front */ // 0
+ x, yph, z, // 1
+ xpw, yph, z, // 2
+ xpw, y, z, // 3
+ // we don't duplicate vertices for the left and right faces, because
+ // they can be reused from the bottom and top faces; we do, however,
+ // duplicate some vertices from front face, because it has custom
+ // texture coordinates which are not shared by the other faces
+ x, y, z, /* front */ // 4
+ x, yph, z, // 5
+ xpw, yph, z, // 6
+ xpw, y, z, // 7
+ x, y, zmt, /* back */ // 8
+ x, yph, zmt, // 9
+ xpw, yph, zmt, // 10
+ xpw, y, zmt); // 11
+
+ // compute the texture coordinates
+ texCoord.push(xotw, yoth,
+ xotw, yphoth,
+ xpwotw, yphoth,
+ xpwotw, yoth,
+ -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0);
+
+ // compute the colors for each vertex in the mesh
+ color.push(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ g10, g11, g12,
+ g10, g11, g12,
+ g10, g11, g12,
+ g10, g11, g12,
+ g20, g21, g22,
+ g20, g21, g22,
+ g20, g21, g22,
+ g20, g21, g22);
+
+ let i = index; // number of vertex points, used to create the indices array
+ let ip1 = i + 1;
+ let ip2 = ip1 + 1;
+ let ip3 = ip2 + 1;
+ let ip4 = ip3 + 1;
+ let ip5 = ip4 + 1;
+ let ip6 = ip5 + 1;
+ let ip7 = ip6 + 1;
+ let ip8 = ip7 + 1;
+ let ip9 = ip8 + 1;
+ let ip10 = ip9 + 1;
+ let ip11 = ip10 + 1;
+
+ // compute the stack indices
+ stacksIndices.unshift(i, ip1, ip2, i, ip2, ip3,
+ ip8, ip9, ip5, ip8, ip5, ip4,
+ ip7, ip6, ip10, ip7, ip10, ip11,
+ ip8, ip4, ip7, ip8, ip7, ip11,
+ ip5, ip9, ip10, ip5, ip10, ip6);
+
+ // compute the wireframe indices
+ if (coord.thickness !== 0) {
+ wireframeIndices.unshift(i, ip1, ip1, ip2,
+ ip2, ip3, ip3, i,
+ ip8, i, ip9, ip1,
+ ip11, ip3, ip10, ip2);
+ }
+
+ // there are 12 vertices in a stack representing a node
+ index += 12;
+
+ // set the maximum mesh width and height to calculate the center offset
+ mesh.width = Math.max(w, mesh.width);
+ mesh.height = Math.max(h, mesh.height);
+
+ // check if we need to save the currently active group; this happens after
+ // we filled all the "slots" in a group or there aren't any remaining nodes
+ if (((n + 1) % maxGroupNodes === 0) || (n === len - 1)) {
+ mesh.groups.push({
+ vertices: vertices,
+ texCoord: texCoord,
+ color: color,
+ stacksIndices: stacksIndices,
+ wireframeIndices: wireframeIndices
+ });
+ mesh.allVertices = mesh.allVertices.concat(vertices);
+ }
+ }
+
+ self.postMessage(mesh);
+ close();
+};
+
+/**
+ * Utility functions for generating random numbers using the Alea algorithm.
+ */
+self.random = {
+
+ /**
+ * The generator function, automatically created with seed 0.
+ */
+ _generator: null,
+
+ /**
+ * Returns a new random number between [0..1)
+ */
+ next: function RNG_next()
+ {
+ return this._generator();
+ },
+
+ /**
+ * From http://baagoe.com/en/RandomMusings/javascript
+ * Johannes Baagoe <baagoe@baagoe.com>, 2010
+ *
+ * Seeds a random generator function with a set of passed arguments.
+ */
+ seed: function RNG_seed()
+ {
+ let s0 = 0;
+ let s1 = 0;
+ let s2 = 0;
+ let c = 1;
+
+ if (arguments.length === 0) {
+ return this.seed(+new Date());
+ } else {
+ s0 = this.mash(" ");
+ s1 = this.mash(" ");
+ s2 = this.mash(" ");
+
+ for (let i = 0, len = arguments.length; i < len; i++) {
+ s0 -= this.mash(arguments[i]);
+ if (s0 < 0) {
+ s0 += 1;
+ }
+ s1 -= this.mash(arguments[i]);
+ if (s1 < 0) {
+ s1 += 1;
+ }
+ s2 -= this.mash(arguments[i]);
+ if (s2 < 0) {
+ s2 += 1;
+ }
+ }
+
+ let random = function() {
+ let t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
+ s0 = s1;
+ s1 = s2;
+ return (s2 = t - (c = t | 0));
+ };
+ random.uint32 = function() {
+ return random() * 0x100000000; // 2^32
+ };
+ random.fract53 = function() {
+ return random() +
+ (random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
+ };
+ return (this._generator = random);
+ }
+ },
+
+ /**
+ * From http://baagoe.com/en/RandomMusings/javascript
+ * Johannes Baagoe <baagoe@baagoe.com>, 2010
+ */
+ mash: function RNG_mash(data)
+ {
+ let h, n = 0xefc8249d;
+
+ for (let i = 0, data = data.toString(), len = data.length; i < len; i++) {
+ n += data.charCodeAt(i);
+ h = 0.02519603282416938 * n;
+ n = h >>> 0;
+ h -= n;
+ h *= n;
+ n = h >>> 0;
+ h -= n;
+ n += h * 0x100000000; // 2^32
+ }
+ return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
+ }
+};
diff --git a/browser/devtools/tilt/TiltWorkerPicker.js b/browser/devtools/tilt/TiltWorkerPicker.js
new file mode 100644
index 000000000..d35e7677d
--- /dev/null
+++ b/browser/devtools/tilt/TiltWorkerPicker.js
@@ -0,0 +1,186 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
+
+/**
+ * This worker handles picking, given a set of vertices and a ray (calculates
+ * the intersection points and offers back information about the closest hit).
+ *
+ * Used in the TiltVisualization.Presenter object.
+ */
+self.onmessage = function TWP_onMessage(event)
+{
+ let data = event.data;
+ let vertices = data.vertices;
+ let ray = data.ray;
+
+ let intersection = null;
+ let hit = [];
+
+ // calculates the squared distance between two points
+ function dsq(p1, p2) {
+ let xd = p2[0] - p1[0];
+ let yd = p2[1] - p1[1];
+ let zd = p2[2] - p1[2];
+
+ return xd * xd + yd * yd + zd * zd;
+ }
+
+ // check each stack face in the visualization mesh for intersections with
+ // the mouse ray (using a ray picking algorithm)
+ for (let i = 0, len = vertices.length; i < len; i += 36) {
+
+ // the front quad
+ let v0f = [vertices[i], vertices[i + 1], vertices[i + 2]];
+ let v1f = [vertices[i + 3], vertices[i + 4], vertices[i + 5]];
+ let v2f = [vertices[i + 6], vertices[i + 7], vertices[i + 8]];
+ let v3f = [vertices[i + 9], vertices[i + 10], vertices[i + 11]];
+
+ // the back quad
+ let v0b = [vertices[i + 24], vertices[i + 25], vertices[i + 26]];
+ let v1b = [vertices[i + 27], vertices[i + 28], vertices[i + 29]];
+ let v2b = [vertices[i + 30], vertices[i + 31], vertices[i + 32]];
+ let v3b = [vertices[i + 33], vertices[i + 34], vertices[i + 35]];
+
+ // don't do anything with degenerate quads
+ if (!v0f[0] && !v1f[0] && !v2f[0] && !v3f[0]) {
+ continue;
+ }
+
+ // for each triangle in the stack box, check for the intersections
+ if (self.intersect(v0f, v1f, v2f, ray, hit) || // front left
+ self.intersect(v0f, v2f, v3f, ray, hit) || // front right
+ self.intersect(v0b, v1b, v1f, ray, hit) || // left back
+ self.intersect(v0b, v1f, v0f, ray, hit) || // left front
+ self.intersect(v3f, v2b, v3b, ray, hit) || // right back
+ self.intersect(v3f, v2f, v2b, ray, hit) || // right front
+ self.intersect(v0b, v0f, v3f, ray, hit) || // top left
+ self.intersect(v0b, v3f, v3b, ray, hit) || // top right
+ self.intersect(v1f, v1b, v2b, ray, hit) || // bottom left
+ self.intersect(v1f, v2b, v2f, ray, hit)) { // bottom right
+
+ // calculate the distance between the intersection hit point and camera
+ let d = dsq(hit, ray.origin);
+
+ // we're picking the closest stack in the mesh from the camera
+ if (intersection === null || d < intersection.distance) {
+ intersection = {
+ // each mesh stack is composed of 12 vertices, so there's information
+ // about a node once in 12 * 3 = 36 iterations (to avoid duplication)
+ index: i / 36,
+ distance: d
+ };
+ }
+ }
+ }
+
+ self.postMessage(intersection);
+ close();
+};
+
+/**
+ * Utility function for finding intersections between a ray and a triangle.
+ */
+self.intersect = (function() {
+
+ // creates a new instance of a vector
+ function create() {
+ return new Float32Array(3);
+ }
+
+ // performs a vector addition
+ function add(aVec, aVec2, aDest) {
+ aDest[0] = aVec[0] + aVec2[0];
+ aDest[1] = aVec[1] + aVec2[1];
+ aDest[2] = aVec[2] + aVec2[2];
+ return aDest;
+ }
+
+ // performs a vector subtraction
+ function subtract(aVec, aVec2, aDest) {
+ aDest[0] = aVec[0] - aVec2[0];
+ aDest[1] = aVec[1] - aVec2[1];
+ aDest[2] = aVec[2] - aVec2[2];
+ return aDest;
+ }
+
+ // performs a vector scaling
+ function scale(aVec, aVal, aDest) {
+ aDest[0] = aVec[0] * aVal;
+ aDest[1] = aVec[1] * aVal;
+ aDest[2] = aVec[2] * aVal;
+ return aDest;
+ }
+
+ // generates the cross product of two vectors
+ function cross(aVec, aVec2, aDest) {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let x2 = aVec2[0];
+ let y2 = aVec2[1];
+ let z2 = aVec2[2];
+
+ aDest[0] = y * z2 - z * y2;
+ aDest[1] = z * x2 - x * z2;
+ aDest[2] = x * y2 - y * x2;
+ return aDest;
+ }
+
+ // calculates the dot product of two vectors
+ function dot(aVec, aVec2) {
+ return aVec[0] * aVec2[0] + aVec[1] * aVec2[1] + aVec[2] * aVec2[2];
+ }
+
+ let edge1 = create();
+ let edge2 = create();
+ let pvec = create();
+ let tvec = create();
+ let qvec = create();
+ let lvec = create();
+
+ // checks for ray-triangle intersections using the Fast Minimum-Storage
+ // (simplified) algorithm by Tomas Moller and Ben Trumbore
+ return function intersect(aVert0, aVert1, aVert2, aRay, aDest) {
+ let dir = aRay.direction;
+ let orig = aRay.origin;
+
+ // find vectors for two edges sharing vert0
+ subtract(aVert1, aVert0, edge1);
+ subtract(aVert2, aVert0, edge2);
+
+ // begin calculating determinant - also used to calculate the U parameter
+ cross(dir, edge2, pvec);
+
+ // if determinant is near zero, ray lines in plane of triangle
+ let inv_det = 1 / dot(edge1, pvec);
+
+ // calculate distance from vert0 to ray origin
+ subtract(orig, aVert0, tvec);
+
+ // calculate U parameter and test bounds
+ let u = dot(tvec, pvec) * inv_det;
+ if (u < 0 || u > 1) {
+ return false;
+ }
+
+ // prepare to test V parameter
+ cross(tvec, edge1, qvec);
+
+ // calculate V parameter and test bounds
+ let v = dot(dir, qvec) * inv_det;
+ if (v < 0 || u + v > 1) {
+ return false;
+ }
+
+ // calculate T, ray intersects triangle
+ let t = dot(edge2, qvec) * inv_det;
+
+ scale(dir, t, lvec);
+ add(orig, lvec, aDest);
+ return true;
+ };
+}());
diff --git a/browser/devtools/tilt/moz.build b/browser/devtools/tilt/moz.build
new file mode 100644
index 000000000..5abe8b3be
--- /dev/null
+++ b/browser/devtools/tilt/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
+
diff --git a/browser/devtools/tilt/test/Makefile.in b/browser/devtools/tilt/test/Makefile.in
new file mode 100644
index 000000000..cf3e6d351
--- /dev/null
+++ b/browser/devtools/tilt/test/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ head.js \
+ browser_tilt_01_lazy_getter.js \
+ browser_tilt_02_notifications-seq.js \
+ browser_tilt_02_notifications.js \
+ browser_tilt_02_notifications-tabs.js \
+ browser_tilt_03_tab_switch.js \
+ browser_tilt_04_initialization.js \
+ browser_tilt_05_destruction-esc.js \
+ browser_tilt_05_destruction-url.js \
+ browser_tilt_05_destruction.js \
+ browser_tilt_arcball-reset-typeahead.js \
+ browser_tilt_arcball-reset.js \
+ browser_tilt_arcball.js \
+ browser_tilt_controller.js \
+ browser_tilt_gl01.js \
+ browser_tilt_gl02.js \
+ browser_tilt_gl03.js \
+ browser_tilt_gl04.js \
+ browser_tilt_gl05.js \
+ browser_tilt_gl06.js \
+ browser_tilt_gl07.js \
+ browser_tilt_gl08.js \
+ browser_tilt_math01.js \
+ browser_tilt_math02.js \
+ browser_tilt_math03.js \
+ browser_tilt_math04.js \
+ browser_tilt_math05.js \
+ browser_tilt_math06.js \
+ browser_tilt_math07.js \
+ browser_tilt_picking.js \
+ browser_tilt_picking_inspector.js \
+ browser_tilt_picking_delete.js \
+ browser_tilt_picking_highlight01-offs.js \
+ browser_tilt_picking_highlight01.js \
+ browser_tilt_picking_highlight02.js \
+ browser_tilt_picking_highlight03.js \
+ browser_tilt_picking_miv.js \
+ browser_tilt_utils01.js \
+ browser_tilt_utils02.js \
+ browser_tilt_utils03.js \
+ browser_tilt_utils04.js \
+ browser_tilt_utils05.js \
+ browser_tilt_utils06.js \
+ browser_tilt_utils07.js \
+ browser_tilt_utils08.js \
+ browser_tilt_visualizer.js \
+ browser_tilt_zoom.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js b/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
new file mode 100644
index 000000000..de77ccb90
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ ok(Tilt,
+ "The Tilt object wasn't got correctly via defineLazyGetter.");
+ is(Tilt.chromeWindow, window,
+ "The top-level window wasn't saved correctly");
+ ok(Tilt.visualizers,
+ "The holder object for all the instances of the visualizer doesn't exist.")
+ ok(Tilt.NOTIFICATIONS,
+ "The notifications constants weren't referenced correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js b/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js
new file mode 100644
index 000000000..18a71338f
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tabEvents = "";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping notifications test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ createTab(function() {
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Services.obs.addObserver(obs_STARTUP, STARTUP, false);
+ Services.obs.addObserver(obs_INITIALIZING, INITIALIZING, false);
+ Services.obs.addObserver(obs_INITIALIZED, INITIALIZED, false);
+ Services.obs.addObserver(obs_DESTROYING, DESTROYING, false);
+ Services.obs.addObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED, false);
+ Services.obs.addObserver(obs_DESTROYED, DESTROYED, false);
+
+ info("Starting up the Tilt notifications test.");
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function obs_STARTUP(win) {
+ info("Handling the STARTUP notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "STARTUP;";
+}
+
+function obs_INITIALIZING(win) {
+ info("Handling the INITIALIZING notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZING;";
+}
+
+function obs_INITIALIZED(win) {
+ info("Handling the INITIALIZED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZED;";
+
+ Tilt.destroy(Tilt.currentWindowId, true);
+}
+
+function obs_DESTROYING(win) {
+ info("Handling the DESTROYING( notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYING;";
+}
+
+function obs_BEFORE_DESTROYED(win) {
+ info("Handling the BEFORE_DESTROYED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "BEFORE_DESTROYED;";
+}
+
+function obs_DESTROYED(win) {
+ info("Handling the DESTROYED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYED;";
+}
+
+function finalize(win) {
+ if (!tabEvents) {
+ return;
+ }
+
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ is(tabEvents, "STARTUP;INITIALIZING;INITIALIZED;DESTROYING;BEFORE_DESTROYED;DESTROYED;",
+ "The notifications weren't fired in the correct order.");
+
+ cleanup();
+}
+
+function cleanup() {
+ info("Cleaning up the notifications test.");
+
+ Services.obs.removeObserver(finalize, DESTROYED);
+ Services.obs.removeObserver(obs_INITIALIZING, INITIALIZING);
+ Services.obs.removeObserver(obs_INITIALIZED, INITIALIZED);
+ Services.obs.removeObserver(obs_DESTROYING, DESTROYING);
+ Services.obs.removeObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED);
+ Services.obs.removeObserver(obs_DESTROYED, DESTROYED);
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js b/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js
new file mode 100644
index 000000000..435af263c
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1, tab2;
+let testStep = -1;
+
+let expected = [];
+function expect(notification, win) {
+ expected.push({ notification: notification, window: win });
+}
+
+function notification(win, topic) {
+ if (expected.length == 0) {
+ is(topic, null, "Shouldn't see a notification");
+ return;
+ }
+
+ let { notification, window } = expected.shift();
+ is(topic, notification, "Saw the expected notification");
+ is(win, window, "Saw the expected window");
+}
+
+function after(notification, callback) {
+ function observer() {
+ Services.obs.removeObserver(observer, notification);
+ executeSoon(callback);
+ }
+ Services.obs.addObserver(observer, notification, false);
+}
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping tab switch test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping tab switch test because WebGL isn't supported.");
+ return;
+ }
+
+ Services.obs.addObserver(notification, STARTUP, false);
+ Services.obs.addObserver(notification, INITIALIZING, false);
+ Services.obs.addObserver(notification, INITIALIZED, false);
+ Services.obs.addObserver(notification, DESTROYING, false);
+ Services.obs.addObserver(notification, BEFORE_DESTROYED, false);
+ Services.obs.addObserver(notification, DESTROYED, false);
+ Services.obs.addObserver(notification, SHOWN, false);
+ Services.obs.addObserver(notification, HIDDEN, false);
+
+ waitForExplicitFinish();
+
+ tab0 = gBrowser.selectedTab;
+ nextStep();
+}
+
+function createTab2() {
+}
+
+let testSteps = [
+ function step0() {
+ tab1 = createTab(function() {
+ expect(STARTUP, tab1.linkedBrowser.contentWindow);
+ expect(INITIALIZING, tab1.linkedBrowser.contentWindow);
+ expect(INITIALIZED, tab1.linkedBrowser.contentWindow);
+ after(INITIALIZED, nextStep);
+
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ },
+ function step1() {
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+
+ tab2 = createTab(function() {
+ expect(STARTUP, tab2.linkedBrowser.contentWindow);
+ expect(INITIALIZING, tab2.linkedBrowser.contentWindow);
+ expect(INITIALIZED, tab2.linkedBrowser.contentWindow);
+ after(INITIALIZED, nextStep);
+
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ },
+ function step2() {
+ expect(HIDDEN, tab2.linkedBrowser.contentWindow);
+ after(HIDDEN, nextStep);
+
+ gBrowser.selectedTab = tab0;
+ },
+ function step3() {
+ expect(SHOWN, tab2.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab2;
+ },
+ function step4() {
+ expect(HIDDEN, tab2.linkedBrowser.contentWindow);
+ expect(SHOWN, tab1.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab1;
+ },
+ function step5() {
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+ expect(SHOWN, tab2.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab2;
+ },
+ function step6() {
+ expect(DESTROYING, tab2.linkedBrowser.contentWindow);
+ expect(BEFORE_DESTROYED, tab2.linkedBrowser.contentWindow);
+ expect(DESTROYED, tab2.linkedBrowser.contentWindow);
+ after(DESTROYED, nextStep);
+
+ Tilt.destroy(Tilt.currentWindowId, true);
+ },
+ function step7() {
+ expect(SHOWN, tab1.linkedBrowser.contentWindow);
+
+ gBrowser.removeCurrentTab();
+ tab2 = null;
+
+ expect(DESTROYING, tab1.linkedBrowser.contentWindow);
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+ expect(BEFORE_DESTROYED, tab1.linkedBrowser.contentWindow);
+ expect(DESTROYED, tab1.linkedBrowser.contentWindow);
+ after(DESTROYED, nextStep);
+
+ gBrowser.removeCurrentTab();
+ tab1 = null;
+ },
+ function step8_cleanup() {
+ is(gBrowser.selectedTab, tab0, "Should be back to the first tab");
+
+ cleanup();
+ }
+];
+
+function cleanup() {
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ tab1 = null;
+ }
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ tab2 = null;
+ }
+
+ Services.obs.removeObserver(notification, STARTUP);
+ Services.obs.removeObserver(notification, INITIALIZING);
+ Services.obs.removeObserver(notification, INITIALIZED);
+ Services.obs.removeObserver(notification, DESTROYING);
+ Services.obs.removeObserver(notification, BEFORE_DESTROYED);
+ Services.obs.removeObserver(notification, DESTROYED);
+ Services.obs.removeObserver(notification, SHOWN);
+ Services.obs.removeObserver(notification, HIDDEN);
+
+ finish();
+}
+
+function nextStep() {
+ let step = testSteps.shift();
+ info("Executing " + step.name);
+ step();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications.js b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
new file mode 100644
index 000000000..fe42001f1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1;
+let testStep = -1;
+let tabEvents = "";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping notifications test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
+ createNewTab();
+}
+
+function createNewTab() {
+ tab0 = gBrowser.selectedTab;
+
+ tab1 = createTab(function() {
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Services.obs.addObserver(tab_STARTUP, STARTUP, false);
+ Services.obs.addObserver(tab_INITIALIZING, INITIALIZING, false);
+ Services.obs.addObserver(tab_DESTROYING, DESTROYING, false);
+ Services.obs.addObserver(tab_SHOWN, SHOWN, false);
+ Services.obs.addObserver(tab_HIDDEN, HIDDEN, false);
+
+ info("Starting up the Tilt notifications test.");
+ createTilt({
+ onTiltOpen: function()
+ {
+ testStep = 0;
+ tabSelect();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function tab_STARTUP(win) {
+ info("Handling the STARTUP notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "STARTUP;";
+}
+
+function tab_INITIALIZING(win) {
+ info("Handling the INITIALIZING notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZING;";
+}
+
+function tab_DESTROYING(win) {
+ info("Handling the DESTROYING notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYING;";
+}
+
+function tab_SHOWN(win) {
+ info("Handling the SHOWN notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "SHOWN;";
+}
+
+function tab_HIDDEN(win) {
+ info("Handling the HIDDEN notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "HIDDEN;";
+}
+
+let testSteps = [
+ function step0() {
+ info("Selecting tab0.");
+ gBrowser.selectedTab = tab0;
+ },
+ function step1() {
+ info("Selecting tab1.");
+ gBrowser.selectedTab = tab1;
+ },
+ function step2() {
+ info("Killing it.");
+ Tilt.destroy(Tilt.currentWindowId, true);
+ }
+];
+
+function finalize(win) {
+ if (!tabEvents) {
+ return;
+ }
+
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+
+ is(tabEvents, "STARTUP;INITIALIZING;HIDDEN;SHOWN;DESTROYING;",
+ "The notifications weren't fired in the correct order.");
+
+ cleanup();
+}
+
+function cleanup() {
+ info("Cleaning up the notifications test.");
+
+ tab0 = null;
+ tab1 = null;
+
+ Services.obs.removeObserver(finalize, DESTROYED);
+ Services.obs.removeObserver(tab_INITIALIZING, INITIALIZING);
+ Services.obs.removeObserver(tab_DESTROYING, DESTROYING);
+ Services.obs.removeObserver(tab_SHOWN, SHOWN);
+ Services.obs.removeObserver(tab_HIDDEN, HIDDEN);
+
+ gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect);
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function tabSelect() {
+ if (testStep !== -1) {
+ executeSoon(testSteps[testStep]);
+ testStep++;
+ }
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js b/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
new file mode 100644
index 000000000..0ea5c886f
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1, tab2;
+let testStep = -1;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping tab switch test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping tab switch test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
+ createTab1();
+}
+
+function createTab1() {
+ tab0 = gBrowser.selectedTab;
+
+ tab1 = createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ createTab2();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function createTab2() {
+ tab2 = createTab(function() {
+
+ createTilt({
+ onTiltOpen: function()
+ {
+ testStep = 0;
+ tabSelect();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+let testSteps = [
+ function step0() {
+ gBrowser.selectedTab = tab1;
+ },
+ function step1() {
+ gBrowser.selectedTab = tab0;
+ },
+ function step2() {
+ gBrowser.selectedTab = tab1;
+ },
+ function step3() {
+ gBrowser.selectedTab = tab2;
+ },
+ function step4() {
+ Tilt.destroy(Tilt.currentWindowId);
+ gBrowser.removeCurrentTab();
+ tab2 = null;
+ },
+ function step5() {
+ Tilt.destroy(Tilt.currentWindowId);
+ gBrowser.removeCurrentTab();
+ tab1 = null;
+ },
+ function step6_cleanup() {
+ cleanup();
+ }
+];
+
+function cleanup() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, false);
+
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ tab1 = null;
+ }
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ tab2 = null;
+ }
+
+ finish();
+}
+
+function tabSelect() {
+ if (testStep !== -1) {
+ executeSoon(testSteps[testStep]);
+ testStep++;
+ }
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_04_initialization.js b/browser/devtools/tilt/test/browser_tilt_04_initialization.js
new file mode 100644
index 000000000..314fb22e6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_04_initialization.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping initialization test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping initialization test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(id, Tilt.currentWindowId,
+ "The unique window identifiers should match for the same window.");
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ is(document.activeElement, instance.presenter.canvas,
+ "The visualizer canvas should be focused on initialization.");
+
+ ok(Tilt.visualizers[id] instanceof TiltVisualizer,
+ "A new instance of the visualizer wasn't created properly.");
+ ok(Tilt.visualizers[id].isInitialized(),
+ "The new instance of the visualizer wasn't initialized properly.");
+ },
+ onTiltClose: function()
+ {
+ is(document.activeElement, gBrowser.selectedBrowser,
+ "The focus wasn't correctly given back to the selectedBrowser.");
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+ },
+ onEnd: function()
+ {
+ cleanup();
+ }
+ }, true, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js b/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
new file mode 100644
index 000000000..503f79254
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ EventUtils.sendKey("ESCAPE");
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js b/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
new file mode 100644
index 000000000..61d428218
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ window.content.location = "about:mozilla";
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction.js b/browser/devtools/tilt/test/browser_tilt_05_destruction.js
new file mode 100644
index 000000000..a083fa1bc
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
new file mode 100644
index 000000000..366bfa323
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping part of the arcball test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping part of the arcball test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ performTest(instance.presenter.canvas,
+ instance.controller.arcball, function() {
+
+ info("Killing arcball reset test.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function performTest(canvas, arcball, callback) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+
+ info("Starting arcball reset test.");
+
+ // start translating and rotating sometime at random
+
+ window.setTimeout(function() {
+ info("Synthesizing key down events.");
+
+ EventUtils.synthesizeKey("VK_S", { type: "keydown" }); // add a little
+ EventUtils.synthesizeKey("VK_RIGHT", { type: "keydown" }); // diversity
+
+ // wait for some arcball translations and rotations to happen
+
+ window.setTimeout(function() {
+ info("Synthesizing key up events.");
+
+ EventUtils.synthesizeKey("VK_S", { type: "keyup" });
+ EventUtils.synthesizeKey("VK_RIGHT", { type: "keyup" });
+
+ // ok, transformations finished, we can now try to reset the model view
+
+ window.setTimeout(function() {
+ info("Synthesizing arcball reset key press.");
+
+ arcball._onResetStart = function() {
+ info("Starting arcball reset animation.");
+ };
+
+ arcball._onResetStep = function() {
+ info("\nlastRot: " + quat4.str(arcball._lastRot) +
+ "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+ "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+ "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+ "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+ "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+ "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+ "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+ "\nzoomAmount: " + arcball._zoomAmount);
+ };
+
+ arcball._onResetFinish = function() {
+ ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+ "The arcball _lastRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+ "The arcball _deltaRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+ "The arcball _currentRot field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+ "The arcball _lastTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+ "The arcball _deltaTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+ "The arcball _currentTrans field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+ "The arcball _additionalRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+ "The arcball _additionalTrans field wasn't reset correctly.");
+
+ ok(isApproxVec([arcball._zoomAmount], [0]),
+ "The arcball _zoomAmount field wasn't reset correctly.");
+
+ executeSoon(function() {
+ info("Finishing arcball reset test.");
+ callback();
+ });
+ };
+
+ EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+
+ }, Math.random() * 1000); // leave enough time for transforms to happen
+ }, Math.random() * 1000);
+ }, Math.random() * 1000);
+}
+
+function cleanup() {
+ info("Cleaning up arcball reset test.");
+
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
new file mode 100644
index 000000000..72e11236e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping part of the arcball test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping part of the arcball test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ performTest(instance.presenter.canvas,
+ instance.controller.arcball, function() {
+
+ info("Killing arcball reset test.");
+
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function performTest(canvas, arcball, callback) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+
+ info("Starting arcball reset test.");
+
+ // start translating and rotating sometime at random
+
+ window.setTimeout(function() {
+ info("Synthesizing key down events.");
+
+ EventUtils.synthesizeKey("VK_W", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+
+ // wait for some arcball translations and rotations to happen
+
+ window.setTimeout(function() {
+ info("Synthesizing key up events.");
+
+ EventUtils.synthesizeKey("VK_W", { type: "keyup" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
+
+ // ok, transformations finished, we can now try to reset the model view
+
+ window.setTimeout(function() {
+ info("Synthesizing arcball reset key press.");
+
+ arcball._onResetStart = function() {
+ info("Starting arcball reset animation.");
+ };
+
+ arcball._onResetStep = function() {
+ info("\nlastRot: " + quat4.str(arcball._lastRot) +
+ "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+ "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+ "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+ "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+ "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+ "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+ "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+ "\nzoomAmount: " + arcball._zoomAmount);
+ };
+
+ arcball._onResetFinish = function() {
+ ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+ "The arcball _lastRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+ "The arcball _deltaRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+ "The arcball _currentRot field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+ "The arcball _lastTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+ "The arcball _deltaTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+ "The arcball _currentTrans field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+ "The arcball _additionalRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+ "The arcball _additionalTrans field wasn't reset correctly.");
+
+ ok(isApproxVec([arcball._zoomAmount], [0]),
+ "The arcball _zoomAmount field wasn't reset correctly.");
+
+ executeSoon(function() {
+ info("Finishing arcball reset test.");
+ callback();
+ });
+ };
+
+ EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+
+ }, Math.random() * 1000); // leave enough time for transforms to happen
+ }, Math.random() * 1000);
+ }, Math.random() * 1000);
+}
+
+function cleanup() {
+ info("Cleaning up arcball reset test.");
+
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball.js b/browser/devtools/tilt/test/browser_tilt_arcball.js
new file mode 100644
index 000000000..3d1078e1b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball.js
@@ -0,0 +1,496 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function cloneUpdate(update) {
+ return {
+ rotation: quat4.create(update.rotation),
+ translation: vec3.create(update.translation)
+ };
+}
+
+function isExpectedUpdate(update1, update2) {
+ if (update1.length !== update2.length) {
+ return false;
+ }
+ for (let i = 0, len = update1.length; i < len; i++) {
+ if (!isApproxVec(update1[i].rotation, update2[i].rotation) ||
+ !isApproxVec(update1[i].translation, update2[i].translation)) {
+ info("isExpectedUpdate expected " + JSON.stringify(update1), ", got " +
+ JSON.stringify(update2) + " instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function test() {
+ let arcball1 = new TiltVisualizer.Arcball(window, 123, 456);
+
+ is(arcball1.width, 123,
+ "The first arcball width wasn't set correctly.");
+ is(arcball1.height, 456,
+ "The first arcball height wasn't set correctly.");
+ is(arcball1.radius, 123,
+ "The first arcball radius wasn't implicitly set correctly.");
+
+
+ let arcball2 = new TiltVisualizer.Arcball(window, 987, 654);
+
+ is(arcball2.width, 987,
+ "The second arcball width wasn't set correctly.");
+ is(arcball2.height, 654,
+ "The second arcball height wasn't set correctly.");
+ is(arcball2.radius, 654,
+ "The second arcball radius wasn't implicitly set correctly.");
+
+
+ let arcball3 = new TiltVisualizer.Arcball(window, 512, 512);
+
+ let sphereVec = vec3.create();
+ arcball3._pointToSphere(123, 456, 256, 512, 512, sphereVec);
+
+ ok(isApproxVec(sphereVec, [-0.009765625, 0.390625, 0.9204980731010437]),
+ "The _pointToSphere() function didn't map the coordinates correctly.");
+
+ let stack1 = [];
+ let expect1 = [
+ { rotation: [
+ -0.08877250552177429, 0.0242881178855896,
+ -0.04222869873046875, -0.9948599338531494],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.13086390495300293, 0.03413732722401619,
+ -0.06334304809570312, -0.9887855648994446],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.15138940513134003, 0.03854173421859741,
+ -0.07390022277832031, -0.9849540591239929],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.1615273654460907, 0.040619146078825,
+ -0.0791788101196289, -0.9828477501869202],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16656573116779327, 0.04162723943591118,
+ -0.0818181037902832, -0.9817478656768799],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16907735168933868, 0.042123712599277496,
+ -0.08313775062561035, -0.9811863303184509],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17033125460147858, 0.042370058596134186,
+ -0.08379757404327393, -0.9809026718139648],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17095772922039032, 0.04249274358153343,
+ -0.08412748575210571, -0.9807600975036621],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17127084732055664, 0.04255397245287895,
+ -0.0842924416065216, -0.9806886315345764],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.171427384018898, 0.042584557086229324,
+ -0.08437491953372955, -0.9806528687477112],
+ translation: [0, 0, 0] }];
+
+ arcball3.mouseDown(10, 10, 1);
+ arcball3.mouseMove(10, 100);
+ for (let i1 = 0; i1 < 10; i1++) {
+ stack1.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack1, expect1),
+ "Mouse down & move events didn't create the expected transform. results.");
+
+ let stack2 = [];
+ let expect2 = [
+ { rotation: [
+ -0.1684110015630722, 0.04199237748980522,
+ -0.0827873945236206, -0.9813361167907715],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16936375200748444, 0.04218007251620293,
+ -0.08328840136528015, -0.9811217188835144],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17003019154071808, 0.04231100529432297,
+ -0.08363909274339676, -0.9809709787368774],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17049652338027954, 0.042402446269989014,
+ -0.0838845893740654, -0.9808651208877563],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17082282900810242, 0.042466338723897934,
+ -0.08405643701553345, -0.9807908535003662],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17105120420455933, 0.04251104220747948,
+ -0.08417671173810959, -0.9807388186454773],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17121103405952454, 0.04254228621721268,
+ -0.08426092565059662, -0.9807023406028748],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17132291197776794, 0.042564138770103455,
+ -0.08431987464427948, -0.9806767106056213],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.1714012324810028, 0.04257945716381073,
+ -0.08436112850904465, -0.9806588888168335],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17145603895187378, 0.042590171098709106,
+ -0.08439001441001892, -0.9806463718414307],
+ translation: [0, 0, 0] }];
+
+ arcball3.mouseUp(100, 100);
+ for (let i2 = 0; i2 < 10; i2++) {
+ stack2.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack2, expect2),
+ "Mouse up events didn't create the expected transformation results.");
+
+ let stack3 = [];
+ let expect3 = [
+ { rotation: [
+ -0.17149439454078674, 0.04259764403104782,
+ -0.08441022783517838, -0.9806375503540039],
+ translation: [0, 0, -1] },
+ { rotation: [
+ -0.17152123153209686, 0.04260288551449776,
+ -0.08442437648773193, -0.980631411075592],
+ translation: [0, 0, -1.899999976158142] },
+ { rotation: [
+ -0.1715400665998459, 0.04260658100247383,
+ -0.08443428575992584, -0.9806271195411682],
+ translation: [0, 0, -2.7100000381469727] },
+ { rotation: [
+ -0.17155319452285767, 0.04260912910103798,
+ -0.08444121479988098, -0.9806240797042847],
+ translation: [0, 0, -3.439000129699707] },
+ { rotation: [
+ -0.17156240344047546, 0.042610932141542435,
+ -0.08444607257843018, -0.9806219935417175],
+ translation: [0, 0, -4.095099925994873] },
+ { rotation: [
+ -0.1715688556432724, 0.042612191289663315,
+ -0.08444946259260178, -0.9806205034255981],
+ translation: [0, 0, -4.685589790344238] },
+ { rotation: [
+ -0.17157337069511414, 0.04261308163404465,
+ -0.0844518393278122, -0.980619490146637],
+ translation: [0, 0, -5.217031002044678] },
+ { rotation: [
+ -0.17157652974128723, 0.0426136814057827,
+ -0.0844535157084465, -0.9806187748908997],
+ translation: [0, 0, -5.6953277587890625] },
+ { rotation: [
+ -0.17157875001430511, 0.04261413961648941,
+ -0.08445467799901962, -0.9806182980537415],
+ translation: [0, 0, -6.125794887542725] },
+ { rotation: [
+ -0.17158031463623047, 0.04261442646384239,
+ -0.08445550501346588, -0.980617880821228],
+ translation: [0, 0, -6.5132155418396] }];
+
+ arcball3.zoom(10);
+ for (let i3 = 0; i3 < 10; i3++) {
+ stack3.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack3, expect3),
+ "Mouse zoom events didn't create the expected transformation results.");
+
+ let stack4 = [];
+ let expect4 = [
+ { rotation: [
+ -0.17158135771751404, 0.04261462762951851,
+ -0.08445606380701065, -0.9806176424026489],
+ translation: [0, 0, -6.861894130706787] },
+ { rotation: [
+ -0.1715821474790573, 0.04261479899287224,
+ -0.08445646613836288, -0.9806175231933594],
+ translation: [0, 0, -7.1757049560546875] },
+ { rotation: [
+ -0.1715826541185379, 0.0426148846745491,
+ -0.08445674180984497, -0.980617344379425],
+ translation: [0, 0, -7.458134651184082] },
+ { rotation: [
+ -0.17158304154872894, 0.04261497035622597,
+ -0.08445693552494049, -0.9806172847747803],
+ translation: [0, 0, -7.7123212814331055] },
+ { rotation: [
+ -0.17158329486846924, 0.042615000158548355,
+ -0.08445708453655243, -0.9806172251701355],
+ translation: [0, 0, -7.941089153289795] },
+ { rotation: [
+ -0.17158347368240356, 0.04261505603790283,
+ -0.084457166492939, -0.9806172251701355],
+ translation: [0, 0, -8.146980285644531] },
+ { rotation: [
+ -0.1715836226940155, 0.04261508584022522,
+ -0.08445724099874496, -0.9806171655654907],
+ translation: [0, 0, -8.332282066345215] },
+ { rotation: [
+ -0.17158368229866028, 0.04261508584022522,
+ -0.08445728570222855, -0.980617105960846],
+ translation: [0, 0, -8.499053955078125] },
+ { rotation: [
+ -0.17158377170562744, 0.04261511191725731,
+ -0.08445732295513153, -0.980617105960846],
+ translation: [0, 0, -8.649148941040039] },
+ { rotation: [
+ -0.17158380150794983, 0.04261511191725731,
+ -0.08445733785629272, -0.980617105960846],
+ translation: [0, 0, -8.784234046936035] }];
+
+ arcball3.keyDown(arcball3.rotateKeys.left);
+ arcball3.keyDown(arcball3.rotateKeys.right);
+ arcball3.keyDown(arcball3.rotateKeys.up);
+ arcball3.keyDown(arcball3.rotateKeys.down);
+ arcball3.keyDown(arcball3.panKeys.left);
+ arcball3.keyDown(arcball3.panKeys.right);
+ arcball3.keyDown(arcball3.panKeys.up);
+ arcball3.keyDown(arcball3.panKeys.down);
+ for (let i4 = 0; i4 < 10; i4++) {
+ stack4.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack4, expect4),
+ "Key down events didn't create the expected transformation results.");
+
+ let stack5 = [];
+ let expect5 = [
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -8.905810356140137] },
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -9.015229225158691] },
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -9.113706588745117] },
+ { rotation: [
+ -0.1715838611125946, 0.04261511191725731,
+ -0.0844573825597763, -0.9806170463562012],
+ translation: [0, 0, -9.202336311340332] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.282102584838867] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.35389232635498] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.418502807617188] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.476652145385742] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.528986930847168] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.576087951660156] }];
+
+ arcball3.keyUp(arcball3.rotateKeys.left);
+ arcball3.keyUp(arcball3.rotateKeys.right);
+ arcball3.keyUp(arcball3.rotateKeys.up);
+ arcball3.keyUp(arcball3.rotateKeys.down);
+ arcball3.keyUp(arcball3.panKeys.left);
+ arcball3.keyUp(arcball3.panKeys.right);
+ arcball3.keyUp(arcball3.panKeys.up);
+ arcball3.keyUp(arcball3.panKeys.down);
+ for (let i5 = 0; i5 < 10; i5++) {
+ stack5.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack5, expect5),
+ "Key up events didn't create the expected transformation results.");
+
+ let stack6 = [];
+ let expect6 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.618478775024414] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -6.156630992889404] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 0.4590320587158203] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 9.913128852844238] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 21.921815872192383] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 36.22963333129883] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 52.60667037963867] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 70.84600067138672] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 90.76139831542969] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 112.18525695800781] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["in"][0]);
+ arcball3.keyDown(arcball3.zoomKeys["in"][1]);
+ arcball3.keyDown(arcball3.zoomKeys["in"][2]);
+ for (let i6 = 0; i6 < 10; i6++) {
+ stack6.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["in"][0]);
+ arcball3.keyUp(arcball3.zoomKeys["in"][1]);
+ arcball3.keyUp(arcball3.zoomKeys["in"][2]);
+
+ ok(isExpectedUpdate(stack6, expect6),
+ "Key zoom in events didn't create the expected transformation results.");
+
+ let stack7 = [];
+ let expect7 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 134.96673583984375] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 151.97006225585938] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 163.77305603027344] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 170.895751953125] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 173.80618286132812] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 172.92556762695312] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 168.6330108642578] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 161.26971435546875] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 151.1427459716797] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 138.52847290039062] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["out"][0]);
+ arcball3.keyDown(arcball3.zoomKeys["out"][1]);
+ for (let i7 = 0; i7 < 10; i7++) {
+ stack7.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["out"][0]);
+ arcball3.keyUp(arcball3.zoomKeys["out"][1]);
+
+ ok(isExpectedUpdate(stack7, expect7),
+ "Key zoom out events didn't create the expected transformation results.");
+
+ let stack8 = [];
+ let expect8 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 123.67562866210938] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 111.30806732177734] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 100.17726135253906] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 90.15953826904297] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 81.14358520507812] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 73.02922821044922] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 65.72630310058594] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 59.15367126464844] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 53.238304138183594] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 47.91447448730469] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["unzoom"]);
+ for (let i8 = 0; i8 < 10; i8++) {
+ stack8.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["unzoom"]);
+
+ ok(isExpectedUpdate(stack8, expect8),
+ "Key zoom reset events didn't create the expected transformation results.");
+
+
+ arcball3.resize(123, 456);
+ is(arcball3.width, 123,
+ "The third arcball width wasn't updated correctly.");
+ is(arcball3.height, 456,
+ "The third arcball height wasn't updated correctly.");
+ is(arcball3.radius, 123,
+ "The third arcball radius wasn't implicitly updated correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_controller.js b/browser/devtools/tilt/test/browser_tilt_controller.js
new file mode 100644
index 000000000..0dbf37aad
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_controller.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping controller test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping controller test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ let canvas = instance.presenter.canvas;
+ let prev_tran = vec3.create([0, 0, 0]);
+ let prev_rot = quat4.create([0, 0, 0, 1]);
+
+ function tran() {
+ return instance.presenter.transforms.translation;
+ }
+
+ function rot() {
+ return instance.presenter.transforms.rotation;
+ }
+
+ function save() {
+ prev_tran = vec3.create(tran());
+ prev_rot = quat4.create(rot());
+ }
+
+ ok(isEqualVec(tran(), prev_tran),
+ "At init, the translation should be zero.");
+ ok(isEqualVec(rot(), prev_rot),
+ "At init, the rotation should be zero.");
+
+
+ function testEventCancel(cancellingEvent) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+ EventUtils.synthesizeKey("VK_A", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+ instance.controller._update();
+
+ ok(!isEqualVec(tran(), prev_tran),
+ "After a translation key is pressed, the vector should change.");
+ ok(!isEqualVec(rot(), prev_rot),
+ "After a rotation key is pressed, the quaternion should change.");
+
+ save();
+
+
+ cancellingEvent();
+ instance.controller._update();
+
+ ok(!isEqualVec(tran(), prev_tran),
+ "Even if the canvas lost focus, the vector has some inertia.");
+ ok(!isEqualVec(rot(), prev_rot),
+ "Even if the canvas lost focus, the quaternion has some inertia.");
+
+ save();
+
+
+ while (!isEqualVec(tran(), prev_tran) ||
+ !isEqualVec(rot(), prev_rot)) {
+ instance.controller._update();
+ save();
+ }
+
+ ok(isEqualVec(tran(), prev_tran) && isEqualVec(rot(), prev_rot),
+ "After focus lost, the transforms inertia eventually stops.");
+ }
+
+ info("Setting typeaheadfind to true.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
+ });
+
+ info("Setting typeaheadfind to false.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
+ });
+
+ info("Testing if loosing focus halts any stacked arcball animations.");
+
+ testEventCancel(function() {
+ gBrowser.selectedBrowser.contentWindow.focus();
+ });
+ },
+ onEnd: function()
+ {
+ cleanup();
+ }
+ }, true, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl01.js b/browser/devtools/tilt/test/browser_tilt_gl01.js
new file mode 100644
index 000000000..7f931d7a3
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl01.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl01 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ ok(renderer,
+ "The TiltGL.Renderer constructor should have initialized a new object.");
+
+ ok(gl instanceof WebGLRenderingContext,
+ "The renderer context wasn't created correctly from the passed canvas.");
+
+
+ let clearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE),
+ clearDepth = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
+
+ is(clearColor[0], 0,
+ "The default red clear color wasn't set correctly at initialization.");
+ is(clearColor[1], 0,
+ "The default green clear color wasn't set correctly at initialization.");
+ is(clearColor[2], 0,
+ "The default blue clear color wasn't set correctly at initialization.");
+ is(clearColor[3], 0,
+ "The default alpha clear color wasn't set correctly at initialization.");
+ is(clearDepth, 1,
+ "The default clear depth wasn't set correctly at initialization.");
+
+ is(renderer.width, canvas.width,
+ "The renderer width wasn't set correctly from the passed canvas.");
+ is(renderer.height, canvas.height,
+ "The renderer height wasn't set correctly from the passed canvas.");
+
+ ok(renderer.mvMatrix,
+ "The model view matrix wasn't initialized properly.");
+ ok(renderer.projMatrix,
+ "The model view matrix wasn't initialized properly.");
+
+ ok(isApproxVec(renderer._fillColor, [1, 1, 1, 1]),
+ "The default fill color wasn't set correctly.");
+ ok(isApproxVec(renderer._strokeColor, [0, 0, 0, 1]),
+ "The default stroke color wasn't set correctly.");
+ is(renderer._strokeWeightValue, 1,
+ "The default stroke weight wasn't set correctly.");
+
+ ok(renderer._colorShader,
+ "A default color shader should have been created.");
+
+ ok(typeof renderer.Program, "function",
+ "At init, the renderer should have created a Program constructor.");
+ ok(typeof renderer.VertexBuffer, "function",
+ "At init, the renderer should have created a VertexBuffer constructor.");
+ ok(typeof renderer.IndexBuffer, "function",
+ "At init, the renderer should have created a IndexBuffer constructor.");
+ ok(typeof renderer.Texture, "function",
+ "At init, the renderer should have created a Texture constructor.");
+
+ renderer.depthTest(true);
+ is(gl.getParameter(gl.DEPTH_TEST), true,
+ "The depth test wasn't enabled when requested.");
+
+ renderer.depthTest(false);
+ is(gl.getParameter(gl.DEPTH_TEST), false,
+ "The depth test wasn't disabled when requested.");
+
+ renderer.stencilTest(true);
+ is(gl.getParameter(gl.STENCIL_TEST), true,
+ "The stencil test wasn't enabled when requested.");
+
+ renderer.stencilTest(false);
+ is(gl.getParameter(gl.STENCIL_TEST), false,
+ "The stencil test wasn't disabled when requested.");
+
+ renderer.cullFace("front");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.FRONT,
+ "The cull face front mode wasn't set correctly.");
+
+ renderer.cullFace("back");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.BACK,
+ "The cull face back mode wasn't set correctly.");
+
+ renderer.cullFace("both");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.FRONT_AND_BACK,
+ "The cull face back mode wasn't set correctly.");
+
+ renderer.cullFace(false);
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The cull face wasn't disabled when requested.");
+
+ renderer.frontFace("cw");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CW,
+ "The front face cw mode wasn't set correctly.");
+
+ renderer.frontFace("ccw");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CCW,
+ "The front face ccw mode wasn't set correctly.");
+
+ renderer.blendMode("alpha");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't enabled when requested.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set correctly.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE_MINUS_SRC_ALPHA,
+ "The destination blend func wasn't set correctly.");
+
+ renderer.blendMode("add");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't enabled when requested.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set correctly.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE,
+ "The destination blend func wasn't set correctly.");
+
+ renderer.blendMode(false);
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The blend mode wasn't disabled when requested.");
+
+
+ is(gl.getParameter(gl.CURRENT_PROGRAM), null,
+ "No program should be initially set in the WebGL context.");
+
+ renderer.useColorShader(new renderer.VertexBuffer([1, 2, 3], 3));
+
+ ok(gl.getParameter(gl.CURRENT_PROGRAM) instanceof WebGLProgram,
+ "The correct program hasn't been set in the WebGL context.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl02.js b/browser/devtools/tilt/test/browser_tilt_gl02.js
new file mode 100644
index 000000000..a55acdd41
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl02 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.fill([1, 0, 0, 1]);
+ ok(isApproxVec(renderer._fillColor, [1, 0, 0, 1]),
+ "The fill color wasn't set correctly.");
+
+ renderer.stroke([0, 1, 0, 1]);
+ ok(isApproxVec(renderer._strokeColor, [0, 1, 0, 1]),
+ "The stroke color wasn't set correctly.");
+
+ renderer.strokeWeight(2);
+ is(renderer._strokeWeightValue, 2,
+ "The stroke weight wasn't set correctly.");
+ is(gl.getParameter(gl.LINE_WIDTH), 2,
+ "The stroke weight wasn't applied correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl03.js b/browser/devtools/tilt/test/browser_tilt_gl03.js
new file mode 100644
index 000000000..491f9279b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl03.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl03 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.defaults();
+ is(gl.getParameter(gl.DEPTH_TEST), true,
+ "The depth test wasn't set to the correct default value.");
+ is(gl.getParameter(gl.STENCIL_TEST), false,
+ "The stencil test wasn't set to the correct default value.");
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The cull face wasn't set to the correct default value.");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CCW,
+ "The front face wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE_MINUS_SRC_ALPHA,
+ "The destination blend func wasn't set to the correct default value.");
+
+
+ ok(isApproxVec(renderer._fillColor, [1, 1, 1, 1]),
+ "The fill color wasn't set to the correct default value.");
+ ok(isApproxVec(renderer._strokeColor, [0, 0, 0, 1]),
+ "The stroke color wasn't set to the correct default value.");
+ is(renderer._strokeWeightValue, 1,
+ "The stroke weight wasn't set to the correct default value.");
+ is(gl.getParameter(gl.LINE_WIDTH), 1,
+ "The stroke weight wasn't applied with the correct default value.");
+
+
+ ok(isApproxVec(renderer.projMatrix, [
+ 1.2071068286895752, 0, 0, 0, 0, -2.4142136573791504, 0, 0, 0, 0,
+ -1.0202020406723022, -1, -181.06602478027344, 181.06602478027344,
+ 148.14492797851562, 181.06602478027344
+ ]), "The default perspective projection matrix wasn't set correctly.");
+
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "The default model view matrix wasn't set correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl04.js b/browser/devtools/tilt/test/browser_tilt_gl04.js
new file mode 100644
index 000000000..4ccfd472a
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl04.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl04 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.perspective();
+ ok(isApproxVec(renderer.projMatrix, [
+ 1.2071068286895752, 0, 0, 0, 0, -2.4142136573791504, 0, 0, 0, 0,
+ -1.0202020406723022, -1, -181.06602478027344, 181.06602478027344,
+ 148.14492797851562, 181.06602478027344
+ ]), "The default perspective proj. matrix wasn't calculated correctly.");
+
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Changing the perpective matrix should reset the modelview by default.");
+
+
+ renderer.ortho();
+ ok(isApproxVec(renderer.projMatrix, [
+ 0.006666666828095913, 0, 0, 0, 0, -0.013333333656191826, 0, 0, 0, 0, -1,
+ 0, -1, 1, 0, 1
+ ]), "The default ortho proj. matrix wasn't calculated correctly.");
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Changing the ortho matrix should reset the modelview by default.");
+
+
+ renderer.projection(mat4.perspective(45, 1, 0.1, 100));
+ ok(isApproxVec(renderer.projMatrix, [
+ 2.4142136573791504, 0, 0, 0, 0, 2.4142136573791504, 0, 0, 0, 0,
+ -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "A custom proj. matrix couldn't be set correctly.");
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Setting a custom proj. matrix should reset the model view by default.");
+
+
+ renderer.translate(1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1
+ ]), "The translation transformation wasn't applied correctly.");
+
+ renderer.rotate(0.5, 1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.9183883666992188, 0.317602276802063, -0.23599065840244293, 0,
+ -0.23599065840244293, 0.9183883666992188, 0.317602276802063, 0,
+ 0.317602276802063, -0.23599065840244293, 0.9183883666992188, 0, 1, 1, 1, 1
+ ]), "The rotation transformation wasn't applied correctly.");
+
+ renderer.rotateX(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.9183883666992188, 0.317602276802063, -0.23599065840244293, 0,
+ -0.05483464524149895, 0.6928216814994812, 0.7190210819244385, 0,
+ 0.391862154006958, -0.6474001407623291, 0.6536949872970581, 0, 1, 1, 1, 1
+ ]), "The X rotation transformation wasn't applied correctly.");
+
+ renderer.rotateY(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.6180928945541382, 0.5891023874282837, -0.5204993486404419, 0,
+ -0.05483464524149895, 0.6928216814994812, 0.7190210819244385, 0,
+ 0.7841902375221252, -0.4158804416656494, 0.4605313837528229, 0, 1, 1, 1, 1
+ ]), "The Y rotation transformation wasn't applied correctly.");
+
+ renderer.rotateZ(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.5161384344100952, 0.8491423726081848, -0.11206408590078354, 0,
+ -0.3444514572620392, 0.3255774974822998, 0.8805410265922546, 0,
+ 0.7841902375221252, -0.4158804416656494, 0.4605313837528229, 0, 1, 1, 1, 1
+ ]), "The Z rotation transformation wasn't applied correctly.");
+
+ renderer.scale(2, 2, 2);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1.0322768688201904, 1.6982847452163696, -0.22412817180156708, 0,
+ -0.6889029145240784, 0.6511549949645996, 1.7610820531845093, 0,
+ 1.5683804750442505, -0.8317608833312988, 0.9210627675056458, 0, 1, 1, 1, 1
+ ]), "The Z rotation transformation wasn't applied correctly.");
+
+ renderer.transform(mat4.create());
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1.0322768688201904, 1.6982847452163696, -0.22412817180156708, 0,
+ -0.6889029145240784, 0.6511549949645996, 1.7610820531845093, 0,
+ 1.5683804750442505, -0.8317608833312988, 0.9210627675056458, 0, 1, 1, 1, 1
+ ]), "The identity matrix transformation wasn't applied correctly.");
+
+ renderer.origin(1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "The origin wasn't reset to identity correctly.");
+
+ renderer.translate(1, 2);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 0, 1
+ ]), "The second translation transformation wasn't applied correctly.");
+
+ renderer.scale(3, 4);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 1, 2, 0, 1
+ ]), "The second scale transformation wasn't applied correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl05.js b/browser/devtools/tilt/test/browser_tilt_gl05.js
new file mode 100644
index 000000000..ffc4d1bb6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl05.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl05 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let mesh = {
+ vertices: new renderer.VertexBuffer([1, 2, 3], 3),
+ indices: new renderer.IndexBuffer([1]),
+ };
+
+ ok(mesh.vertices instanceof TiltGL.VertexBuffer,
+ "The mesh vertices weren't saved at initialization.");
+ ok(mesh.indices instanceof TiltGL.IndexBuffer,
+ "The mesh indices weren't saved at initialization.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl06.js b/browser/devtools/tilt/test/browser_tilt_gl06.js
new file mode 100644
index 000000000..0f900d1b1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl06.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl06 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let vb = new renderer.VertexBuffer([1, 2, 3, 4, 5, 6], 3);
+
+ ok(vb instanceof TiltGL.VertexBuffer,
+ "The vertex buffer object wasn't instantiated correctly.");
+ ok(vb._ref,
+ "The vertex buffer gl element wasn't created at initialization.");
+ ok(vb.components,
+ "The vertex buffer components weren't created at initialization.");
+ is(vb.itemSize, 3,
+ "The vertex buffer item size isn't set correctly.");
+ is(vb.numItems, 2,
+ "The vertex buffer number of items weren't calculated correctly.");
+
+
+ let ib = new renderer.IndexBuffer([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+
+ ok(ib instanceof TiltGL.IndexBuffer,
+ "The index buffer object wasn't instantiated correctly.");
+ ok(ib._ref,
+ "The index buffer gl element wasn't created at initialization.");
+ ok(ib.components,
+ "The index buffer components weren't created at initialization.");
+ is(ib.itemSize, 1,
+ "The index buffer item size isn't set correctly.");
+ is(ib.numItems, 10,
+ "The index buffer number of items weren't calculated correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl07.js b/browser/devtools/tilt/test/browser_tilt_gl07.js
new file mode 100644
index 000000000..671a4fa2c
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl07.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl07 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let p = new renderer.Program({
+ vs: TiltGL.ColorShader.vs,
+ fs: TiltGL.ColorShader.fs,
+ attributes: ["vertexPosition"],
+ uniforms: ["mvMatrix", "projMatrix", "fill"]
+ });
+
+ ok(p instanceof TiltGL.Program,
+ "The program object wasn't instantiated correctly.");
+
+ ok(p._ref,
+ "The program WebGL object wasn't created properly.");
+ isnot(p._id, -1,
+ "The program id wasn't set properly.");
+ ok(p._attributes,
+ "The program attributes cache wasn't created properly.");
+ ok(p._uniforms,
+ "The program uniforms cache wasn't created properly.");
+
+ is(typeof p._attributes.vertexPosition, "number",
+ "The vertexPosition attribute wasn't cached as it should.");
+ is(typeof p._uniforms.mvMatrix, "object",
+ "The mvMatrix uniform wasn't cached as it should.");
+ is(typeof p._uniforms.projMatrix, "object",
+ "The projMatrix uniform wasn't cached as it should.");
+ is(typeof p._uniforms.fill, "object",
+ "The fill uniform wasn't cached as it should.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl08.js b/browser/devtools/tilt/test/browser_tilt_gl08.js
new file mode 100644
index 000000000..10fff4932
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl08.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl08 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let t = new renderer.Texture({
+ source: canvas,
+ format: "RGB"
+ });
+
+ ok(t instanceof TiltGL.Texture,
+ "The texture object wasn't instantiated correctly.");
+
+ ok(t._ref,
+ "The texture WebGL object wasn't created properly.");
+ isnot(t._id, -1,
+ "The texture id wasn't set properly.");
+ isnot(t.width, -1,
+ "The texture width wasn't set properly.");
+ isnot(t.height, -1,
+ "The texture height wasn't set properly.");
+ ok(t.loaded,
+ "The texture loaded flag wasn't set to true as it should.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math01.js b/browser/devtools/tilt/test/browser_tilt_math01.js
new file mode 100644
index 000000000..da9e23285
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math01.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ ok(isApprox(TiltMath.radians(30), 0.523598775),
+ "The radians() function didn't calculate the value correctly.");
+
+ ok(isApprox(TiltMath.degrees(0.5), 28.64788975),
+ "The degrees() function didn't calculate the value correctly.");
+
+ ok(isApprox(TiltMath.map(0.5, 0, 1, 0, 100), 50),
+ "The map() function didn't calculate the value correctly.");
+
+ is(TiltMath.isPowerOfTwo(32), true,
+ "The isPowerOfTwo() function didn't return the expected value.");
+
+ is(TiltMath.isPowerOfTwo(33), false,
+ "The isPowerOfTwo() function didn't return the expected value.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(31), 32),
+ "The nextPowerOfTwo() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(32), 32),
+ "The nextPowerOfTwo() function didn't calculate the 2nd value correctly.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(33), 64),
+ "The nextPowerOfTwo() function didn't calculate the 3rd value correctly.");
+
+ ok(isApprox(TiltMath.clamp(5, 1, 3), 3),
+ "The clamp() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.clamp(5, 3, 1), 3),
+ "The clamp() function didn't calculate the 2nd value correctly.");
+
+ ok(isApprox(TiltMath.saturate(5), 1),
+ "The saturate() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.saturate(-5), 0),
+ "The saturate() function didn't calculate the 2nd value correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#f00"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 1st rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#f008"), [1, 0, 0, 0.53]),
+ "The hex2rgba() function didn't calculate the 2nd rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#ff0000"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 3rd rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#ff0000aa"), [1, 0, 0, 0.66]),
+ "The hex2rgba() function didn't calculate the 4th rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("rgba(255, 0, 0, 0.5)"), [1, 0, 0, 0.5]),
+ "The hex2rgba() function didn't calculate the 5th rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("rgb(255, 0, 0)"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 6th rgba values correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math02.js b/browser/devtools/tilt/test/browser_tilt_math02.js
new file mode 100644
index 000000000..dae2708c4
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math02.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let v1 = vec3.create();
+
+ ok(v1, "Should have created a vector with vec3.create().");
+ is(v1.length, 3, "A vec3 should have 3 elements.");
+
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "When created, a vec3 should have the values default to 0.");
+
+ vec3.set([1, 2, 3], v1);
+ ok(isApproxVec(v1, [1, 2, 3]),
+ "The vec3.set() function didn't set the values correctly.");
+
+ vec3.zero(v1);
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "The vec3.zero() function didn't set the values correctly.");
+
+ let v2 = vec3.create([4, 5, 6]);
+ ok(isApproxVec(v2, [4, 5, 6]),
+ "When cloning arrays, a vec3 should have the values copied.");
+
+ let v3 = vec3.create(v2);
+ ok(isApproxVec(v3, [4, 5, 6]),
+ "When cloning vectors, a vec3 should have the values copied.");
+
+ vec3.add(v2, v3);
+ ok(isApproxVec(v2, [8, 10, 12]),
+ "The vec3.add() function didn't set the x value correctly.");
+
+ vec3.subtract(v2, v3);
+ ok(isApproxVec(v2, [4, 5, 6]),
+ "The vec3.subtract() function didn't set the values correctly.");
+
+ vec3.negate(v2);
+ ok(isApproxVec(v2, [-4, -5, -6]),
+ "The vec3.negate() function didn't set the values correctly.");
+
+ vec3.scale(v2, -2);
+ ok(isApproxVec(v2, [8, 10, 12]),
+ "The vec3.scale() function didn't set the values correctly.");
+
+ vec3.normalize(v1);
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "Normalizing a vector with zero length should return [0, 0, 0].");
+
+ vec3.normalize(v2);
+ ok(isApproxVec(v2, [
+ 0.4558423161506653, 0.5698028802871704, 0.6837634444236755
+ ]), "The vec3.normalize() function didn't set the values correctly.");
+
+ vec3.cross(v2, v3);
+ ok(isApproxVec(v2, [
+ 5.960464477539063e-8, -1.1920928955078125e-7, 5.960464477539063e-8
+ ]), "The vec3.cross() function didn't set the values correctly.");
+
+ vec3.dot(v2, v3);
+ ok(isApproxVec(v2, [
+ 5.960464477539063e-8, -1.1920928955078125e-7, 5.960464477539063e-8
+ ]), "The vec3.dot() function didn't set the values correctly.");
+
+ ok(isApproxVec([vec3.length(v2)], [1.4600096599955427e-7]),
+ "The vec3.length() function didn't calculate the value correctly.");
+
+ vec3.direction(v2, v3);
+ ok(isApproxVec(v2, [
+ -0.4558422863483429, -0.5698028802871704, -0.6837634444236755
+ ]), "The vec3.direction() function didn't set the values correctly.");
+
+ vec3.lerp(v2, v3, 0.5);
+ ok(isApproxVec(v2, [
+ 1.7720788717269897, 2.2150986194610596, 2.65811824798584
+ ]), "The vec3.lerp() function didn't set the values correctly.");
+
+
+ vec3.project([100, 100, 10], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100), v1);
+ ok(isApproxVec(v1, [-1157.10693359375, 1257.10693359375, 0]),
+ "The vec3.project() function didn't set the values correctly.");
+
+ vec3.unproject([100, 100, 1], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100), v1);
+ ok(isApproxVec(v1, [
+ 41.420406341552734, -41.420406341552734, -99.99771118164062
+ ]), "The vec3.project() function didn't set the values correctly.");
+
+
+ let ray = vec3.createRay([10, 10, 0], [100, 100, 1], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100));
+
+ ok(isApproxVec(ray.origin, [
+ -0.03313708305358887, 0.03313708305358887, -0.1000000014901161
+ ]), "The vec3.createRay() function didn't create the position correctly.");
+ ok(isApproxVec(ray.direction, [
+ 0.35788586614428364, -0.35788586614428364, -0.862458934459091
+ ]), "The vec3.createRay() function didn't create the direction correctly.");
+
+
+ is(vec3.str([0, 0, 0]), "[0, 0, 0]",
+ "The vec3.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math03.js b/browser/devtools/tilt/test/browser_tilt_math03.js
new file mode 100644
index 000000000..9a039ae77
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math03.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat3.create();
+
+ ok(m1, "Should have created a matrix with mat3.create().");
+ is(m1.length, 9, "A mat3 should have 9 elements.");
+
+ ok(isApproxVec(m1, [1, 0, 0, 0, 1, 0, 0, 0, 1]),
+ "When created, a mat3 should have the values default to identity.");
+
+ mat3.set([1, 2, 3, 4, 5, 6, 7, 8, 9], m1);
+ ok(isApproxVec(m1, [1, 2, 3, 4, 5, 6, 7, 8, 9]),
+ "The mat3.set() function didn't set the values correctly.");
+
+ mat3.transpose(m1);
+ ok(isApproxVec(m1, [1, 4, 7, 2, 5, 8, 3, 6, 9]),
+ "The mat3.transpose() function didn't set the values correctly.");
+
+ mat3.identity(m1);
+ ok(isApproxVec(m1, [1, 0, 0, 0, 1, 0, 0, 0, 1]),
+ "The mat3.identity() function didn't set the values correctly.");
+
+ let m2 = mat3.toMat4(m1);
+ ok(isApproxVec(m2, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "The mat3.toMat4() function didn't set the values correctly.");
+
+
+ is(mat3.str([1, 2, 3, 4, 5, 6, 7, 8, 9]), "[1, 2, 3, 4, 5, 6, 7, 8, 9]",
+ "The mat3.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math04.js b/browser/devtools/tilt/test/browser_tilt_math04.js
new file mode 100644
index 000000000..587dc45fd
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math04.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat4.create();
+
+ ok(m1, "Should have created a matrix with mat4.create().");
+ is(m1.length, 16, "A mat4 should have 16 elements.");
+
+ ok(isApproxVec(m1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "When created, a mat4 should have the values default to identity.");
+
+ mat4.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], m1);
+ ok(isApproxVec(m1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
+ "The mat4.set() function didn't set the values correctly.");
+
+ mat4.transpose(m1);
+ ok(isApproxVec(m1, [1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]),
+ "The mat4.transpose() function didn't set the values correctly.");
+
+ mat4.identity(m1);
+ ok(isApproxVec(m1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "The mat4.identity() function didn't set the values correctly.");
+
+ ok(isApprox(mat4.determinant(m1), 1),
+ "The mat4.determinant() function didn't calculate the value correctly.");
+
+ let m2 = mat4.inverse([1, 3, 1, 1, 1, 1, 2, 1, 2, 3, 4, 1, 1, 1, 1, 1]);
+ ok(isApproxVec(m2, [
+ -1, -3, 1, 3, 0.5, 0, 0, -0.5, 0, 1, 0, -1, 0.5, 2, -1, -0.5
+ ]), "The mat4.inverse() function didn't calculate the values correctly.");
+
+ let m3 = mat4.toRotationMat([
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+ ok(isApproxVec(m3, [
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 0, 0, 0, 1
+ ]), "The mat4.toRotationMat() func. didn't calculate the values correctly.");
+
+ let m4 = mat4.toMat3([
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+ ok(isApproxVec(m4, [1, 5, 9, 2, 6, 10, 3, 7, 11]),
+ "The mat4.toMat3() function didn't set the values correctly.");
+
+ let m5 = mat4.toInverseMat3([
+ 1, 3, 1, 1, 1, 1, 2, 1, 2, 3, 4, 1, 1, 1, 1, 1]);
+ ok(isApproxVec(m5, [2, 9, -5, 0, -2, 1, -1, -3, 2]),
+ "The mat4.toInverseMat3() function didn't set the values correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math05.js b/browser/devtools/tilt/test/browser_tilt_math05.js
new file mode 100644
index 000000000..d39695f55
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math05.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat4.create([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+
+ let m2 = mat4.create([
+ 0, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+
+ mat4.multiply(m1, m2);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 304, 336, 368, 400,
+ 332, 368, 404, 440, 360, 400, 440, 480
+ ]), "The mat4.multiply() function didn't set the values correctly.");
+
+ let v1 = mat4.multiplyVec3(m1, [1, 2, 3]);
+ ok(isApproxVec(v1, [2239, 2478, 2717]),
+ "The mat4.multiplyVec3() function didn't set the values correctly.");
+
+ let v2 = mat4.multiplyVec4(m1, [1, 2, 3, 0]);
+ ok(isApproxVec(v2, [1879, 2078, 2277, 2476]),
+ "The mat4.multiplyVec4() function didn't set the values correctly.");
+
+ mat4.translate(m1, [1, 2, 3]);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 304, 336, 368, 400,
+ 332, 368, 404, 440, 2239, 2478, 2717, 2956
+ ]), "The mat4.translate() function didn't set the values correctly.");
+
+ mat4.scale(m1, [1, 2, 3]);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 608, 672, 736, 800,
+ 996, 1104, 1212, 1320, 2239, 2478, 2717, 2956
+ ]), "The mat4.scale() function didn't set the values correctly.");
+
+ mat4.rotate(m1, 0.5, [1, 1, 1]);
+ ok(isApproxVec(m1, [
+ 210.6123046875, 230.2483367919922, 249.88438415527344, 269.5204162597656,
+ 809.8145751953125, 896.520751953125, 983.2268676757812,
+ 1069.9329833984375, 858.5731201171875, 951.23095703125,
+ 1043.8887939453125, 1136.5465087890625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotate() function didn't set the values correctly.");
+
+ mat4.rotateX(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 210.6123046875, 230.2483367919922, 249.88438415527344, 269.5204162597656,
+ 1122.301025390625, 1242.8154296875, 1363.3297119140625,
+ 1483.843994140625, 365.2230224609375, 404.96875, 444.71453857421875,
+ 484.460205078125, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateX() function didn't set the values correctly.");
+
+ mat4.rotateY(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 9.732441902160645, 7.909564018249512, 6.086670875549316,
+ 4.263822555541992, 1122.301025390625, 1242.8154296875, 1363.3297119140625,
+ 1483.843994140625, 421.48626708984375, 465.78045654296875,
+ 510.0746765136719, 554.3687744140625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateY() function didn't set the values correctly.");
+
+ mat4.rotateZ(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 546.6007690429688, 602.7787475585938, 658.9566650390625, 715.1345825195312,
+ 980.245849609375, 1086.881103515625, 1193.5162353515625,
+ 1300.1514892578125, 421.48626708984375, 465.78045654296875,
+ 510.0746765136719, 554.3687744140625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateZ() function didn't set the values correctly.");
+
+
+ let m3 = mat4.frustum(0, 100, 200, 0, 0.1, 100);
+ ok(isApproxVec(m3, [
+ 0.0020000000949949026, 0, 0, 0, 0, -0.0010000000474974513, 0, 0, 1, -1,
+ -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "The mat4.frustum() function didn't compute the values correctly.");
+
+ let m4 = mat4.perspective(45, 1.6, 0.1, 100);
+ ok(isApproxVec(m4, [1.5088834762573242, 0, 0, 0, 0, 2.4142136573791504, 0,
+ 0, 0, 0, -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "The mat4.frustum() function didn't compute the values correctly.");
+
+ let m5 = mat4.ortho(0, 100, 200, 0, -1, 1);
+ ok(isApproxVec(m5, [
+ 0.019999999552965164, 0, 0, 0, 0, -0.009999999776482582, 0, 0,
+ 0, 0, -1, 0, -1, 1, 0, 1
+ ]), "The mat4.ortho() function didn't compute the values correctly.");
+
+ let m6 = mat4.lookAt([1, 2, 3], [4, 5, 6], [0, 1, 0]);
+ ok(isApproxVec(m6, [
+ -0.7071067690849304, -0.40824830532073975, -0.5773502588272095, 0, 0,
+ 0.8164966106414795, -0.5773502588272095, 0, 0.7071067690849304,
+ -0.40824830532073975, -0.5773502588272095, 0, -1.4142135381698608, 0,
+ 3.464101552963257, 1
+ ]), "The mat4.lookAt() function didn't compute the values correctly.");
+
+
+ is(mat4.str([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
+ "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]",
+ "The mat4.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math06.js b/browser/devtools/tilt/test/browser_tilt_math06.js
new file mode 100644
index 000000000..2ed331eaa
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math06.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let q1 = quat4.create();
+
+ ok(q1, "Should have created a quaternion with quat4.create().");
+ is(q1.length, 4, "A quat4 should have 4 elements.");
+
+ ok(isApproxVec(q1, [0, 0, 0, 1]),
+ "When created, a vec3 should have the values default to identity.");
+
+ quat4.set([1, 2, 3, 4], q1);
+ ok(isApproxVec(q1, [1, 2, 3, 4]),
+ "The quat4.set() function didn't set the values correctly.");
+
+ quat4.identity(q1);
+ ok(isApproxVec(q1, [0, 0, 0, 1]),
+ "The quat4.identity() function didn't set the values correctly.");
+
+ quat4.set([5, 6, 7, 8], q1);
+ ok(isApproxVec(q1, [5, 6, 7, 8]),
+ "The quat4.set() function didn't set the values correctly.");
+
+ quat4.calculateW(q1);
+ ok(isApproxVec(q1, [5, 6, 7, -10.440306663513184]),
+ "The quat4.calculateW() function didn't compute the values correctly.");
+
+ quat4.inverse(q1);
+ ok(isApproxVec(q1, [-5, -6, -7, -10.440306663513184]),
+ "The quat4.inverse() function didn't compute the values correctly.");
+
+ quat4.normalize(q1);
+ ok(isApproxVec(q1, [
+ -0.33786869049072266, -0.40544241666793823,
+ -0.4730161726474762, -0.7054905295372009
+ ]), "The quat4.normalize() function didn't compute the values correctly.");
+
+ ok(isApprox(quat4.length(q1), 1),
+ "The mat4.length() function didn't calculate the value correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math07.js b/browser/devtools/tilt/test/browser_tilt_math07.js
new file mode 100644
index 000000000..309d3763d
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math07.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let q1 = quat4.create([1, 2, 3, 4]);
+ let q2 = quat4.create([5, 6, 7, 8]);
+
+ quat4.multiply(q1, q2);
+ ok(isApproxVec(q1, [24, 48, 48, -6]),
+ "The quat4.multiply() function didn't set the values correctly.");
+
+ let v1 = quat4.multiplyVec3(q1, [9, 9, 9]);
+ ok(isApproxVec(v1, [5508, 54756, 59940]),
+ "The quat4.multiplyVec3() function didn't set the values correctly.");
+
+ let m1 = quat4.toMat3(q1);
+ ok(isApproxVec(m1, [
+ -9215, 2880, 1728, 1728, -5759, 4896, 2880, 4320, -5759
+ ]), "The quat4.toMat3() function didn't set the values correctly.");
+
+ let m2 = quat4.toMat4(q1);
+ ok(isApproxVec(m2, [
+ -9215, 2880, 1728, 0, 1728, -5759, 4896, 0,
+ 2880, 4320, -5759, 0, 0, 0, 0, 1
+ ]), "The quat4.toMat4() function didn't set the values correctly.");
+
+ quat4.calculateW(q1);
+ quat4.calculateW(q2);
+ quat4.slerp(q1, q2, 0.5);
+ ok(isApproxVec(q1, [24, 48, 48, -71.99305725097656]),
+ "The quat4.slerp() function didn't set the values correctly.");
+
+ let q3 = quat4.fromAxis([1, 1, 1], 0.5);
+ ok(isApproxVec(q3, [
+ 0.24740396440029144, 0.24740396440029144, 0.24740396440029144,
+ 0.9689124226570129
+ ]), "The quat4.fromAxis() function didn't compute the values correctly.");
+
+ let q4 = quat4.fromEuler(0.5, 0.75, 1.25);
+ ok(isApproxVec(q4, [
+ 0.15310347080230713, 0.39433568716049194,
+ 0.4540249705314636, 0.7841683626174927
+ ]), "The quat4.fromEuler() function didn't compute the values correctly.");
+
+
+ is(quat4.str([1, 2, 3, 4]), "[1, 2, 3, 4]",
+ "The quat4.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking.js b/browser/devtools/tilt/test/browser_tilt_picking.js
new file mode 100644
index 000000000..c79056a3b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let pickDone = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping picking test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping picking test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ let presenter = instance.presenter;
+ let canvas = presenter.canvas;
+
+ presenter._onSetupMesh = function() {
+ let p = getPickablePoint(presenter);
+
+ presenter.pickNode(p[0], p[1], {
+ onpick: function(data)
+ {
+ ok(data.index > 0,
+ "Simply picking a node didn't work properly.");
+
+ pickDone = true;
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+ });
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ if (pickDone) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_delete.js b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
new file mode 100644
index 000000000..c45d44b03
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeDeleted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping picking delete test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping picking delete test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenNodeRemoved, NODE_REMOVED, false);
+
+ presenter._onSetupMesh = function() {
+ let p = getPickablePoint(presenter);
+
+ presenter.highlightNodeAt(p[0], p[1], {
+ onpick: function()
+ {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ nodeDeleted = true;
+ presenter.deleteNode();
+ }
+ });
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenNodeRemoved() {
+ ok(presenter._currentSelection > 0,
+ "Deleting a node shouldn't change the current selection.");
+ ok(presenter._highlight.disabled,
+ "After deleting a node, it shouldn't be highlighted.");
+
+ let nodeIndex = presenter._currentSelection;
+ let vertices = presenter._meshStacks[0].vertices.components;
+
+ for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+ is(vertices[i + k], 0,
+ "The stack vertices weren't degenerated properly.");
+ }
+
+ executeSoon(function() {
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeDeleted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
new file mode 100644
index 000000000..abfd4f586
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("far-far-away");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div, "moveIntoView");
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible should trigger a reset!");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
new file mode 100644
index 000000000..82871270e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("first-law");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div, "moveIntoView");
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's already visible shouldn't trigger a reset.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
new file mode 100644
index 000000000..fc8d0fc51
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ nodeHighlighted = true;
+ presenter.highlightNodeAt.apply(this, getPickablePoint(presenter));
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
new file mode 100644
index 000000000..721189f65
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ nodeHighlighted = true;
+ presenter.highlightNodeFor(3); // 1 = html, 2 = body, 3 = first div
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNodeFor(-1);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_inspector.js b/browser/devtools/tilt/test/browser_tilt_picking_inspector.js
new file mode 100644
index 000000000..0ec302a07
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_inspector.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let contentDocument = toolbox.target.tab.linkedBrowser.contentDocument;
+ let div = contentDocument.getElementById("first-law");
+ toolbox.getCurrentPanel().selection.setNode(div);
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ whenOpen();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ });
+}
+
+function whenOpen() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's already visible shouldn't trigger a reset.");
+
+ executeSoon(function() {
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ Services.obs.removeObserver(cleanup, DESTROYED);
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_miv.js b/browser/devtools/tilt/test/browser_tilt_picking_miv.js
new file mode 100644
index 000000000..64b911a00
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_miv.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("far-far-away");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div);
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible shouldn't trigger a reset " +
+ "without this being explicitly requested!");
+
+ EventUtils.sendKey("F");
+ executeSoon(whenBringingIntoView);
+}
+
+function whenBringingIntoView() {
+ ok(presenter._currentSelection > 0,
+ "The node should still be selected.");
+ ok(!presenter._highlight.disabled,
+ "The node should still be highlighted");
+ ok(presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible should trigger a reset " +
+ "when this is being explicitly requested!");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils01.js b/browser/devtools/tilt/test/browser_tilt_utils01.js
new file mode 100644
index 000000000..7beb6a3a2
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils01.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let prefs = TiltUtils.Preferences;
+ ok(prefs, "The TiltUtils.Preferences wasn't found.");
+
+ prefs.create("test-pref-bool", "boolean", true);
+ prefs.create("test-pref-int", "integer", 42);
+ prefs.create("test-pref-string", "string", "hello world!");
+
+ is(prefs.get("test-pref-bool", "boolean"), true,
+ "The boolean test preference wasn't initially set correctly.");
+ is(prefs.get("test-pref-int", "integer"), 42,
+ "The integer test preference wasn't initially set correctly.");
+ is(prefs.get("test-pref-string", "string"), "hello world!",
+ "The string test preference wasn't initially set correctly.");
+
+
+ prefs.set("test-pref-bool", "boolean", false);
+ prefs.set("test-pref-int", "integer", 24);
+ prefs.set("test-pref-string", "string", "!dlrow olleh");
+
+ is(prefs.get("test-pref-bool", "boolean"), false,
+ "The boolean test preference wasn't changed correctly.");
+ is(prefs.get("test-pref-int", "integer"), 24,
+ "The integer test preference wasn't changed correctly.");
+ is(prefs.get("test-pref-string", "string"), "!dlrow olleh",
+ "The string test preference wasn't changed correctly.");
+
+
+ is(typeof prefs.get("unknown-pref", "boolean"), "undefined",
+ "Inexisted boolean prefs should be handled as undefined.");
+ is(typeof prefs.get("unknown-pref", "integer"), "undefined",
+ "Inexisted integer prefs should be handled as undefined.");
+ is(typeof prefs.get("unknown-pref", "string"), "undefined",
+ "Inexisted string prefs should be handled as undefined.");
+
+
+ is(prefs.get("test-pref-bool", "integer"), null,
+ "The get() boolean function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-bool", "string"), null,
+ "The get() boolean function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-int", "boolean"), null,
+ "The get() integer function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-int", "string"), null,
+ "The get() integer function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-string", "boolean"), null,
+ "The get() string function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-string", "integer"), null,
+ "The get() string function didn't handle incorrect types as it should.");
+
+
+ is(typeof prefs.get(), "undefined",
+ "The get() function should not work if not enough params are passed.");
+ is(typeof prefs.set(), "undefined",
+ "The set() function should not work if not enough params are passed.");
+ is(typeof prefs.create(), "undefined",
+ "The create() function should not work if not enough params are passed.");
+
+
+ is(prefs.get("test-pref-wrong-type", "wrong-type", 1), null,
+ "The get() function should expect only correct pref types.");
+ is(prefs.set("test-pref-wrong-type", "wrong-type", 1), false,
+ "The set() function should expect only correct pref types.");
+ is(prefs.create("test-pref-wrong-type", "wrong-type", 1), false,
+ "The create() function should expect only correct pref types.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils02.js b/browser/devtools/tilt/test/browser_tilt_utils02.js
new file mode 100644
index 000000000..fcee265c6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils02.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let l10 = TiltUtils.L10n;
+ ok(l10, "The TiltUtils.L10n wasn't found.");
+
+
+ ok(l10.stringBundle,
+ "The necessary string bundle wasn't found.");
+ is(l10.get(), null,
+ "The get() function shouldn't work if no params are passed.");
+ is(l10.format(), null,
+ "The format() function shouldn't work if no params are passed.");
+
+ is(typeof l10.get("initWebGL.error"), "string",
+ "No valid string was returned from a corect name in the bundle.");
+ is(typeof l10.format("linkProgram.error", ["error"]), "string",
+ "No valid formatted string was returned from a name in the bundle.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils03.js b/browser/devtools/tilt/test/browser_tilt_utils03.js
new file mode 100644
index 000000000..61d256fe1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils03.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let dom = TiltUtils.DOM;
+
+ is(dom.parentNode, null,
+ "The parent node should not be initially set.");
+
+ dom.parentNode = {};
+ ok(dom.parentNode,
+ "The parent node should now be set.");
+
+ TiltUtils.clearCache();
+ is(dom.parentNode, null,
+ "The parent node should not be set after clearing the cache.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils04.js b/browser/devtools/tilt/test/browser_tilt_utils04.js
new file mode 100644
index 000000000..8574c266e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils04.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+
+ is(dom.initCanvas(), null,
+ "The initCanvas() function shouldn't work if no parent node is set.");
+
+
+ dom.parentNode = gBrowser.parentNode;
+ ok(dom.parentNode,
+ "The necessary parent node wasn't found.");
+
+
+ let canvas = dom.initCanvas(null, {
+ append: true,
+ focusable: true,
+ width: 123,
+ height: 456,
+ id: "tilt-test-canvas"
+ });
+
+ is(canvas.width, 123,
+ "The test canvas doesn't have the correct width set.");
+ is(canvas.height, 456,
+ "The test canvas doesn't have the correct height set.");
+ is(canvas.getAttribute("tabindex"), 1,
+ "The test canvas tab index wasn't set correctly.");
+ is(canvas.getAttribute("id"), "tilt-test-canvas",
+ "The test canvas doesn't have the correct id set.");
+ ok(dom.parentNode.ownerDocument.getElementById(canvas.id),
+ "A canvas should be appended to the parent node if specified.");
+ canvas.parentNode.removeChild(canvas);
+
+ let canvas2 = dom.initCanvas(null, { id: "tilt-test-canvas2" });
+
+ is(canvas2.width, dom.parentNode.clientWidth,
+ "The second test canvas doesn't have the implicit width set.");
+ is(canvas2.height, dom.parentNode.clientHeight,
+ "The second test canvas doesn't have the implicit height set.");
+ is(canvas2.id, "tilt-test-canvas2",
+ "The second test canvas doesn't have the correct id set.");
+ is(dom.parentNode.ownerDocument.getElementById(canvas2.id), null,
+ "A canvas shouldn't be appended to the parent node if not specified.");
+
+
+ dom.parentNode = null;
+ is(dom.parentNode, null,
+ "The necessary parent node shouldn't be found anymore.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils05.js b/browser/devtools/tilt/test/browser_tilt_utils05.js
new file mode 100644
index 000000000..0f09d198e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<head>",
+ "<style>",
+ "</style>",
+ "<script>",
+ "</script>",
+ "</head>",
+ "<body style='margin: 0;'>",
+ "<div style='margin-top: 98px;" +
+ "margin-left: 76px;" +
+ "width: 123px;" +
+ "height: 456px;' id='test-div'>",
+ "<span></span>",
+ "</div>",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let cwDimensions = dom.getContentWindowDimensions(iframe.contentWindow);
+
+ is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
+ iframe.contentWindow.innerWidth,
+ "The content window width wasn't calculated correctly.");
+ is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
+ iframe.contentWindow.innerHeight,
+ "The content window height wasn't calculated correctly.");
+
+ let nodeCoordinates = LayoutHelpers.getRect(
+ iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
+
+ let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+ let frameRect = iframe.getBoundingClientRect();
+
+ is(nodeCoordinates.top, frameRect.top + frameOffset[0] + 98,
+ "The node coordinates top value wasn't calculated correctly.");
+ is(nodeCoordinates.left, frameRect.left + frameOffset[1] + 76,
+ "The node coordinates left value wasn't calculated correctly.");
+ is(nodeCoordinates.width, 123,
+ "The node coordinates width value wasn't calculated correctly.");
+ is(nodeCoordinates.height, 456,
+ "The node coordinates height value wasn't calculated correctly.");
+
+
+ let store = dom.traverse(iframe.contentWindow);
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "style", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "script", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils06.js b/browser/devtools/tilt/test/browser_tilt_utils06.js
new file mode 100644
index 000000000..eee915261
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils06.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let someObject = {
+ a: 1,
+ func: function()
+ {
+ this.b = 2;
+ }
+};
+
+let anotherObject = {
+ _finalize: function()
+ {
+ someObject.c = 3;
+ }
+};
+
+function test() {
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ TiltUtils.bindObjectFunc(someObject, "", anotherObject);
+ someObject.func();
+
+ is(someObject.a, 1,
+ "The bindObjectFunc() messed the non-function members of the object.");
+ isnot(someObject.b, 2,
+ "The bindObjectFunc() didn't ignore the old scope correctly.");
+ is(anotherObject.b, 2,
+ "The bindObjectFunc() didn't set the new scope correctly.");
+
+
+ TiltUtils.destroyObject(anotherObject);
+ is(someObject.c, 3,
+ "The finalize function wasn't called when an object was destroyed.");
+
+
+ TiltUtils.destroyObject(someObject);
+ is(typeof someObject.a, "undefined",
+ "Not all members of the destroyed object were deleted.");
+ is(typeof someObject.func, "undefined",
+ "Not all function members of the destroyed object were deleted.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils07.js b/browser/devtools/tilt/test/browser_tilt_utils07.js
new file mode 100644
index 000000000..0c07300a8
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils07.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<frameset cols='50%,50%'>",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<div id='test-div' style='width: 123px; height: 456px;'></div>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "</frameset>",
+ "<iframe src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "'></iframe>",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<iframe src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body>",
+ "<div></div>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "'></iframe>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let cwDimensions = dom.getContentWindowDimensions(iframe.contentWindow);
+
+ is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
+ iframe.contentWindow.innerWidth,
+ "The content window width wasn't calculated correctly.");
+ is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
+ iframe.contentWindow.innerHeight,
+ "The content window height wasn't calculated correctly.");
+
+ let nodeCoordinates = LayoutHelpers.getRect(
+ iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
+
+ let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+ let frameRect = iframe.getBoundingClientRect();
+
+ is(nodeCoordinates.top, frameRect.top + frameOffset[0],
+ "The node coordinates top value wasn't calculated correctly.");
+ is(nodeCoordinates.left, frameRect.left + frameOffset[1],
+ "The node coordinates left value wasn't calculated correctly.");
+ is(nodeCoordinates.width, 123,
+ "The node coordinates width value wasn't calculated correctly.");
+ is(nodeCoordinates.height, 456,
+ "The node coordinates height value wasn't calculated correctly.");
+
+
+ let store = dom.traverse(iframe.contentWindow);
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "iframe", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "iframe", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "html", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "html", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 5 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 5 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils08.js b/browser/devtools/tilt/test/browser_tilt_utils08.js
new file mode 100644
index 000000000..797c9e7a7
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils08.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<div>",
+ "<p>Foo</p>",
+ "<div>",
+ "<span>Bar</span>",
+ "</div>",
+ "<div></div>",
+ "</div>",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function nodeCallback(aContentWindow, aNode, aParentPosition) {
+ let coord = TiltUtils.DOM.getNodePosition(aContentWindow, aNode, aParentPosition);
+
+ if (aNode.localName != "div")
+ coord.thickness = 0;
+
+ if (aNode.localName == "span")
+ coord.depth += STACK_THICKNESS;
+
+ return coord;
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let store = dom.traverse(iframe.contentWindow, {
+ nodeCallback: nodeCallback
+ });
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "head", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "body", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "div", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "p", depth: 1 * STACK_THICKNESS, thickness: 0 },
+ { name: "div", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 3 * STACK_THICKNESS, thickness: 0 },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_visualizer.js b/browser/devtools/tilt/test/browser_tilt_visualizer.js
new file mode 100644
index 000000000..bc7c2bc18
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping visualizer test because WebGL isn't supported.");
+ return;
+ }
+
+ let webGLError = false;
+ let webGLLoad = false;
+
+ let visualizer = new TiltVisualizer({
+ chromeWindow: window,
+ contentWindow: gBrowser.selectedBrowser.contentWindow,
+ parentNode: gBrowser.selectedBrowser.parentNode,
+ notifications: Tilt.NOTIFICATIONS,
+ tab: gBrowser.selectedTab,
+
+ onError: function onWebGLError()
+ {
+ webGLError = true;
+ },
+
+ onLoad: function onWebGLLoad()
+ {
+ webGLLoad = true;
+ }
+ });
+ visualizer.init();
+
+ ok(webGLError ^ webGLLoad,
+ "The WebGL context should either be created or not.");
+
+ if (webGLError) {
+ info("Skipping visualizer test because WebGL couldn't be initialized.");
+ return;
+ }
+
+ ok(visualizer.canvas,
+ "Visualizer constructor should have created a child canvas object.");
+ ok(visualizer.presenter,
+ "Visualizer constructor should have created a child presenter object.");
+ ok(visualizer.controller,
+ "Visualizer constructor should have created a child controller object.");
+ ok(visualizer.isInitialized(),
+ "The visualizer should have been initialized properly.");
+ ok(visualizer.presenter.isInitialized(),
+ "The visualizer presenter should have been initialized properly.");
+ ok(visualizer.controller.isInitialized(),
+ "The visualizer controller should have been initialized properly.");
+
+ testPresenter(visualizer.presenter);
+ testController(visualizer.controller);
+
+ visualizer.removeOverlay();
+ is(visualizer.canvas.parentNode, null,
+ "The visualizer canvas wasn't removed from the parent node.");
+
+ visualizer.cleanup();
+ is(visualizer.presenter, undefined,
+ "The visualizer presenter wasn't destroyed.");
+ is(visualizer.controller, undefined,
+ "The visualizer controller wasn't destroyed.");
+ is(visualizer.canvas, undefined,
+ "The visualizer canvas wasn't destroyed.");
+}
+
+function testPresenter(presenter) {
+ ok(presenter._renderer,
+ "The presenter renderer wasn't initialized properly.");
+ ok(presenter._visualizationProgram,
+ "The presenter visualizationProgram wasn't initialized properly.");
+ ok(presenter._texture,
+ "The presenter texture wasn't initialized properly.");
+ ok(!presenter._meshStacks,
+ "The presenter meshStacks shouldn't be initialized yet.");
+ ok(!presenter._meshWireframe,
+ "The presenter meshWireframe shouldn't be initialized yet.");
+ ok(presenter._traverseData,
+ "The presenter nodesInformation wasn't initialized properly.");
+ ok(presenter._highlight,
+ "The presenter highlight wasn't initialized properly.");
+ ok(presenter._highlight.disabled,
+ "The presenter highlight should be initially disabled.");
+ ok(isApproxVec(presenter._highlight.v0, [0, 0, 0]),
+ "The presenter highlight first vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v1, [0, 0, 0]),
+ "The presenter highlight second vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v2, [0, 0, 0]),
+ "The presenter highlight third vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v3, [0, 0, 0]),
+ "The presenter highlight fourth vertex should be initially zeroed.");
+ ok(presenter.transforms,
+ "The presenter transforms wasn't initialized properly.");
+ is(presenter.transforms.zoom, 1,
+ "The presenter transforms zoom should be initially 1.");
+ ok(isApproxVec(presenter.transforms.offset, [0, 0, 0]),
+ "The presenter transforms offset should be initially zeroed.");
+ ok(isApproxVec(presenter.transforms.translation, [0, 0, 0]),
+ "The presenter transforms translation should be initially zeroed.");
+ ok(isApproxVec(presenter.transforms.rotation, [0, 0, 0, 1]),
+ "The presenter transforms rotation should be initially set to identity.");
+
+ presenter.setTranslation([1, 2, 3]);
+ presenter.setRotation([5, 6, 7, 8]);
+
+ ok(isApproxVec(presenter.transforms.translation, [1, 2, 3]),
+ "The presenter transforms translation wasn't modified as it should");
+ ok(isApproxVec(presenter.transforms.rotation, [5, 6, 7, 8]),
+ "The presenter transforms rotation wasn't modified as it should");
+ ok(presenter._redraw,
+ "The new transforms should have issued a redraw request.");
+}
+
+function testController(controller) {
+ ok(controller.arcball,
+ "The controller arcball wasn't initialized properly.");
+ ok(!controller.coordinates,
+ "The presenter meshWireframe shouldn't be initialized yet.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_zoom.js b/browser/devtools/tilt/test/browser_tilt_zoom.js
new file mode 100644
index 000000000..af6ac2c91
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_zoom.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ZOOM = 2;
+const RESIZE = 50;
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping controller test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping controller test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ TiltUtils.setDocumentZoom(window, ZOOM);
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ ok(isApprox(instance.presenter._getPageZoom(), ZOOM),
+ "The Highlighter zoom doesn't have the expected results.");
+
+ ok(isApprox(instance.presenter.transforms.zoom, ZOOM),
+ "The presenter transforms zoom wasn't initially set correctly.");
+
+ let contentWindow = gBrowser.selectedBrowser.contentWindow;
+ let initialWidth = contentWindow.innerWidth;
+ let initialHeight = contentWindow.innerHeight;
+
+ let renderer = instance.presenter._renderer;
+ let arcball = instance.controller.arcball;
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
+ "The renderer width wasn't set correctly before the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
+ "The renderer height wasn't set correctly before the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
+ "The arcball width wasn't set correctly before the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
+ "The arcball height wasn't set correctly before the resize.");
+
+
+ window.resizeBy(-RESIZE * ZOOM, -RESIZE * ZOOM);
+
+ executeSoon(function() {
+ ok(isApprox(contentWindow.innerWidth + RESIZE, initialWidth, 1),
+ "The content window width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight + RESIZE, initialHeight, 1),
+ "The content window height wasn't set correctly after the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
+ "The renderer width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
+ "The renderer height wasn't set correctly after the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
+ "The arcball width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
+ "The arcball height wasn't set correctly after the resize.");
+
+
+ window.resizeBy(RESIZE * ZOOM, RESIZE * ZOOM);
+
+
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/head.js b/browser/devtools/tilt/test/head.js
new file mode 100644
index 000000000..25482ead6
--- /dev/null
+++ b/browser/devtools/tilt/test/head.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TiltManager = devtools.require("devtools/tilt/tilt").TiltManager;
+let TiltGL = devtools.require("devtools/tilt/tilt-gl");
+let {EPSILON, TiltMath, vec3, mat3, mat4, quat4} = devtools.require("devtools/tilt/tilt-math");
+let TiltUtils = devtools.require("devtools/tilt/tilt-utils");
+let {TiltVisualizer} = devtools.require("devtools/tilt/tilt-visualizer");
+
+let tempScope = {};
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
+let LayoutHelpers = tempScope.LayoutHelpers;
+
+
+const DEFAULT_HTML = "data:text/html," +
+ "<DOCTYPE html>" +
+ "<html>" +
+ "<head>" +
+ "<meta charset='utf-8'/>" +
+ "<title>Three Laws</title>" +
+ "</head>" +
+ "<body>" +
+ "<div id='first-law'>" +
+ "A robot may not injure a human being or, through inaction, allow a " +
+ "human being to come to harm." +
+ "</div>" +
+ "<div>" +
+ "A robot must obey the orders given to it by human beings, except " +
+ "where such orders would conflict with the First Law." +
+ "</div>" +
+ "<div>" +
+ "A robot must protect its own existence as long as such protection " +
+ "does not conflict with the First or Second Laws." +
+ "</div>" +
+ "<div id='far-far-away' style='position: absolute; top: 250%;'>" +
+ "I like bacon." +
+ "</div>" +
+ "<body>" +
+ "</html>";
+
+let Tilt = TiltManager.getTiltForBrowser(window);
+
+const STARTUP = Tilt.NOTIFICATIONS.STARTUP;
+const INITIALIZING = Tilt.NOTIFICATIONS.INITIALIZING;
+const INITIALIZED = Tilt.NOTIFICATIONS.INITIALIZED;
+const DESTROYING = Tilt.NOTIFICATIONS.DESTROYING;
+const BEFORE_DESTROYED = Tilt.NOTIFICATIONS.BEFORE_DESTROYED;
+const DESTROYED = Tilt.NOTIFICATIONS.DESTROYED;
+const SHOWN = Tilt.NOTIFICATIONS.SHOWN;
+const HIDDEN = Tilt.NOTIFICATIONS.HIDDEN;
+const HIGHLIGHTING = Tilt.NOTIFICATIONS.HIGHLIGHTING;
+const UNHIGHLIGHTING = Tilt.NOTIFICATIONS.UNHIGHLIGHTING;
+const NODE_REMOVED = Tilt.NOTIFICATIONS.NODE_REMOVED;
+
+const TILT_ENABLED = Services.prefs.getBoolPref("devtools.tilt.enabled");
+
+
+function isTiltEnabled() {
+ info("Apparently, Tilt is" + (TILT_ENABLED ? "" : " not") + " enabled.");
+ return TILT_ENABLED;
+}
+
+function isWebGLSupported() {
+ let supported = !TiltGL.isWebGLForceEnabled() &&
+ TiltGL.isWebGLSupported() &&
+ TiltGL.create3DContext(createCanvas());
+
+ info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+ return supported;
+}
+
+function isApprox(num1, num2, delta) {
+ if (Math.abs(num1 - num2) > (delta || EPSILON)) {
+ info("isApprox expected " + num1 + ", got " + num2 + " instead.");
+ return false;
+ }
+ return true;
+}
+
+function isApproxVec(vec1, vec2, delta) {
+ vec1 = Array.prototype.slice.call(vec1);
+ vec2 = Array.prototype.slice.call(vec2);
+
+ if (vec1.length !== vec2.length) {
+ return false;
+ }
+ for (let i = 0, len = vec1.length; i < len; i++) {
+ if (!isApprox(vec1[i], vec2[i], delta)) {
+ info("isApproxVec expected [" + vec1 + "], got [" + vec2 + "] instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function isEqualVec(vec1, vec2) {
+ vec1 = Array.prototype.slice.call(vec1);
+ vec2 = Array.prototype.slice.call(vec2);
+
+ if (vec1.length !== vec2.length) {
+ return false;
+ }
+ for (let i = 0, len = vec1.length; i < len; i++) {
+ if (vec1[i] !== vec2[i]) {
+ info("isEqualVec expected [" + vec1 + "], got [" + vec2 + "] instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function createCanvas() {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+
+function createTab(callback, location) {
+ info("Creating a tab, with callback " + typeof callback +
+ ", and location " + location + ".");
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ callback(tab);
+ }, true);
+
+ gBrowser.selectedBrowser.contentWindow.location = location || DEFAULT_HTML;
+ return tab;
+}
+
+
+function createTilt(callbacks, close, suddenDeath) {
+ info("Creating Tilt, with callbacks {" + Object.keys(callbacks) + "}" +
+ ", autoclose param " + close +
+ ", and sudden death handler " + typeof suddenDeath + ".");
+
+ handleFailure(suddenDeath);
+
+ Services.prefs.setBoolPref("webgl.verbose", true);
+ TiltUtils.Output.suppressAlerts = true;
+
+ info("Attempting to start Tilt.");
+ Services.obs.addObserver(onTiltOpen, INITIALIZING, false);
+ Tilt.toggle();
+
+ function onTiltOpen() {
+ info("Tilt was opened.");
+ Services.obs.removeObserver(onTiltOpen, INITIALIZING);
+
+ executeSoon(function() {
+ if ("function" === typeof callbacks.onTiltOpen) {
+ info("Calling 'onTiltOpen'.");
+ callbacks.onTiltOpen(Tilt.visualizers[Tilt.currentWindowId]);
+ }
+ if (close) {
+ executeSoon(function() {
+ info("Attempting to close Tilt.");
+ Services.obs.addObserver(onTiltClose, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ });
+ }
+
+ function onTiltClose() {
+ info("Tilt was closed.");
+ Services.obs.removeObserver(onTiltClose, DESTROYED);
+
+ executeSoon(function() {
+ if ("function" === typeof callbacks.onTiltClose) {
+ info("Calling 'onTiltClose'.");
+ callbacks.onTiltClose();
+ }
+ if ("function" === typeof callbacks.onEnd) {
+ info("Calling 'onEnd'.");
+ callbacks.onEnd();
+ }
+ });
+ }
+
+ function handleFailure(suddenDeath) {
+ Tilt.failureCallback = function() {
+ info("Tilt FAIL.");
+ Services.obs.removeObserver(onTiltOpen, INITIALIZING);
+
+ info("Now relying on sudden death handler " + typeof suddenDeath + ".");
+ suddenDeath && suddenDeath();
+ }
+ }
+}
+
+function getPickablePoint(presenter) {
+ let vertices = presenter._meshStacks[0].vertices.components;
+
+ let topLeft = vec3.create([vertices[0], vertices[1], vertices[2]]);
+ let bottomRight = vec3.create([vertices[6], vertices[7], vertices[8]]);
+ let center = vec3.lerp(topLeft, bottomRight, 0.5, []);
+
+ let renderer = presenter._renderer;
+ let viewport = [0, 0, renderer.width, renderer.height];
+
+ return vec3.project(center, viewport, renderer.mvMatrix, renderer.projMatrix);
+}
diff --git a/browser/devtools/tilt/test/moz.build b/browser/devtools/tilt/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/tilt/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/tilt/tilt-gl.js b/browser/devtools/tilt/tilt-gl.js
new file mode 100644
index 000000000..0f3367fec
--- /dev/null
+++ b/browser/devtools/tilt/tilt-gl.js
@@ -0,0 +1,1595 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 {Cc, Ci, Cu} = require("chrome");
+
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let {TiltMath, mat4} = require("devtools/tilt/tilt-math");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const WEBGL_CONTEXT_NAME = "experimental-webgl";
+
+
+/**
+ * Module containing thin wrappers around low-level WebGL functions.
+ */
+let TiltGL = {};
+module.exports = TiltGL;
+
+/**
+ * Contains commonly used helper methods used in any 3D application.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas element used for rendering
+ * @param {Function} onError
+ * optional, function called if initialization failed
+ * @param {Function} onLoad
+ * optional, function called if initialization worked
+ */
+TiltGL.Renderer = function TGL_Renderer(aCanvas, onError, onLoad)
+{
+ /**
+ * The WebGL context obtained from the canvas element, used for drawing.
+ */
+ this.context = TiltGL.create3DContext(aCanvas);
+
+ // check if the context was created successfully
+ if (!this.context) {
+ TiltUtils.Output.alert("Firefox", TiltUtils.L10n.get("initTilt.error"));
+ TiltUtils.Output.error(TiltUtils.L10n.get("initWebGL.error"));
+
+ if ("function" === typeof onError) {
+ onError();
+ }
+ return;
+ }
+
+ // set the default clear color and depth buffers
+ this.context.clearColor(0, 0, 0, 0);
+ this.context.clearDepth(1);
+
+ /**
+ * Variables representing the current framebuffer width and height.
+ */
+ this.width = aCanvas.width;
+ this.height = aCanvas.height;
+ this.initialWidth = this.width;
+ this.initialHeight = this.height;
+
+ /**
+ * The current model view matrix.
+ */
+ this.mvMatrix = mat4.identity(mat4.create());
+
+ /**
+ * The current projection matrix.
+ */
+ this.projMatrix = mat4.identity(mat4.create());
+
+ /**
+ * The current fill color applied to any objects which can be filled.
+ * These are rectangles, circles, boxes, 2d or 3d primitives in general.
+ */
+ this._fillColor = [];
+
+ /**
+ * The current stroke color applied to any objects which can be stroked.
+ * This property mostly refers to lines.
+ */
+ this._strokeColor = [];
+
+ /**
+ * Variable representing the current stroke weight.
+ */
+ this._strokeWeightValue = 0;
+
+ /**
+ * A shader useful for drawing vertices with only a color component.
+ */
+ this._colorShader = new TiltGL.Program(this.context, {
+ vs: TiltGL.ColorShader.vs,
+ fs: TiltGL.ColorShader.fs,
+ attributes: ["vertexPosition"],
+ uniforms: ["mvMatrix", "projMatrix", "fill"]
+ });
+
+ // create helper functions to create shaders, meshes, buffers and textures
+ this.Program =
+ TiltGL.Program.bind(TiltGL.Program, this.context);
+ this.VertexBuffer =
+ TiltGL.VertexBuffer.bind(TiltGL.VertexBuffer, this.context);
+ this.IndexBuffer =
+ TiltGL.IndexBuffer.bind(TiltGL.IndexBuffer, this.context);
+ this.Texture =
+ TiltGL.Texture.bind(TiltGL.Texture, this.context);
+
+ // set the default mvp matrices, tint, fill, stroke and other visual props.
+ this.defaults();
+
+ // the renderer was created successfully
+ if ("function" === typeof onLoad) {
+ onLoad();
+ }
+};
+
+TiltGL.Renderer.prototype = {
+
+ /**
+ * Clears the color and depth buffers.
+ */
+ clear: function TGLR_clear()
+ {
+ let gl = this.context;
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+ },
+
+ /**
+ * Sets if depth testing should be enabled or not.
+ * Disabling could be useful when handling transparency (for example).
+ *
+ * @param {Boolean} aEnabledFlag
+ * true if depth testing should be enabled
+ */
+ depthTest: function TGLR_depthTest(aEnabledFlag)
+ {
+ let gl = this.context;
+
+ if (aEnabledFlag) {
+ gl.enable(gl.DEPTH_TEST);
+ } else {
+ gl.disable(gl.DEPTH_TEST);
+ }
+ },
+
+ /**
+ * Sets if stencil testing should be enabled or not.
+ *
+ * @param {Boolean} aEnabledFlag
+ * true if stencil testing should be enabled
+ */
+ stencilTest: function TGLR_stencilTest(aEnabledFlag)
+ {
+ let gl = this.context;
+
+ if (aEnabledFlag) {
+ gl.enable(gl.STENCIL_TEST);
+ } else {
+ gl.disable(gl.STENCIL_TEST);
+ }
+ },
+
+ /**
+ * Sets cull face, either "front", "back" or disabled.
+ *
+ * @param {String} aModeFlag
+ * blending mode, either "front", "back", "both" or falsy
+ */
+ cullFace: function TGLR_cullFace(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "front":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.FRONT);
+ break;
+ case "back":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.BACK);
+ break;
+ case "both":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.FRONT_AND_BACK);
+ break;
+ default:
+ gl.disable(gl.CULL_FACE);
+ }
+ },
+
+ /**
+ * Specifies the orientation of front-facing polygons.
+ *
+ * @param {String} aModeFlag
+ * either "cw" or "ccw"
+ */
+ frontFace: function TGLR_frontFace(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "cw":
+ gl.frontFace(gl.CW);
+ break;
+ case "ccw":
+ gl.frontFace(gl.CCW);
+ break;
+ }
+ },
+
+ /**
+ * Sets blending, either "alpha" or "add" (additive blending).
+ * Anything else disables blending.
+ *
+ * @param {String} aModeFlag
+ * blending mode, either "alpha", "add" or falsy
+ */
+ blendMode: function TGLR_blendMode(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "alpha":
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+ break;
+ case "add":
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
+ break;
+ default:
+ gl.disable(gl.BLEND);
+ }
+ },
+
+ /**
+ * Helper function to activate the color shader.
+ *
+ * @param {TiltGL.VertexBuffer} aVerticesBuffer
+ * a buffer of vertices positions
+ * @param {Array} aColor
+ * the color fill to be used as [r, g, b, a] with 0..1 range
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ */
+ useColorShader: function TGLR_useColorShader(
+ aVerticesBuffer, aColor, aMvMatrix, aProjMatrix)
+ {
+ let program = this._colorShader;
+
+ // use this program
+ program.use();
+
+ // bind the attributes and uniforms as necessary
+ program.bindVertexBuffer("vertexPosition", aVerticesBuffer);
+ program.bindUniformMatrix("mvMatrix", aMvMatrix || this.mvMatrix);
+ program.bindUniformMatrix("projMatrix", aProjMatrix || this.projMatrix);
+ program.bindUniformVec4("fill", aColor || this._fillColor);
+ },
+
+ /**
+ * Draws bound vertex buffers using the specified parameters.
+ *
+ * @param {Number} aDrawMode
+ * WebGL enum, like TRIANGLES
+ * @param {Number} aCount
+ * the number of indices to be rendered
+ */
+ drawVertices: function TGLR_drawVertices(aDrawMode, aCount)
+ {
+ this.context.drawArrays(aDrawMode, 0, aCount);
+ },
+
+ /**
+ * Draws bound vertex buffers using the specified parameters.
+ * This function also makes use of an index buffer.
+ *
+ * @param {Number} aDrawMode
+ * WebGL enum, like TRIANGLES
+ * @param {TiltGL.IndexBuffer} aIndicesBuffer
+ * indices for the vertices buffer
+ */
+ drawIndexedVertices: function TGLR_drawIndexedVertices(
+ aDrawMode, aIndicesBuffer)
+ {
+ let gl = this.context;
+
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, aIndicesBuffer._ref);
+ gl.drawElements(aDrawMode, aIndicesBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+ },
+
+ /**
+ * Sets the current fill color.
+ *
+ * @param {Array} aColor
+ * the color fill to be used as [r, g, b, a] with 0..1 range
+ * @param {Number} aMultiplyAlpha
+ * optional, scalar to multiply the alpha element with
+ */
+ fill: function TGLR_fill(aColor, aMultiplyAlpha)
+ {
+ let fill = this._fillColor;
+
+ fill[0] = aColor[0];
+ fill[1] = aColor[1];
+ fill[2] = aColor[2];
+ fill[3] = aColor[3] * (aMultiplyAlpha || 1);
+ },
+
+ /**
+ * Sets the current stroke color.
+ *
+ * @param {Array} aColor
+ * the color stroke to be used as [r, g, b, a] with 0..1 range
+ * @param {Number} aMultiplyAlpha
+ * optional, scalar to multiply the alpha element with
+ */
+ stroke: function TGLR_stroke(aColor, aMultiplyAlpha)
+ {
+ let stroke = this._strokeColor;
+
+ stroke[0] = aColor[0];
+ stroke[1] = aColor[1];
+ stroke[2] = aColor[2];
+ stroke[3] = aColor[3] * (aMultiplyAlpha || 1);
+ },
+
+ /**
+ * Sets the current stroke weight (line width).
+ *
+ * @param {Number} aWeight
+ * the stroke weight
+ */
+ strokeWeight: function TGLR_strokeWeight(aWeight)
+ {
+ if (this._strokeWeightValue !== aWeight) {
+ this._strokeWeightValue = aWeight;
+ this.context.lineWidth(aWeight);
+ }
+ },
+
+ /**
+ * Sets a default perspective projection, with the near frustum rectangle
+ * mapped to the canvas width and height bounds.
+ */
+ perspective: function TGLR_perspective()
+ {
+ let fov = 45;
+ let w = this.width;
+ let h = this.height;
+ let x = w / 2;
+ let y = h / 2;
+ let z = y / Math.tan(TiltMath.radians(fov) / 2);
+ let aspect = w / h;
+ let znear = z / 10;
+ let zfar = z * 10;
+
+ mat4.perspective(fov, aspect, znear, zfar, this.projMatrix, -1);
+ mat4.translate(this.projMatrix, [-x, -y, -z]);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Sets a default orthographic projection (recommended for 2d rendering).
+ */
+ ortho: function TGLR_ortho()
+ {
+ mat4.ortho(0, this.width, this.height, 0, -1, 1, this.projMatrix);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Sets a custom projection matrix.
+ * @param {Array} matrix: the custom projection matrix to be used
+ */
+ projection: function TGLR_projection(aMatrix)
+ {
+ mat4.set(aMatrix, this.projMatrix);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Resets the model view matrix to identity.
+ * This is a default matrix with no rotation, no scaling, at (0, 0, 0);
+ */
+ origin: function TGLR_origin()
+ {
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Transforms the model view matrix with a new matrix.
+ * Useful for creating custom transformations.
+ *
+ * @param {Array} matrix: the matrix to be multiply the model view with
+ */
+ transform: function TGLR_transform(aMatrix)
+ {
+ mat4.multiply(this.mvMatrix, aMatrix);
+ },
+
+ /**
+ * Translates the model view by the x, y and z coordinates.
+ *
+ * @param {Number} x
+ * the x amount of translation
+ * @param {Number} y
+ * the y amount of translation
+ * @param {Number} z
+ * optional, the z amount of translation
+ */
+ translate: function TGLR_translate(x, y, z)
+ {
+ mat4.translate(this.mvMatrix, [x, y, z || 0]);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the x, y and z axis.
+ *
+ * @param {Number} angle
+ * the angle expressed in radians
+ * @param {Number} x
+ * the x axis of the rotation
+ * @param {Number} y
+ * the y axis of the rotation
+ * @param {Number} z
+ * the z axis of the rotation
+ */
+ rotate: function TGLR_rotate(angle, x, y, z)
+ {
+ mat4.rotate(this.mvMatrix, angle, [x, y, z]);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the x axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateX: function TGLR_rotateX(aAngle)
+ {
+ mat4.rotateX(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the y axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateY: function TGLR_rotateY(aAngle)
+ {
+ mat4.rotateY(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the z axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateZ: function TGLR_rotateZ(aAngle)
+ {
+ mat4.rotateZ(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Scales the model view by the x, y and z coordinates.
+ *
+ * @param {Number} x
+ * the x amount of scaling
+ * @param {Number} y
+ * the y amount of scaling
+ * @param {Number} z
+ * optional, the z amount of scaling
+ */
+ scale: function TGLR_scale(x, y, z)
+ {
+ mat4.scale(this.mvMatrix, [x, y, z || 1]);
+ },
+
+ /**
+ * Performs a custom interpolation between two matrices.
+ * The result is saved in the first operand.
+ *
+ * @param {Array} aMat
+ * the first matrix
+ * @param {Array} aMat2
+ * the second matrix
+ * @param {Number} aLerp
+ * interpolation amount between the two inputs
+ * @param {Number} aDamping
+ * optional, scalar adjusting the interpolation amortization
+ * @param {Number} aBalance
+ * optional, scalar adjusting the interpolation shift ammount
+ */
+ lerp: function TGLR_lerp(aMat, aMat2, aLerp, aDamping, aBalance)
+ {
+ if (aLerp < 0 || aLerp > 1) {
+ return;
+ }
+
+ // calculate the interpolation factor based on the damping and step
+ let f = Math.pow(1 - Math.pow(aLerp, aDamping || 1), 1 / aBalance || 1);
+
+ // interpolate each element from the two matrices
+ for (let i = 0, len = this.projMatrix.length; i < len; i++) {
+ aMat[i] = aMat[i] + f * (aMat2[i] - aMat[i]);
+ }
+ },
+
+ /**
+ * Resets the drawing style to default.
+ */
+ defaults: function TGLR_defaults()
+ {
+ this.depthTest(true);
+ this.stencilTest(false);
+ this.cullFace(false);
+ this.frontFace("ccw");
+ this.blendMode("alpha");
+ this.fill([1, 1, 1, 1]);
+ this.stroke([0, 0, 0, 1]);
+ this.strokeWeight(1);
+ this.perspective();
+ this.origin();
+ },
+
+ /**
+ * Draws a quad composed of four vertices.
+ * Vertices must be in clockwise order, or else drawing will be distorted.
+ * Do not abuse this function, it is quite slow.
+ *
+ * @param {Array} aV0
+ * the [x, y, z] position of the first triangle point
+ * @param {Array} aV1
+ * the [x, y, z] position of the second triangle point
+ * @param {Array} aV2
+ * the [x, y, z] position of the third triangle point
+ * @param {Array} aV3
+ * the [x, y, z] position of the fourth triangle point
+ */
+ quad: function TGLR_quad(aV0, aV1, aV2, aV3)
+ {
+ let gl = this.context;
+ let fill = this._fillColor;
+ let stroke = this._strokeColor;
+ let vert = new TiltGL.VertexBuffer(gl, [aV0[0], aV0[1], aV0[2] || 0,
+ aV1[0], aV1[1], aV1[2] || 0,
+ aV2[0], aV2[1], aV2[2] || 0,
+ aV3[0], aV3[1], aV3[2] || 0], 3);
+
+ // use the necessary shader and draw the vertices
+ this.useColorShader(vert, fill);
+ this.drawVertices(gl.TRIANGLE_FAN, vert.numItems);
+
+ this.useColorShader(vert, stroke);
+ this.drawVertices(gl.LINE_LOOP, vert.numItems);
+
+ TiltUtils.destroyObject(vert);
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLR_finalize()
+ {
+ if (this.context) {
+ TiltUtils.destroyObject(this._colorShader);
+ }
+ }
+};
+
+/**
+ * Creates a vertex buffer containing an array of elements.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Array} aElementsArray
+ * an array of numbers (floats)
+ * @param {Number} aItemSize
+ * how many items create a block
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+TiltGL.VertexBuffer = function TGL_VertexBuffer(
+ aContext, aElementsArray, aItemSize, aNumItems)
+{
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * The array buffer.
+ */
+ this._ref = null;
+
+ /**
+ * Array of number components contained in the buffer.
+ */
+ this.components = null;
+
+ /**
+ * Variables defining the internal structure of the buffer.
+ */
+ this.itemSize = 0;
+ this.numItems = 0;
+
+ // if the array is specified in the constructor, initialize directly
+ if (aElementsArray) {
+ this.initBuffer(aElementsArray, aItemSize, aNumItems);
+ }
+};
+
+TiltGL.VertexBuffer.prototype = {
+
+ /**
+ * Initializes buffer data to be used for drawing, using an array of floats.
+ * The "aNumItems" param can be specified to use only a portion of the array.
+ *
+ * @param {Array} aElementsArray
+ * an array of floats
+ * @param {Number} aItemSize
+ * how many items create a block
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+ initBuffer: function TGLVB_initBuffer(aElementsArray, aItemSize, aNumItems)
+ {
+ let gl = this._context;
+
+ // the aNumItems parameter is optional, we can compute it if not specified
+ aNumItems = aNumItems || aElementsArray.length / aItemSize;
+
+ // create the Float32Array using the elements array
+ this.components = new Float32Array(aElementsArray);
+
+ // create an array buffer and bind the elements as a Float32Array
+ this._ref = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._ref);
+ gl.bufferData(gl.ARRAY_BUFFER, this.components, gl.STATIC_DRAW);
+
+ // remember some properties, useful when binding the buffer to a shader
+ this.itemSize = aItemSize;
+ this.numItems = aNumItems;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLVB_finalize()
+ {
+ if (this._context) {
+ this._context.deleteBuffer(this._ref);
+ }
+ }
+};
+
+/**
+ * Creates an index buffer containing an array of indices.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Array} aElementsArray
+ * an array of unsigned integers
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+TiltGL.IndexBuffer = function TGL_IndexBuffer(
+ aContext, aElementsArray, aNumItems)
+{
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * The element array buffer.
+ */
+ this._ref = null;
+
+ /**
+ * Array of number components contained in the buffer.
+ */
+ this.components = null;
+
+ /**
+ * Variables defining the internal structure of the buffer.
+ */
+ this.itemSize = 0;
+ this.numItems = 0;
+
+ // if the array is specified in the constructor, initialize directly
+ if (aElementsArray) {
+ this.initBuffer(aElementsArray, aNumItems);
+ }
+};
+
+TiltGL.IndexBuffer.prototype = {
+
+ /**
+ * Initializes a buffer of vertex indices, using an array of unsigned ints.
+ * The item size will automatically default to 1, and the "numItems" will be
+ * equal to the number of items in the array if not specified.
+ *
+ * @param {Array} aElementsArray
+ * an array of numbers (unsigned integers)
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+ initBuffer: function TGLIB_initBuffer(aElementsArray, aNumItems)
+ {
+ let gl = this._context;
+
+ // the aNumItems parameter is optional, we can compute it if not specified
+ aNumItems = aNumItems || aElementsArray.length;
+
+ // create the Uint16Array using the elements array
+ this.components = new Uint16Array(aElementsArray);
+
+ // create an array buffer and bind the elements as a Uint16Array
+ this._ref = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._ref);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.components, gl.STATIC_DRAW);
+
+ // remember some properties, useful when binding the buffer to a shader
+ this.itemSize = 1;
+ this.numItems = aNumItems;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLIB_finalize()
+ {
+ if (this._context) {
+ this._context.deleteBuffer(this._ref);
+ }
+ }
+};
+
+/**
+ * A program is composed of a vertex and a fragment shader.
+ *
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+TiltGL.Program = function(aContext, aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * A reference to the actual GLSL program.
+ */
+ this._ref = null;
+
+ /**
+ * Each program has an unique id assigned.
+ */
+ this._id = -1;
+
+ /**
+ * Two arrays: an attributes array, containing all the cached attributes
+ * and a uniforms array, containing all the cached uniforms.
+ */
+ this._attributes = null;
+ this._uniforms = null;
+
+ // if the sources are specified in the constructor, initialize directly
+ if (aProperties.vs && aProperties.fs) {
+ this.initProgram(aProperties);
+ }
+};
+
+TiltGL.Program.prototype = {
+
+ /**
+ * Initializes a shader program, using specified source code as strings.
+ *
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+ initProgram: function TGLP_initProgram(aProperties)
+ {
+ this._ref = TiltGL.ProgramUtils.create(this._context, aProperties);
+
+ // cache for faster access
+ this._id = this._ref.id;
+ this._attributes = this._ref.attributes;
+ this._uniforms = this._ref.uniforms;
+
+ // cleanup
+ delete this._ref.id;
+ delete this._ref.attributes;
+ delete this._ref.uniforms;
+ },
+
+ /**
+ * Uses the shader program as current one for the WebGL context; it also
+ * enables vertex attributes necessary to enable when using this program.
+ * This method also does some useful caching, as the function "useProgram"
+ * could take quite a lot of time.
+ */
+ use: function TGLP_use()
+ {
+ let id = this._id;
+ let utils = TiltGL.ProgramUtils;
+
+ // check if the program wasn't already active
+ if (utils._activeProgram !== id) {
+ utils._activeProgram = id;
+
+ // use the the program if it wasn't already set
+ this._context.useProgram(this._ref);
+ this.cleanupVertexAttrib();
+
+ // enable any necessary vertex attributes using the cache
+ for each (let attribute in this._attributes) {
+ this._context.enableVertexAttribArray(attribute);
+ utils._enabledAttributes.push(attribute);
+ }
+ }
+ },
+
+ /**
+ * Disables all currently enabled vertex attribute arrays.
+ */
+ cleanupVertexAttrib: function TGLP_cleanupVertexAttrib()
+ {
+ let utils = TiltGL.ProgramUtils;
+
+ for each (let attribute in utils._enabledAttributes) {
+ this._context.disableVertexAttribArray(attribute);
+ }
+ utils._enabledAttributes = [];
+ },
+
+ /**
+ * Binds a vertex buffer as an array buffer for a specific shader attribute.
+ *
+ * @param {String} aAtribute
+ * the attribute name obtained from the shader
+ * @param {Float32Array} aBuffer
+ * the buffer to be bound
+ */
+ bindVertexBuffer: function TGLP_bindVertexBuffer(aAtribute, aBuffer)
+ {
+ // get the cached attribute value from the shader
+ let gl = this._context;
+ let attr = this._attributes[aAtribute];
+ let size = aBuffer.itemSize;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, aBuffer._ref);
+ gl.vertexAttribPointer(attr, size, gl.FLOAT, false, 0, 0);
+ },
+
+ /**
+ * Binds a uniform matrix to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Float32Array} m
+ * the matrix to be bound
+ */
+ bindUniformMatrix: function TGLP_bindUniformMatrix(aUniform, m)
+ {
+ this._context.uniformMatrix4fv(this._uniforms[aUniform], false, m);
+ },
+
+ /**
+ * Binds a uniform vector of 4 elements to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Float32Array} v
+ * the vector to be bound
+ */
+ bindUniformVec4: function TGLP_bindUniformVec4(aUniform, v)
+ {
+ this._context.uniform4fv(this._uniforms[aUniform], v);
+ },
+
+ /**
+ * Binds a simple float element to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Number} v
+ * the variable to be bound
+ */
+ bindUniformFloat: function TGLP_bindUniformFloat(aUniform, f)
+ {
+ this._context.uniform1f(this._uniforms[aUniform], f);
+ },
+
+ /**
+ * Binds a uniform texture for a sampler to the current shader.
+ *
+ * @param {String} aSampler
+ * the sampler name to bind the texture to
+ * @param {TiltGL.Texture} aTexture
+ * the texture to be bound
+ */
+ bindTexture: function TGLP_bindTexture(aSampler, aTexture)
+ {
+ let gl = this._context;
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, aTexture._ref);
+ gl.uniform1i(this._uniforms[aSampler], 0);
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLP_finalize()
+ {
+ if (this._context) {
+ this._context.useProgram(null);
+ this._context.deleteProgram(this._ref);
+ }
+ }
+};
+
+/**
+ * Utility functions for handling GLSL shaders and programs.
+ */
+TiltGL.ProgramUtils = {
+
+ /**
+ * Initializes a shader program, using specified source code as strings,
+ * returning the newly created shader program, by compiling and linking the
+ * vertex and fragment shader.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+ create: function TGLPU_create(aContext, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // compile the two shaders
+ let vertShader = this.compile(aContext, aProperties.vs, "vertex");
+ let fragShader = this.compile(aContext, aProperties.fs, "fragment");
+ let program = this.link(aContext, vertShader, fragShader);
+
+ aContext.deleteShader(vertShader);
+ aContext.deleteShader(fragShader);
+
+ return this.cache(aContext, aProperties, program);
+ },
+
+ /**
+ * Compiles a shader source of a specific type, either vertex or fragment.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {String} aShaderSource
+ * the source code for the shader
+ * @param {String} aShaderType
+ * the shader type ("vertex" or "fragment")
+ *
+ * @return {WebGLShader} the compiled shader
+ */
+ compile: function TGLPU_compile(aContext, aShaderSource, aShaderType)
+ {
+ let gl = aContext, shader, status;
+
+ // make sure the shader source is valid
+ if ("string" !== typeof aShaderSource || aShaderSource.length < 1) {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.get("compileShader.source.error"));
+ return null;
+ }
+
+ // also make sure the necessary shader mime type is valid
+ if (aShaderType === "vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ } else if (aShaderType === "fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("compileShader.type.error", [aShaderSource]));
+ return null;
+ }
+
+ // set the shader source and compile it
+ gl.shaderSource(shader, aShaderSource);
+ gl.compileShader(shader);
+
+ // remember the shader source (useful for debugging and caching)
+ shader.src = aShaderSource;
+
+ // verify the compile status; if something went wrong, log the error
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ status = gl.getShaderInfoLog(shader);
+
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("compileShader.compile.error", [status]));
+ return null;
+ }
+
+ // return the newly compiled shader from the specified source
+ return shader;
+ },
+
+ /**
+ * Links two compiled vertex or fragment shaders together to form a program.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {WebGLShader} aVertShader
+ * the compiled vertex shader
+ * @param {WebGLShader} aFragShader
+ * the compiled fragment shader
+ *
+ * @return {WebGLProgram} the newly created and linked shader program
+ */
+ link: function TGLPU_link(aContext, aVertShader, aFragShader)
+ {
+ let gl = aContext, program, status;
+
+ // create a program and attach the compiled vertex and fragment shaders
+ program = gl.createProgram();
+
+ // attach the vertex and fragment shaders to the program
+ gl.attachShader(program, aVertShader);
+ gl.attachShader(program, aFragShader);
+ gl.linkProgram(program);
+
+ // verify the link status; if something went wrong, log the error
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ status = gl.getProgramInfoLog(program);
+
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("linkProgram.error", [status]));
+ return null;
+ }
+
+ // generate an id for the program
+ program.id = this._count++;
+
+ return program;
+ },
+
+ /**
+ * Caches shader attributes and uniforms as properties for a program object.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Array} attributes: optional, an array of attributes as strings
+ * {Array} uniforms: optional, an array of uniforms as strings
+ * @param {WebGLProgram} aProgram
+ * the shader program used for caching
+ *
+ * @return {WebGLProgram} the same program
+ */
+ cache: function TGLPU_cache(aContext, aProperties, aProgram)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // make sure the attributes and uniforms cache objects are created
+ aProgram.attributes = {};
+ aProgram.uniforms = {};
+
+ Object.defineProperty(aProgram.attributes, "length",
+ { value: 0, writable: true, enumerable: false, configurable: true });
+
+ Object.defineProperty(aProgram.uniforms, "length",
+ { value: 0, writable: true, enumerable: false, configurable: true });
+
+
+ let attr = aProperties.attributes;
+ let unif = aProperties.uniforms;
+
+ if (attr) {
+ for (let i = 0, len = attr.length; i < len; i++) {
+ // try to get a shader attribute from the program
+ let param = attr[i];
+ let loc = aContext.getAttribLocation(aProgram, param);
+
+ if ("number" === typeof loc && loc > -1) {
+ // if we get an attribute location, store it
+ // bind the new parameter only if it was not already defined
+ if (aProgram.attributes[param] === undefined) {
+ aProgram.attributes[param] = loc;
+ aProgram.attributes.length++;
+ }
+ }
+ }
+ }
+
+ if (unif) {
+ for (let i = 0, len = unif.length; i < len; i++) {
+ // try to get a shader uniform from the program
+ let param = unif[i];
+ let loc = aContext.getUniformLocation(aProgram, param);
+
+ if ("object" === typeof loc && loc) {
+ // if we get a uniform object, store it
+ // bind the new parameter only if it was not already defined
+ if (aProgram.uniforms[param] === undefined) {
+ aProgram.uniforms[param] = loc;
+ aProgram.uniforms.length++;
+ }
+ }
+ }
+ }
+
+ return aProgram;
+ },
+
+ /**
+ * The total number of programs created.
+ */
+ _count: 0,
+
+ /**
+ * Represents the current active shader, identified by an id.
+ */
+ _activeProgram: -1,
+
+ /**
+ * Represents the current enabled attributes.
+ */
+ _enabledAttributes: []
+};
+
+/**
+ * This constructor creates a texture from an Image.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ */
+TiltGL.Texture = function(aContext, aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * A reference to the WebGL texture object.
+ */
+ this._ref = null;
+
+ /**
+ * Each texture has an unique id assigned.
+ */
+ this._id = -1;
+
+ /**
+ * Variables specifying the width and height of the texture.
+ * If these values are less than 0, the texture hasn't loaded yet.
+ */
+ this.width = -1;
+ this.height = -1;
+
+ /**
+ * Specifies if the texture has loaded or not.
+ */
+ this.loaded = false;
+
+ // if the image is specified in the constructor, initialize directly
+ if ("object" === typeof aProperties.source) {
+ this.initTexture(aProperties);
+ } else {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.get("initTexture.source.error"));
+ }
+};
+
+TiltGL.Texture.prototype = {
+
+ /**
+ * Initializes a texture from a pre-existing image or canvas.
+ *
+ * @param {Image} aImage
+ * the source image or canvas
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ */
+ initTexture: function TGLT_initTexture(aProperties)
+ {
+ this._ref = TiltGL.TextureUtils.create(this._context, aProperties);
+
+ // cache for faster access
+ this._id = this._ref.id;
+ this.width = this._ref.width;
+ this.height = this._ref.height;
+ this.loaded = true;
+
+ // cleanup
+ delete this._ref.id;
+ delete this._ref.width;
+ delete this._ref.height;
+ delete this.onload;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLT_finalize()
+ {
+ if (this._context) {
+ this._context.deleteTexture(this._ref);
+ }
+ }
+};
+
+/**
+ * Utility functions for creating and manipulating textures.
+ */
+TiltGL.TextureUtils = {
+
+ /**
+ * Initializes a texture from a pre-existing image or canvas.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Image} aImage
+ * the source image or canvas
+ * @param {Object} aProperties
+ * an object containing some of the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ *
+ * @return {WebGLTexture} the created texture
+ */
+ create: function TGLTU_create(aContext, aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ if (!aProperties.source) {
+ return null;
+ }
+
+ let gl = aContext;
+ let width = aProperties.source.width;
+ let height = aProperties.source.height;
+ let format = gl[aProperties.format || "RGB"];
+
+ // make sure the image is power of two before binding to a texture
+ let source = this.resizeImageToPowerOfTwo(aProperties);
+
+ // first, create the texture to hold the image data
+ let texture = gl.createTexture();
+
+ // attach the image data to the newly create texture
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, format, gl.UNSIGNED_BYTE, source);
+ this.setTextureParams(gl, aProperties);
+
+ // do some cleanup
+ gl.bindTexture(gl.TEXTURE_2D, null);
+
+ // remember the width and the height
+ texture.width = width;
+ texture.height = height;
+
+ // generate an id for the texture
+ texture.id = this._count++;
+
+ return texture;
+ },
+
+ /**
+ * Sets texture parameters for the current texture binding.
+ * Optionally, you can also (re)set the current texture binding manually.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the texture properties
+ */
+ setTextureParams: function TGLTU_setTextureParams(aContext, aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ let gl = aContext;
+ let minFilter = gl.TEXTURE_MIN_FILTER;
+ let magFilter = gl.TEXTURE_MAG_FILTER;
+ let wrapS = gl.TEXTURE_WRAP_S;
+ let wrapT = gl.TEXTURE_WRAP_T;
+
+ // bind a new texture if necessary
+ if (aProperties.texture) {
+ gl.bindTexture(gl.TEXTURE_2D, aProperties.texture.ref);
+ }
+
+ // set the minification filter
+ if ("nearest" === aProperties.minFilter) {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.NEAREST);
+ } else if ("linear" === aProperties.minFilter && aProperties.mipmap) {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR_MIPMAP_LINEAR);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR);
+ }
+
+ // set the magnification filter
+ if ("nearest" === aProperties.magFilter) {
+ gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.NEAREST);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.LINEAR);
+ }
+
+ // set the wrapping on the x-axis for the texture
+ if ("repeat" === aProperties.wrapS) {
+ gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.REPEAT);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.CLAMP_TO_EDGE);
+ }
+
+ // set the wrapping on the y-axis for the texture
+ if ("repeat" === aProperties.wrapT) {
+ gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.REPEAT);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.CLAMP_TO_EDGE);
+ }
+
+ // generate mipmap if necessary
+ if (aProperties.mipmap) {
+ gl.generateMipmap(gl.TEXTURE_2D);
+ }
+ },
+
+ /**
+ * This shim renders a content window to a canvas element, but clamps the
+ * maximum width and height of the canvas to the WebGL MAX_TEXTURE_SIZE.
+ *
+ * @param {Window} aContentWindow
+ * the content window to get a texture from
+ * @param {Number} aMaxImageSize
+ * the maximum image size to be used
+ *
+ * @return {Image} the new content window image
+ */
+ createContentImage: function TGLTU_createContentImage(
+ aContentWindow, aMaxImageSize)
+ {
+ // calculate the total width and height of the content page
+ let size = TiltUtils.DOM.getContentWindowDimensions(aContentWindow);
+
+ // use a custom canvas element and a 2d context to draw the window
+ let canvas = TiltUtils.DOM.initCanvas(null);
+ canvas.width = TiltMath.clamp(size.width, 0, aMaxImageSize);
+ canvas.height = TiltMath.clamp(size.height, 0, aMaxImageSize);
+
+ // use the 2d context.drawWindow() magic
+ let ctx = canvas.getContext("2d");
+ ctx.drawWindow(aContentWindow, 0, 0, canvas.width, canvas.height, "#fff");
+
+ return canvas;
+ },
+
+ /**
+ * Scales an image's width and height to next power of two.
+ * If the image already has power of two sizes, it is immediately returned,
+ * otherwise, a new image is created.
+ *
+ * @param {Image} aImage
+ * the image to be scaled
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Image} source: the source image to resize
+ * {Boolean} resize: true to resize the image if it has npot dimensions
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an image outline
+ * {Number} strokeWeight: optional, the width of the outline
+ *
+ * @return {Image} the resized image
+ */
+ resizeImageToPowerOfTwo: function TGLTU_resizeImageToPowerOfTwo(aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ if (!aProperties.source) {
+ return null;
+ }
+
+ let isPowerOfTwoWidth = TiltMath.isPowerOfTwo(aProperties.source.width);
+ let isPowerOfTwoHeight = TiltMath.isPowerOfTwo(aProperties.source.height);
+
+ // first check if the image is not already power of two
+ if (!aProperties.resize || (isPowerOfTwoWidth && isPowerOfTwoHeight)) {
+ return aProperties.source;
+ }
+
+ // calculate the power of two dimensions for the npot image
+ let width = TiltMath.nextPowerOfTwo(aProperties.source.width);
+ let height = TiltMath.nextPowerOfTwo(aProperties.source.height);
+
+ // create a canvas, then we will use a 2d context to scale the image
+ let canvas = TiltUtils.DOM.initCanvas(null, {
+ width: width,
+ height: height
+ });
+
+ let ctx = canvas.getContext("2d");
+
+ // optional fill (useful when handling transparent images)
+ if (aProperties.fill) {
+ ctx.fillStyle = aProperties.fill;
+ ctx.fillRect(0, 0, width, height);
+ }
+
+ // draw the image with power of two dimensions
+ ctx.drawImage(aProperties.source, 0, 0, width, height);
+
+ // optional stroke (useful when creating textures for edges)
+ if (aProperties.stroke) {
+ ctx.strokeStyle = aProperties.stroke;
+ ctx.lineWidth = aProperties.strokeWeight;
+ ctx.strokeRect(0, 0, width, height);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * The total number of textures created.
+ */
+ _count: 0
+};
+
+/**
+ * A color shader. The only useful thing it does is set the gl_FragColor.
+ *
+ * @param {Attribute} vertexPosition: the vertex position
+ * @param {Uniform} mvMatrix: the model view matrix
+ * @param {Uniform} projMatrix: the projection matrix
+ * @param {Uniform} color: the color to set the gl_FragColor to
+ */
+TiltGL.ColorShader = {
+
+ /**
+ * Vertex shader.
+ */
+ vs: [
+ "attribute vec3 vertexPosition;",
+
+ "uniform mat4 mvMatrix;",
+ "uniform mat4 projMatrix;",
+
+ "void main() {",
+ " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);",
+ "}"
+ ].join("\n"),
+
+ /**
+ * Fragment shader.
+ */
+ fs: [
+ "#ifdef GL_ES",
+ "precision lowp float;",
+ "#endif",
+
+ "uniform vec4 fill;",
+
+ "void main() {",
+ " gl_FragColor = fill;",
+ "}"
+ ].join("\n")
+};
+
+TiltGL.isWebGLForceEnabled = function TGL_isWebGLForceEnabled()
+{
+ return Services.prefs.getBoolPref("webgl.force-enabled");
+};
+
+/**
+ * Tests if the WebGL OpenGL or Angle renderer is available using the
+ * GfxInfo service.
+ *
+ * @return {Boolean} true if WebGL is available
+ */
+TiltGL.isWebGLSupported = function TGL_isWebGLSupported()
+{
+ 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_NO_INFO ||
+ gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_NO_INFO;
+ } catch(e) {
+ if (e && e.message) { TiltUtils.Output.error(e.message); }
+ return false;
+ }
+ return supported;
+};
+
+/**
+ * Helper function to create a 3D context.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas to get the WebGL context from
+ * @param {Object} aFlags
+ * optional, flags used for initialization
+ *
+ * @return {Object} the WebGL context, or null if anything failed
+ */
+TiltGL.create3DContext = function TGL_create3DContext(aCanvas, aFlags)
+{
+ TiltGL.clearCache();
+
+ // try to get a valid context from an existing canvas
+ let context = null;
+
+ try {
+ context = aCanvas.getContext(WEBGL_CONTEXT_NAME, aFlags);
+ } catch(e) {
+ if (e && e.message) { TiltUtils.Output.error(e.message); }
+ return null;
+ }
+ return context;
+};
+
+/**
+ * Clears the cache and sets all the variables to default.
+ */
+TiltGL.clearCache = function TGL_clearCache()
+{
+ TiltGL.ProgramUtils._activeProgram = -1;
+ TiltGL.ProgramUtils._enabledAttributes = [];
+};
diff --git a/browser/devtools/tilt/tilt-math.js b/browser/devtools/tilt/tilt-math.js
new file mode 100644
index 000000000..6b2d2e101
--- /dev/null
+++ b/browser/devtools/tilt/tilt-math.js
@@ -0,0 +1,2322 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 {Cu} = require("chrome");
+
+let TiltUtils = require("devtools/tilt/tilt-utils");
+
+/**
+ * Module containing high performance matrix and vector operations for WebGL.
+ * Inspired by glMatrix, version 0.9.6, (c) 2011 Brandon Jones.
+ */
+
+let EPSILON = 0.01;
+exports.EPSILON = EPSILON;
+
+const PI_OVER_180 = Math.PI / 180;
+const INV_PI_OVER_180 = 180 / Math.PI;
+const FIFTEEN_OVER_225 = 15 / 225;
+const ONE_OVER_255 = 1 / 255;
+
+/**
+ * vec3 - 3 Dimensional Vector.
+ */
+let vec3 = {
+
+ /**
+ * Creates a new instance of a vec3 using the Float32Array type.
+ * Any array containing at least 3 numeric elements can serve as a vec3.
+ *
+ * @param {Array} aVec
+ * optional, vec3 containing values to initialize with
+ *
+ * @return {Array} a new instance of a vec3
+ */
+ create: function V3_create(aVec)
+ {
+ let dest = new Float32Array(3);
+
+ if (aVec) {
+ vec3.set(aVec, dest);
+ } else {
+ vec3.zero(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one vec3 to another.
+ *
+ * @param {Array} aVec
+ * vec3 containing values to copy
+ * @param {Array} aDest
+ * vec3 receiving copied values
+ *
+ * @return {Array} the destination vec3 receiving copied values
+ */
+ set: function V3_set(aVec, aDest)
+ {
+ aDest[0] = aVec[0];
+ aDest[1] = aVec[1];
+ aDest[2] = aVec[2] || 0;
+ return aDest;
+ },
+
+ /**
+ * Sets a vec3 to an zero vector.
+ *
+ * @param {Array} aDest
+ * vec3 to set
+ *
+ * @return {Array} the same vector
+ */
+ zero: function V3_zero(aDest)
+ {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ },
+
+ /**
+ * Performs a vector addition.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ add: function V3_add(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] + aVec2[0];
+ aDest[1] = aVec[1] + aVec2[1];
+ aDest[2] = aVec[2] + aVec2[2];
+ return aDest;
+ },
+
+ /**
+ * Performs a vector subtraction.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ subtract: function V3_subtract(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] - aVec2[0];
+ aDest[1] = aVec[1] - aVec2[1];
+ aDest[2] = aVec[2] - aVec2[2];
+ return aDest;
+ },
+
+ /**
+ * Negates the components of a vec3.
+ *
+ * @param {Array} aVec
+ * vec3 to negate
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ negate: function V3_negate(aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = -aVec[0];
+ aDest[1] = -aVec[1];
+ aDest[2] = -aVec[2];
+ return aDest;
+ },
+
+ /**
+ * Multiplies the components of a vec3 by a scalar value.
+ *
+ * @param {Array} aVec
+ * vec3 to scale
+ * @param {Number} aVal
+ * numeric value to scale by
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ scale: function V3_scale(aVec, aVal, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] * aVal;
+ aDest[1] = aVec[1] * aVal;
+ aDest[2] = aVec[2] * aVal;
+ return aDest;
+ },
+
+ /**
+ * Generates a unit vector of the same direction as the provided vec3.
+ * If vector length is 0, returns [0, 0, 0].
+ *
+ * @param {Array} aVec
+ * vec3 to normalize
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ normalize: function V3_normalize(aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let len = Math.sqrt(x * x + y * y + z * z);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ return aDest;
+ },
+
+ /**
+ * Generates the cross product of two vectors.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ cross: function V3_cross(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let x2 = aVec2[0];
+ let y2 = aVec2[1];
+ let z2 = aVec2[2];
+
+ aDest[0] = y * z2 - z * y2;
+ aDest[1] = z * x2 - x * z2;
+ aDest[2] = x * y2 - y * x2;
+ return aDest;
+ },
+
+ /**
+ * Caclulate the dot product of two vectors.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ *
+ * @return {Array} dot product of the first and second operand
+ */
+ dot: function V3_dot(aVec, aVec2)
+ {
+ return aVec[0] * aVec2[0] + aVec[1] * aVec2[1] + aVec[2] * aVec2[2];
+ },
+
+ /**
+ * Caclulate the length of a vec3.
+ *
+ * @param {Array} aVec
+ * vec3 to calculate length of
+ *
+ * @return {Array} length of the vec3
+ */
+ length: function V3_length(aVec)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ return Math.sqrt(x * x + y * y + z * z);
+ },
+
+ /**
+ * Generates a unit vector pointing from one vector to another.
+ *
+ * @param {Array} aVec
+ * origin vec3
+ * @param {Array} aVec2
+ * vec3 to point to
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ direction: function V3_direction(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0] - aVec2[0];
+ let y = aVec[1] - aVec2[1];
+ let z = aVec[2] - aVec2[2];
+ let len = Math.sqrt(x * x + y * y + z * z);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ return aDest;
+ },
+
+ /**
+ * Performs a linear interpolation between two vec3.
+ *
+ * @param {Array} aVec
+ * first vector
+ * @param {Array} aVec2
+ * second vector
+ * @param {Number} aLerp
+ * interpolation amount between the two inputs
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ lerp: function V3_lerp(aVec, aVec2, aLerp, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] + aLerp * (aVec2[0] - aVec[0]);
+ aDest[1] = aVec[1] + aLerp * (aVec2[1] - aVec[1]);
+ aDest[2] = aVec[2] + aLerp * (aVec2[2] - aVec[2]);
+ return aDest;
+ },
+
+ /**
+ * Projects a 3D point on a 2D screen plane.
+ *
+ * @param {Array} aP
+ * the [x, y, z] coordinates of the point to project
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ * @param {Array} aDest
+ * optional parameter, the array to write the values to
+ *
+ * @return {Array} the projected coordinates
+ */
+ project: function V3_project(aP, aViewport, aMvMatrix, aProjMatrix, aDest)
+ {
+ /*jshint undef: false */
+
+ let mvpMatrix = new Float32Array(16);
+ let coordinates = new Float32Array(4);
+
+ // compute the perspective * model view matrix
+ mat4.multiply(aProjMatrix, aMvMatrix, mvpMatrix);
+
+ // now transform that vector into homogenous coordinates
+ coordinates[0] = aP[0];
+ coordinates[1] = aP[1];
+ coordinates[2] = aP[2];
+ coordinates[3] = 1;
+ mat4.multiplyVec4(mvpMatrix, coordinates);
+
+ // transform the homogenous coordinates into screen space
+ coordinates[0] /= coordinates[3];
+ coordinates[0] *= aViewport[2] * 0.5;
+ coordinates[0] += aViewport[2] * 0.5;
+ coordinates[1] /= coordinates[3];
+ coordinates[1] *= -aViewport[3] * 0.5;
+ coordinates[1] += aViewport[3] * 0.5;
+ coordinates[2] = 0;
+
+ if (!aDest) {
+ vec3.set(coordinates, aP);
+ } else {
+ vec3.set(coordinates, aDest);
+ }
+ return coordinates;
+ },
+
+ /**
+ * Unprojects a 2D point to 3D space.
+ *
+ * @param {Array} aP
+ * the [x, y, z] coordinates of the point to unproject;
+ * the z value should range between 0 and 1, as clipping plane
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ * @param {Array} aDest
+ * optional parameter, the array to write the values to
+ *
+ * @return {Array} the unprojected coordinates
+ */
+ unproject: function V3_unproject(
+ aP, aViewport, aMvMatrix, aProjMatrix, aDest)
+ {
+ /*jshint undef: false */
+
+ let mvpMatrix = new Float32Array(16);
+ let coordinates = new Float32Array(4);
+
+ // compute the inverse of the perspective * model view matrix
+ mat4.multiply(aProjMatrix, aMvMatrix, mvpMatrix);
+ mat4.inverse(mvpMatrix);
+
+ // transformation of normalized coordinates (-1 to 1)
+ coordinates[0] = +((aP[0] - aViewport[0]) / aViewport[2] * 2 - 1);
+ coordinates[1] = -((aP[1] - aViewport[1]) / aViewport[3] * 2 - 1);
+ coordinates[2] = 2 * aP[2] - 1;
+ coordinates[3] = 1;
+
+ // now transform that vector into space coordinates
+ mat4.multiplyVec4(mvpMatrix, coordinates);
+
+ // invert to normalize x, y, and z values
+ coordinates[3] = 1 / coordinates[3];
+ coordinates[0] *= coordinates[3];
+ coordinates[1] *= coordinates[3];
+ coordinates[2] *= coordinates[3];
+
+ if (!aDest) {
+ vec3.set(coordinates, aP);
+ } else {
+ vec3.set(coordinates, aDest);
+ }
+ return coordinates;
+ },
+
+ /**
+ * Create a ray between two points using the current model view & projection
+ * matrices. This is useful when creating a ray destined for 3D picking.
+ *
+ * @param {Array} aP0
+ * the [x, y, z] coordinates of the first point
+ * @param {Array} aP1
+ * the [x, y, z] coordinates of the second point
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ *
+ * @return {Object} a ray object containing the direction vector between
+ * the two unprojected points, the position and the lookAt
+ */
+ createRay: function V3_createRay(aP0, aP1, aViewport, aMvMatrix, aProjMatrix)
+ {
+ // unproject the two points
+ vec3.unproject(aP0, aViewport, aMvMatrix, aProjMatrix, aP0);
+ vec3.unproject(aP1, aViewport, aMvMatrix, aProjMatrix, aP1);
+
+ return {
+ origin: aP0,
+ direction: vec3.normalize(vec3.subtract(aP1, aP0))
+ };
+ },
+
+ /**
+ * Returns a string representation of a vector.
+ *
+ * @param {Array} aVec
+ * vec3 to represent as a string
+ *
+ * @return {String} representation of the vector
+ */
+ str: function V3_str(aVec)
+ {
+ return '[' + aVec[0] + ", " + aVec[1] + ", " + aVec[2] + ']';
+ }
+};
+
+exports.vec3 = vec3;
+
+/**
+ * mat3 - 3x3 Matrix.
+ */
+let mat3 = {
+
+ /**
+ * Creates a new instance of a mat3 using the Float32Array array type.
+ * Any array containing at least 9 numeric elements can serve as a mat3.
+ *
+ * @param {Array} aMat
+ * optional, mat3 containing values to initialize with
+ *
+ * @return {Array} a new instance of a mat3
+ */
+ create: function M3_create(aMat)
+ {
+ let dest = new Float32Array(9);
+
+ if (aMat) {
+ mat3.set(aMat, dest);
+ } else {
+ mat3.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one mat3 to another.
+ *
+ * @param {Array} aMat
+ * mat3 containing values to copy
+ * @param {Array} aDest
+ * mat3 receiving copied values
+ *
+ * @return {Array} the destination mat3 receiving copied values
+ */
+ set: function M3_set(aMat, aDest)
+ {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ return aDest;
+ },
+
+ /**
+ * Sets a mat3 to an identity matrix.
+ *
+ * @param {Array} aDest
+ * mat3 to set
+ *
+ * @return {Array} the same matrix
+ */
+ identity: function M3_identity(aDest)
+ {
+ aDest[0] = 1;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 1;
+ aDest[5] = 0;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 1;
+ return aDest;
+ },
+
+ /**
+ * Transposes a mat3 (flips the values over the diagonal).
+ *
+ * @param {Array} aMat
+ * mat3 to transpose
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ transpose: function M3_transpose(aMat, aDest)
+ {
+ if (!aDest || aMat === aDest) {
+ let a01 = aMat[1];
+ let a02 = aMat[2];
+ let a12 = aMat[5];
+
+ aMat[1] = aMat[3];
+ aMat[2] = aMat[6];
+ aMat[3] = a01;
+ aMat[5] = aMat[7];
+ aMat[6] = a02;
+ aMat[7] = a12;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[3];
+ aDest[2] = aMat[6];
+ aDest[3] = aMat[1];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[7];
+ aDest[6] = aMat[2];
+ aDest[7] = aMat[5];
+ aDest[8] = aMat[8];
+ return aDest;
+ },
+
+ /**
+ * Copies the elements of a mat3 into the upper 3x3 elements of a mat4.
+ *
+ * @param {Array} aMat
+ * mat3 containing values to copy
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat4: function M3_toMat4(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = 0;
+ aDest[4] = aMat[3];
+ aDest[5] = aMat[4];
+ aDest[6] = aMat[5];
+ aDest[7] = 0;
+ aDest[8] = aMat[6];
+ aDest[9] = aMat[7];
+ aDest[10] = aMat[8];
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a 3x3 matrix.
+ *
+ * @param {Array} aMat
+ * mat3 to represent as a string
+ *
+ * @return {String} representation of the matrix
+ */
+ str: function M3_str(aMat)
+ {
+ return "[" + aMat[0] + ", " + aMat[1] + ", " + aMat[2] +
+ ", " + aMat[3] + ", " + aMat[4] + ", " + aMat[5] +
+ ", " + aMat[6] + ", " + aMat[7] + ", " + aMat[8] + "]";
+ }
+};
+
+exports.mat3 = mat3;
+
+/**
+ * mat4 - 4x4 Matrix.
+ */
+let mat4 = {
+
+ /**
+ * Creates a new instance of a mat4 using the default Float32Array type.
+ * Any array containing at least 16 numeric elements can serve as a mat4.
+ *
+ * @param {Array} aMat
+ * optional, mat4 containing values to initialize with
+ *
+ * @return {Array} a new instance of a mat4
+ */
+ create: function M4_create(aMat)
+ {
+ let dest = new Float32Array(16);
+
+ if (aMat) {
+ mat4.set(aMat, dest);
+ } else {
+ mat4.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one mat4 to another
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * mat4 receiving copied values
+ *
+ * @return {Array} the destination mat4 receiving copied values
+ */
+ set: function M4_set(aMat, aDest)
+ {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Sets a mat4 to an identity matrix.
+ *
+ * @param {Array} aDest
+ * mat4 to set
+ *
+ * @return {Array} the same matrix
+ */
+ identity: function M4_identity(aDest)
+ {
+ aDest[0] = 1;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = 1;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 0;
+ aDest[9] = 0;
+ aDest[10] = 1;
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Transposes a mat4 (flips the values over the diagonal).
+ *
+ * @param {Array} aMat
+ * mat4 to transpose
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ transpose: function M4_transpose(aMat, aDest)
+ {
+ if (!aDest || aMat === aDest) {
+ let a01 = aMat[1];
+ let a02 = aMat[2];
+ let a03 = aMat[3];
+ let a12 = aMat[6];
+ let a13 = aMat[7];
+ let a23 = aMat[11];
+
+ aMat[1] = aMat[4];
+ aMat[2] = aMat[8];
+ aMat[3] = aMat[12];
+ aMat[4] = a01;
+ aMat[6] = aMat[9];
+ aMat[7] = aMat[13];
+ aMat[8] = a02;
+ aMat[9] = a12;
+ aMat[11] = aMat[14];
+ aMat[12] = a03;
+ aMat[13] = a13;
+ aMat[14] = a23;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[4];
+ aDest[2] = aMat[8];
+ aDest[3] = aMat[12];
+ aDest[4] = aMat[1];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[9];
+ aDest[7] = aMat[13];
+ aDest[8] = aMat[2];
+ aDest[9] = aMat[6];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[14];
+ aDest[12] = aMat[3];
+ aDest[13] = aMat[7];
+ aDest[14] = aMat[11];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Calculate the determinant of a mat4.
+ *
+ * @param {Array} aMat
+ * mat4 to calculate determinant of
+ *
+ * @return {Number} determinant of the matrix
+ */
+ determinant: function M4_determinant(mat)
+ {
+ let a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3];
+ let a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7];
+ let a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11];
+ let a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15];
+
+ return a30 * a21 * a12 * a03 - a20 * a31 * a12 * a03 -
+ a30 * a11 * a22 * a03 + a10 * a31 * a22 * a03 +
+ a20 * a11 * a32 * a03 - a10 * a21 * a32 * a03 -
+ a30 * a21 * a02 * a13 + a20 * a31 * a02 * a13 +
+ a30 * a01 * a22 * a13 - a00 * a31 * a22 * a13 -
+ a20 * a01 * a32 * a13 + a00 * a21 * a32 * a13 +
+ a30 * a11 * a02 * a23 - a10 * a31 * a02 * a23 -
+ a30 * a01 * a12 * a23 + a00 * a31 * a12 * a23 +
+ a10 * a01 * a32 * a23 - a00 * a11 * a32 * a23 -
+ a20 * a11 * a02 * a33 + a10 * a21 * a02 * a33 +
+ a20 * a01 * a12 * a33 - a00 * a21 * a12 * a33 -
+ a10 * a01 * a22 * a33 + a00 * a11 * a22 * a33;
+ },
+
+ /**
+ * Calculate the inverse of a mat4.
+ *
+ * @param {Array} aMat
+ * mat4 to calculate inverse of
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ inverse: function M4_inverse(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+ let a30 = aMat[12], a31 = aMat[13], a32 = aMat[14], a33 = aMat[15];
+
+ let b00 = a00 * a11 - a01 * a10;
+ let b01 = a00 * a12 - a02 * a10;
+ let b02 = a00 * a13 - a03 * a10;
+ let b03 = a01 * a12 - a02 * a11;
+ let b04 = a01 * a13 - a03 * a11;
+ let b05 = a02 * a13 - a03 * a12;
+ let b06 = a20 * a31 - a21 * a30;
+ let b07 = a20 * a32 - a22 * a30;
+ let b08 = a20 * a33 - a23 * a30;
+ let b09 = a21 * a32 - a22 * a31;
+ let b10 = a21 * a33 - a23 * a31;
+ let b11 = a22 * a33 - a23 * a32;
+ let id = 1 / ((b00 * b11 - b01 * b10 + b02 * b09 +
+ b03 * b08 - b04 * b07 + b05 * b06) || EPSILON);
+
+ aDest[0] = ( a11 * b11 - a12 * b10 + a13 * b09) * id;
+ aDest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * id;
+ aDest[2] = ( a31 * b05 - a32 * b04 + a33 * b03) * id;
+ aDest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * id;
+ aDest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * id;
+ aDest[5] = ( a00 * b11 - a02 * b08 + a03 * b07) * id;
+ aDest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * id;
+ aDest[7] = ( a20 * b05 - a22 * b02 + a23 * b01) * id;
+ aDest[8] = ( a10 * b10 - a11 * b08 + a13 * b06) * id;
+ aDest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * id;
+ aDest[10] = ( a30 * b04 - a31 * b02 + a33 * b00) * id;
+ aDest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * id;
+ aDest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * id;
+ aDest[13] = ( a00 * b09 - a01 * b07 + a02 * b06) * id;
+ aDest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * id;
+ aDest[15] = ( a20 * b03 - a21 * b01 + a22 * b00) * id;
+ return aDest;
+ },
+
+ /**
+ * Copies the upper 3x3 elements of a mat4 into another mat4.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ toRotationMat: function M4_toRotationMat(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Copies the upper 3x3 elements of a mat4 into a mat3.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat3: function M4_toMat3(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[4];
+ aDest[4] = aMat[5];
+ aDest[5] = aMat[6];
+ aDest[6] = aMat[8];
+ aDest[7] = aMat[9];
+ aDest[8] = aMat[10];
+ return aDest;
+ },
+
+ /**
+ * Calculate the inverse of the upper 3x3 elements of a mat4 and copies
+ * the result into a mat3. The resulting matrix is useful for calculating
+ * transformed normals.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to invert and copy
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toInverseMat3: function M4_toInverseMat3(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10];
+
+ let b01 = a22 * a11 - a12 * a21;
+ let b11 = -a22 * a10 + a12 * a20;
+ let b21 = a21 * a10 - a11 * a20;
+ let id = 1 / ((a00 * b01 + a01 * b11 + a02 * b21) || EPSILON);
+
+ aDest[0] = b01 * id;
+ aDest[1] = (-a22 * a01 + a02 * a21) * id;
+ aDest[2] = ( a12 * a01 - a02 * a11) * id;
+ aDest[3] = b11 * id;
+ aDest[4] = ( a22 * a00 - a02 * a20) * id;
+ aDest[5] = (-a12 * a00 + a02 * a10) * id;
+ aDest[6] = b21 * id;
+ aDest[7] = (-a21 * a00 + a01 * a20) * id;
+ aDest[8] = ( a11 * a00 - a01 * a10) * id;
+ return aDest;
+ },
+
+ /**
+ * Performs a matrix multiplication.
+ *
+ * @param {Array} aMat
+ * first operand
+ * @param {Array} aMat2
+ * second operand
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ multiply: function M4_multiply(aMat, aMat2, aDest)
+ {
+ if (!aDest) {
+ aDest = aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+ let a30 = aMat[12], a31 = aMat[13], a32 = aMat[14], a33 = aMat[15];
+
+ let b00 = aMat2[0], b01 = aMat2[1], b02 = aMat2[2], b03 = aMat2[3];
+ let b10 = aMat2[4], b11 = aMat2[5], b12 = aMat2[6], b13 = aMat2[7];
+ let b20 = aMat2[8], b21 = aMat2[9], b22 = aMat2[10], b23 = aMat2[11];
+ let b30 = aMat2[12], b31 = aMat2[13], b32 = aMat2[14], b33 = aMat2[15];
+
+ aDest[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30;
+ aDest[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31;
+ aDest[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32;
+ aDest[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33;
+ aDest[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30;
+ aDest[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31;
+ aDest[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32;
+ aDest[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33;
+ aDest[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30;
+ aDest[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31;
+ aDest[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32;
+ aDest[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33;
+ aDest[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30;
+ aDest[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31;
+ aDest[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32;
+ aDest[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33;
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec3 with the given matrix.
+ * 4th vector component is implicitly 1.
+ *
+ * @param {Array} aMat
+ * mat4 to transform the vector with
+ * @param {Array} aVec
+ * vec3 to transform
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, aVec operand otherwise
+ */
+ multiplyVec3: function M4_multiplyVec3(aMat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ aDest[0] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12];
+ aDest[1] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13];
+ aDest[2] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14];
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec4 with the given matrix.
+ *
+ * @param {Array} aMat
+ * mat4 to transform the vector with
+ * @param {Array} aVec
+ * vec4 to transform
+ * @param {Array} aDest
+ * optional, vec4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec4 if specified, vec4 operand otherwise
+ */
+ multiplyVec4: function M4_multiplyVec4(aMat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let w = aVec[3];
+
+ aDest[0] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12] * w;
+ aDest[1] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13] * w;
+ aDest[2] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14] * w;
+ aDest[3] = aMat[3] * x + aMat[7] * y + aMat[11] * z + aMat[15] * w;
+ return aDest;
+ },
+
+ /**
+ * Translates a matrix by the given vector.
+ *
+ * @param {Array} aMat
+ * mat4 to translate
+ * @param {Array} aVec
+ * vec3 specifying the translation
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ translate: function M4_translate(aMat, aVec, aDest)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ if (!aDest || aMat === aDest) {
+ aMat[12] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12];
+ aMat[13] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13];
+ aMat[14] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14];
+ aMat[15] = aMat[3] * x + aMat[7] * y + aMat[11] * z + aMat[15];
+ return aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ aDest[0] = a00;
+ aDest[1] = a01;
+ aDest[2] = a02;
+ aDest[3] = a03;
+ aDest[4] = a10;
+ aDest[5] = a11;
+ aDest[6] = a12;
+ aDest[7] = a13;
+ aDest[8] = a20;
+ aDest[9] = a21;
+ aDest[10] = a22;
+ aDest[11] = a23;
+ aDest[12] = a00 * x + a10 * y + a20 * z + aMat[12];
+ aDest[13] = a01 * x + a11 * y + a21 * z + aMat[13];
+ aDest[14] = a02 * x + a12 * y + a22 * z + aMat[14];
+ aDest[15] = a03 * x + a13 * y + a23 * z + aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Scales a matrix by the given vector.
+ *
+ * @param {Array} aMat
+ * mat4 to translate
+ * @param {Array} aVec
+ * vec3 specifying the scale on each axis
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ scale: function M4_scale(aMat, aVec, aDest)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ if (!aDest || aMat === aDest) {
+ aMat[0] *= x;
+ aMat[1] *= x;
+ aMat[2] *= x;
+ aMat[3] *= x;
+ aMat[4] *= y;
+ aMat[5] *= y;
+ aMat[6] *= y;
+ aMat[7] *= y;
+ aMat[8] *= z;
+ aMat[9] *= z;
+ aMat[10] *= z;
+ aMat[11] *= z;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0] * x;
+ aDest[1] = aMat[1] * x;
+ aDest[2] = aMat[2] * x;
+ aDest[3] = aMat[3] * x;
+ aDest[4] = aMat[4] * y;
+ aDest[5] = aMat[5] * y;
+ aDest[6] = aMat[6] * y;
+ aDest[7] = aMat[7] * y;
+ aDest[8] = aMat[8] * z;
+ aDest[9] = aMat[9] * z;
+ aDest[10] = aMat[10] * z;
+ aDest[11] = aMat[11] * z;
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the specified axis.
+ * If rotating around a primary axis (x, y, z) one of the specialized
+ * rotation functions should be used instead for performance,
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aAxis
+ * vec3 representing the axis to rotate around
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotate: function M4_rotate(aMat, aAngle, aAxis, aDest)
+ {
+ let x = aAxis[0];
+ let y = aAxis[1];
+ let z = aAxis[2];
+ let len = 1 / (Math.sqrt(x * x + y * y + z * z) || EPSILON);
+
+ x *= len;
+ y *= len;
+ z *= len;
+
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+ let t = 1 - c;
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ let b00 = x * x * t + c, b01 = y * x * t + z * s, b02 = z * x * t - y * s;
+ let b10 = x * y * t - z * s, b11 = y * y * t + c, b12 = z * y * t + x * s;
+ let b20 = x * z * t + y * s, b21 = y * z * t - x * s, b22 = z * z * t + c;
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * b00 + a10 * b01 + a20 * b02;
+ aDest[1] = a01 * b00 + a11 * b01 + a21 * b02;
+ aDest[2] = a02 * b00 + a12 * b01 + a22 * b02;
+ aDest[3] = a03 * b00 + a13 * b01 + a23 * b02;
+ aDest[4] = a00 * b10 + a10 * b11 + a20 * b12;
+ aDest[5] = a01 * b10 + a11 * b11 + a21 * b12;
+ aDest[6] = a02 * b10 + a12 * b11 + a22 * b12;
+ aDest[7] = a03 * b10 + a13 * b11 + a23 * b12;
+ aDest[8] = a00 * b20 + a10 * b21 + a20 * b22;
+ aDest[9] = a01 * b20 + a11 * b21 + a21 * b22;
+ aDest[10] = a02 * b20 + a12 * b21 + a22 * b22;
+ aDest[11] = a03 * b20 + a13 * b21 + a23 * b22;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the X axis.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateX: function M4_rotateX(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[4] = a10 * c + a20 * s;
+ aDest[5] = a11 * c + a21 * s;
+ aDest[6] = a12 * c + a22 * s;
+ aDest[7] = a13 * c + a23 * s;
+ aDest[8] = a10 * -s + a20 * c;
+ aDest[9] = a11 * -s + a21 * c;
+ aDest[10] = a12 * -s + a22 * c;
+ aDest[11] = a13 * -s + a23 * c;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the Y axix.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateY: function M4_rotateY(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * c + a20 * -s;
+ aDest[1] = a01 * c + a21 * -s;
+ aDest[2] = a02 * c + a22 * -s;
+ aDest[3] = a03 * c + a23 * -s;
+ aDest[8] = a00 * s + a20 * c;
+ aDest[9] = a01 * s + a21 * c;
+ aDest[10] = a02 * s + a22 * c;
+ aDest[11] = a03 * s + a23 * c;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the Z axix.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateZ: function M4_rotateZ(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * c + a10 * s;
+ aDest[1] = a01 * c + a11 * s;
+ aDest[2] = a02 * c + a12 * s;
+ aDest[3] = a03 * c + a13 * s;
+ aDest[4] = a00 * -s + a10 * c;
+ aDest[5] = a01 * -s + a11 * c;
+ aDest[6] = a02 * -s + a12 * c;
+ aDest[7] = a03 * -s + a13 * c;
+ return aDest;
+ },
+
+ /**
+ * Generates a frustum matrix with the given bounds.
+ *
+ * @param {Number} aLeft
+ * scalar, left bound of the frustum
+ * @param {Number} aRight
+ * scalar, right bound of the frustum
+ * @param {Number} aBottom
+ * scalar, bottom bound of the frustum
+ * @param {Number} aTop
+ * scalar, top bound of the frustum
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ frustum: function M4_frustum(
+ aLeft, aRight, aBottom, aTop, aNear, aFar, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let rl = (aRight - aLeft);
+ let tb = (aTop - aBottom);
+ let fn = (aFar - aNear);
+
+ aDest[0] = (aNear * 2) / rl;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = (aNear * 2) / tb;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = (aRight + aLeft) / rl;
+ aDest[9] = (aTop + aBottom) / tb;
+ aDest[10] = -(aFar + aNear) / fn;
+ aDest[11] = -1;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = -(aFar * aNear * 2) / fn;
+ aDest[15] = 0;
+ return aDest;
+ },
+
+ /**
+ * Generates a perspective projection matrix with the given bounds.
+ *
+ * @param {Number} aFovy
+ * scalar, vertical field of view (degrees)
+ * @param {Number} aAspect
+ * scalar, aspect ratio (typically viewport width/height)
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ perspective: function M4_perspective(
+ aFovy, aAspect, aNear, aFar, aDest, aFlip)
+ {
+ let upper = aNear * Math.tan(aFovy * 0.00872664626); // PI * 180 / 2
+ let right = upper * aAspect;
+ let top = upper * (aFlip || 1);
+
+ return mat4.frustum(-right, right, -top, top, aNear, aFar, aDest);
+ },
+
+ /**
+ * Generates a orthogonal projection matrix with the given bounds.
+ *
+ * @param {Number} aLeft
+ * scalar, left bound of the frustum
+ * @param {Number} aRight
+ * scalar, right bound of the frustum
+ * @param {Number} aBottom
+ * scalar, bottom bound of the frustum
+ * @param {Number} aTop
+ * scalar, top bound of the frustum
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ ortho: function M4_ortho(aLeft, aRight, aBottom, aTop, aNear, aFar, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let rl = (aRight - aLeft);
+ let tb = (aTop - aBottom);
+ let fn = (aFar - aNear);
+
+ aDest[0] = 2 / rl;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = 2 / tb;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 0;
+ aDest[9] = 0;
+ aDest[10] = -2 / fn;
+ aDest[11] = 0;
+ aDest[12] = -(aLeft + aRight) / rl;
+ aDest[13] = -(aTop + aBottom) / tb;
+ aDest[14] = -(aFar + aNear) / fn;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Generates a look-at matrix with the given eye position, focal point, and
+ * up axis.
+ *
+ * @param {Array} aEye
+ * vec3, position of the viewer
+ * @param {Array} aCenter
+ * vec3, point the viewer is looking at
+ * @param {Array} aUp
+ * vec3 pointing up
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ lookAt: function M4_lookAt(aEye, aCenter, aUp, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let eyex = aEye[0];
+ let eyey = aEye[1];
+ let eyez = aEye[2];
+ let upx = aUp[0];
+ let upy = aUp[1];
+ let upz = aUp[2];
+ let centerx = aCenter[0];
+ let centery = aCenter[1];
+ let centerz = aCenter[2];
+
+ let z0 = eyex - aCenter[0];
+ let z1 = eyey - aCenter[1];
+ let z2 = eyez - aCenter[2];
+ let len = 1 / (Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2) || EPSILON);
+
+ z0 *= len;
+ z1 *= len;
+ z2 *= len;
+
+ let x0 = upy * z2 - upz * z1;
+ let x1 = upz * z0 - upx * z2;
+ let x2 = upx * z1 - upy * z0;
+ len = 1 / (Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2) || EPSILON);
+
+ x0 *= len;
+ x1 *= len;
+ x2 *= len;
+
+ let y0 = z1 * x2 - z2 * x1;
+ let y1 = z2 * x0 - z0 * x2;
+ let y2 = z0 * x1 - z1 * x0;
+ len = 1 / (Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2) || EPSILON);
+
+ y0 *= len;
+ y1 *= len;
+ y2 *= len;
+
+ aDest[0] = x0;
+ aDest[1] = y0;
+ aDest[2] = z0;
+ aDest[3] = 0;
+ aDest[4] = x1;
+ aDest[5] = y1;
+ aDest[6] = z1;
+ aDest[7] = 0;
+ aDest[8] = x2;
+ aDest[9] = y2;
+ aDest[10] = z2;
+ aDest[11] = 0;
+ aDest[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+ aDest[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+ aDest[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+ aDest[15] = 1;
+
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a 4x4 matrix.
+ *
+ * @param {Array} aMat
+ * mat4 to represent as a string
+ *
+ * @return {String} representation of the matrix
+ */
+ str: function M4_str(mat)
+ {
+ return "[" + mat[0] + ", " + mat[1] + ", " + mat[2] + ", " + mat[3] +
+ ", "+ mat[4] + ", " + mat[5] + ", " + mat[6] + ", " + mat[7] +
+ ", "+ mat[8] + ", " + mat[9] + ", " + mat[10] + ", " + mat[11] +
+ ", "+ mat[12] + ", " + mat[13] + ", " + mat[14] + ", " + mat[15] +
+ "]";
+ }
+};
+
+exports.mat4 = mat4;
+
+/**
+ * quat4 - Quaternion.
+ */
+let quat4 = {
+
+ /**
+ * Creates a new instance of a quat4 using the default Float32Array type.
+ * Any array containing at least 4 numeric elements can serve as a quat4.
+ *
+ * @param {Array} aQuat
+ * optional, quat4 containing values to initialize with
+ *
+ * @return {Array} a new instance of a quat4
+ */
+ create: function Q4_create(aQuat)
+ {
+ let dest = new Float32Array(4);
+
+ if (aQuat) {
+ quat4.set(aQuat, dest);
+ } else {
+ quat4.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one quat4 to another.
+ *
+ * @param {Array} aQuat
+ * quat4 containing values to copy
+ * @param {Array} aDest
+ * quat4 receiving copied values
+ *
+ * @return {Array} the destination quat4 receiving copied values
+ */
+ set: function Q4_set(aQuat, aDest)
+ {
+ aDest[0] = aQuat[0];
+ aDest[1] = aQuat[1];
+ aDest[2] = aQuat[2];
+ aDest[3] = aQuat[3];
+ return aDest;
+ },
+
+ /**
+ * Sets a quat4 to an identity quaternion.
+ *
+ * @param {Array} aDest
+ * quat4 to set
+ *
+ * @return {Array} the same quaternion
+ */
+ identity: function Q4_identity(aDest)
+ {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 1;
+ return aDest;
+ },
+
+ /**
+ * Calculate the W component of a quat4 from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate W component of
+ * @param {Array} aDest
+ * optional, quat4 receiving calculated values
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ calculateW: function Q4_calculateW(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+
+ aDest[0] = x;
+ aDest[1] = y;
+ aDest[2] = z;
+ aDest[3] = -Math.sqrt(Math.abs(1 - x * x - y * y - z * z));
+ return aDest;
+ },
+
+ /**
+ * Calculate the inverse of a quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate the inverse of
+ * @param {Array} aDest
+ * optional, quat4 receiving the inverse values
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ inverse: function Q4_inverse(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ aQuat[0] = -aQuat[0];
+ aQuat[1] = -aQuat[1];
+ aQuat[2] = -aQuat[2];
+ return aQuat;
+ },
+
+ /**
+ * Generates a unit quaternion of the same direction as the provided quat4.
+ * If quaternion length is 0, returns [0, 0, 0, 0].
+ *
+ * @param {Array} aQuat
+ * quat4 to normalize
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ normalize: function Q4_normalize(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+ let len = Math.sqrt(x * x + y * y + z * z + w * w);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ aDest[3] = w * len;
+ return aDest;
+ },
+
+ /**
+ * Calculate the length of a quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate the length of
+ *
+ * @return {Number} length of the quaternion
+ */
+ length: function Q4_length(aQuat)
+ {
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ return Math.sqrt(x * x + y * y + z * z + w * w);
+ },
+
+ /**
+ * Performs a quaternion multiplication.
+ *
+ * @param {Array} aQuat
+ * first operand
+ * @param {Array} aQuat2
+ * second operand
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ multiply: function Q4_multiply(aQuat, aQuat2, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let qax = aQuat[0];
+ let qay = aQuat[1];
+ let qaz = aQuat[2];
+ let qaw = aQuat[3];
+ let qbx = aQuat2[0];
+ let qby = aQuat2[1];
+ let qbz = aQuat2[2];
+ let qbw = aQuat2[3];
+
+ aDest[0] = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
+ aDest[1] = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
+ aDest[2] = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
+ aDest[3] = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec3 with the given quaternion.
+ *
+ * @param {Array} aQuat
+ * quat4 to transform the vector with
+ * @param {Array} aVec
+ * vec3 to transform
+ * @param {Array} aDest
+ * optional, vec3 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, aVec operand otherwise
+ */
+ multiplyVec3: function Q4_multiplyVec3(aQuat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ let qx = aQuat[0];
+ let qy = aQuat[1];
+ let qz = aQuat[2];
+ let qw = aQuat[3];
+
+ let ix = qw * x + qy * z - qz * y;
+ let iy = qw * y + qz * x - qx * z;
+ let iz = qw * z + qx * y - qy * x;
+ let iw = -qx * x - qy * y - qz * z;
+
+ aDest[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ aDest[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ aDest[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ return aDest;
+ },
+
+ /**
+ * Performs a spherical linear interpolation between two quat4.
+ *
+ * @param {Array} aQuat
+ * first quaternion
+ * @param {Array} aQuat2
+ * second quaternion
+ * @param {Number} aSlerp
+ * interpolation amount between the two inputs
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ slerp: function Q4_slerp(aQuat, aQuat2, aSlerp, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let cosHalfTheta = aQuat[0] * aQuat2[0] +
+ aQuat[1] * aQuat2[1] +
+ aQuat[2] * aQuat2[2] +
+ aQuat[3] * aQuat2[3];
+
+ if (Math.abs(cosHalfTheta) >= 1) {
+ aDest[0] = aQuat[0];
+ aDest[1] = aQuat[1];
+ aDest[2] = aQuat[2];
+ aDest[3] = aQuat[3];
+ return aDest;
+ }
+
+ let halfTheta = Math.acos(cosHalfTheta);
+ let sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);
+
+ if (Math.abs(sinHalfTheta) < EPSILON) {
+ aDest[0] = (aQuat[0] * 0.5 + aQuat2[0] * 0.5);
+ aDest[1] = (aQuat[1] * 0.5 + aQuat2[1] * 0.5);
+ aDest[2] = (aQuat[2] * 0.5 + aQuat2[2] * 0.5);
+ aDest[3] = (aQuat[3] * 0.5 + aQuat2[3] * 0.5);
+ return aDest;
+ }
+
+ let ratioA = Math.sin((1 - aSlerp) * halfTheta) / sinHalfTheta;
+ let ratioB = Math.sin(aSlerp * halfTheta) / sinHalfTheta;
+
+ aDest[0] = (aQuat[0] * ratioA + aQuat2[0] * ratioB);
+ aDest[1] = (aQuat[1] * ratioA + aQuat2[1] * ratioB);
+ aDest[2] = (aQuat[2] * ratioA + aQuat2[2] * ratioB);
+ aDest[3] = (aQuat[3] * ratioA + aQuat2[3] * ratioB);
+ return aDest;
+ },
+
+ /**
+ * Calculates a 3x3 matrix from the given quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to create matrix from
+ * @param {Array} aDest
+ * optional, mat3 receiving the initialization result
+ * if not specified, a new matrix is created
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat3: function Q4_toMat3(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ let x2 = x + x;
+ let y2 = y + y;
+ let z2 = z + z;
+ let xx = x * x2;
+ let xy = x * y2;
+ let xz = x * z2;
+ let yy = y * y2;
+ let yz = y * z2;
+ let zz = z * z2;
+ let wx = w * x2;
+ let wy = w * y2;
+ let wz = w * z2;
+
+ aDest[0] = 1 - (yy + zz);
+ aDest[1] = xy - wz;
+ aDest[2] = xz + wy;
+ aDest[3] = xy + wz;
+ aDest[4] = 1 - (xx + zz);
+ aDest[5] = yz - wx;
+ aDest[6] = xz - wy;
+ aDest[7] = yz + wx;
+ aDest[8] = 1 - (xx + yy);
+ return aDest;
+ },
+
+ /**
+ * Calculates a 4x4 matrix from the given quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to create matrix from
+ * @param {Array} aDest
+ * optional, mat4 receiving the initialization result
+ * if not specified, a new matrix is created
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ toMat4: function Q4_toMat4(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ let x2 = x + x;
+ let y2 = y + y;
+ let z2 = z + z;
+ let xx = x * x2;
+ let xy = x * y2;
+ let xz = x * z2;
+ let yy = y * y2;
+ let yz = y * z2;
+ let zz = z * z2;
+ let wx = w * x2;
+ let wy = w * y2;
+ let wz = w * z2;
+
+ aDest[0] = 1 - (yy + zz);
+ aDest[1] = xy - wz;
+ aDest[2] = xz + wy;
+ aDest[3] = 0;
+ aDest[4] = xy + wz;
+ aDest[5] = 1 - (xx + zz);
+ aDest[6] = yz - wx;
+ aDest[7] = 0;
+ aDest[8] = xz - wy;
+ aDest[9] = yz + wx;
+ aDest[10] = 1 - (xx + yy);
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Creates a rotation quaternion from axis-angle.
+ * This function expects that the axis is a normalized vector.
+ *
+ * @param {Array} aAxis
+ * an array of elements representing the [x, y, z] axis
+ * @param {Number} aAngle
+ * the angle of rotation
+ * @param {Array} aDest
+ * optional, quat4 receiving the initialization result
+ * if not specified, a new quaternion is created
+ *
+ * @return {Array} the quaternion as [x, y, z, w]
+ */
+ fromAxis: function Q4_fromAxis(aAxis, aAngle, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(4);
+ }
+
+ let ang = aAngle * 0.5;
+ let sin = Math.sin(ang);
+ let cos = Math.cos(ang);
+
+ aDest[0] = aAxis[0] * sin;
+ aDest[1] = aAxis[1] * sin;
+ aDest[2] = aAxis[2] * sin;
+ aDest[3] = cos;
+ return aDest;
+ },
+
+ /**
+ * Creates a rotation quaternion from Euler angles.
+ *
+ * @param {Number} aYaw
+ * the yaw angle of rotation
+ * @param {Number} aPitch
+ * the pitch angle of rotation
+ * @param {Number} aRoll
+ * the roll angle of rotation
+ * @param {Array} aDest
+ * optional, quat4 receiving the initialization result
+ * if not specified, a new quaternion is created
+ *
+ * @return {Array} the quaternion as [x, y, z, w]
+ */
+ fromEuler: function Q4_fromEuler(aYaw, aPitch, aRoll, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(4);
+ }
+
+ let x = aPitch * 0.5;
+ let y = aYaw * 0.5;
+ let z = aRoll * 0.5;
+
+ let sinr = Math.sin(x);
+ let sinp = Math.sin(y);
+ let siny = Math.sin(z);
+ let cosr = Math.cos(x);
+ let cosp = Math.cos(y);
+ let cosy = Math.cos(z);
+
+ aDest[0] = sinr * cosp * cosy - cosr * sinp * siny;
+ aDest[1] = cosr * sinp * cosy + sinr * cosp * siny;
+ aDest[2] = cosr * cosp * siny - sinr * sinp * cosy;
+ aDest[3] = cosr * cosp * cosy + sinr * sinp * siny;
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a quaternion.
+ *
+ * @param {Array} aQuat
+ * quat4 to represent as a string
+ *
+ * @return {String} representation of the quaternion
+ */
+ str: function Q4_str(aQuat) {
+ return "[" + aQuat[0] + ", " +
+ aQuat[1] + ", " +
+ aQuat[2] + ", " +
+ aQuat[3] + "]";
+ }
+};
+
+exports.quat4 = quat4;
+
+/**
+ * Various algebraic math functions required by the engine.
+ */
+let TiltMath = {
+
+ /**
+ * Helper function, converts degrees to radians.
+ *
+ * @param {Number} aDegrees
+ * the degrees to be converted to radians
+ *
+ * @return {Number} the degrees converted to radians
+ */
+ radians: function TM_radians(aDegrees)
+ {
+ return aDegrees * PI_OVER_180;
+ },
+
+ /**
+ * Helper function, converts radians to degrees.
+ *
+ * @param {Number} aRadians
+ * the radians to be converted to degrees
+ *
+ * @return {Number} the radians converted to degrees
+ */
+ degrees: function TM_degrees(aRadians)
+ {
+ return aRadians * INV_PI_OVER_180;
+ },
+
+ /**
+ * Re-maps a number from one range to another.
+ *
+ * @param {Number} aValue
+ * the number to map
+ * @param {Number} aLow1
+ * the normal lower bound of the number
+ * @param {Number} aHigh1
+ * the normal upper bound of the number
+ * @param {Number} aLow2
+ * the new lower bound of the number
+ * @param {Number} aHigh2
+ * the new upper bound of the number
+ *
+ * @return {Number} the remapped number
+ */
+ map: function TM_map(aValue, aLow1, aHigh1, aLow2, aHigh2)
+ {
+ return aLow2 + (aHigh2 - aLow2) * ((aValue - aLow1) / (aHigh1 - aLow1));
+ },
+
+ /**
+ * Returns if number is power of two.
+ *
+ * @param {Number} aNumber
+ * the number to be verified
+ *
+ * @return {Boolean} true if x is power of two
+ */
+ isPowerOfTwo: function TM_isPowerOfTwo(aNumber)
+ {
+ return !(aNumber & (aNumber - 1));
+ },
+
+ /**
+ * Returns the next closest power of two greater than a number.
+ *
+ * @param {Number} aNumber
+ * the number to be converted
+ *
+ * @return {Number} the next closest power of two for x
+ */
+ nextPowerOfTwo: function TM_nextPowerOfTwo(aNumber)
+ {
+ --aNumber;
+
+ for (let i = 1; i < 32; i <<= 1) {
+ aNumber = aNumber | aNumber >> i;
+ }
+ return aNumber + 1;
+ },
+
+ /**
+ * A convenient way of limiting values to a set boundary.
+ *
+ * @param {Number} aValue
+ * the number to be limited
+ * @param {Number} aMin
+ * the minimum allowed value for the number
+ * @param {Number} aMax
+ * the maximum allowed value for the number
+ */
+ clamp: function TM_clamp(aValue, aMin, aMax)
+ {
+ return Math.max(aMin, Math.min(aMax, aValue));
+ },
+
+ /**
+ * Convenient way to clamp a value to 0..1
+ *
+ * @param {Number} aValue
+ * the number to be limited
+ */
+ saturate: function TM_saturate(aValue)
+ {
+ return Math.max(0, Math.min(1, aValue));
+ },
+
+ /**
+ * Converts a hex color to rgba.
+ * If the passed param is invalid, it will be converted to [0, 0, 0, 1];
+ *
+ * @param {String} aColor
+ * color expressed in hex, or using rgb() or rgba()
+ *
+ * @return {Array} with 4 color 0..1 components: [red, green, blue, alpha]
+ */
+ hex2rgba: (function()
+ {
+ let cache = {};
+
+ return function TM_hex2rgba(aColor) {
+ let hex = aColor.charAt(0) === "#" ? aColor.substring(1) : aColor;
+
+ // check the cache to see if this color wasn't converted already
+ if (cache[hex] !== undefined) {
+ return cache[hex];
+ }
+
+ // e.g. "f00"
+ if (hex.length === 3) {
+ let r = parseInt(hex.substring(0, 1), 16) * FIFTEEN_OVER_225;
+ let g = parseInt(hex.substring(1, 2), 16) * FIFTEEN_OVER_225;
+ let b = parseInt(hex.substring(2, 3), 16) * FIFTEEN_OVER_225;
+
+ return (cache[hex] = [r, g, b, 1]);
+ }
+ // e.g. "f008"
+ if (hex.length === 4) {
+ let r = parseInt(hex.substring(0, 1), 16) * FIFTEEN_OVER_225;
+ let g = parseInt(hex.substring(1, 2), 16) * FIFTEEN_OVER_225;
+ let b = parseInt(hex.substring(2, 3), 16) * FIFTEEN_OVER_225;
+ let a = parseInt(hex.substring(3, 4), 16) * FIFTEEN_OVER_225;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g. "ff0000"
+ if (hex.length === 6) {
+ let r = parseInt(hex.substring(0, 2), 16) * ONE_OVER_255;
+ let g = parseInt(hex.substring(2, 4), 16) * ONE_OVER_255;
+ let b = parseInt(hex.substring(4, 6), 16) * ONE_OVER_255;
+ let a = 1;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g "ff0000aa"
+ if (hex.length === 8) {
+ let r = parseInt(hex.substring(0, 2), 16) * ONE_OVER_255;
+ let g = parseInt(hex.substring(2, 4), 16) * ONE_OVER_255;
+ let b = parseInt(hex.substring(4, 6), 16) * ONE_OVER_255;
+ let a = parseInt(hex.substring(6, 8), 16) * ONE_OVER_255;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g. "rgba(255, 0, 0, 0.5)"
+ if (hex.match("^rgba")) {
+ let rgba = hex.substring(5, hex.length - 1).split(",");
+ rgba[0] *= ONE_OVER_255;
+ rgba[1] *= ONE_OVER_255;
+ rgba[2] *= ONE_OVER_255;
+ // in CSS, the alpha component of rgba() is already in the range 0..1
+
+ return (cache[hex] = rgba);
+ }
+ // e.g. "rgb(255, 0, 0)"
+ if (hex.match("^rgb")) {
+ let rgba = hex.substring(4, hex.length - 1).split(",");
+ rgba[0] *= ONE_OVER_255;
+ rgba[1] *= ONE_OVER_255;
+ rgba[2] *= ONE_OVER_255;
+ rgba[3] = 1;
+
+ return (cache[hex] = rgba);
+ }
+
+ // your argument is invalid
+ return (cache[hex] = [0, 0, 0, 1]);
+ };
+ }())
+};
+
+exports.TiltMath = TiltMath;
+
+// bind the owner object to the necessary functions
+TiltUtils.bindObjectFunc(vec3);
+TiltUtils.bindObjectFunc(mat3);
+TiltUtils.bindObjectFunc(mat4);
+TiltUtils.bindObjectFunc(quat4);
+TiltUtils.bindObjectFunc(TiltMath);
diff --git a/browser/devtools/tilt/tilt-utils.js b/browser/devtools/tilt/tilt-utils.js
new file mode 100644
index 000000000..1309f24f4
--- /dev/null
+++ b/browser/devtools/tilt/tilt-utils.js
@@ -0,0 +1,612 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 {Cc, Ci, Cu} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+const STACK_THICKNESS = 15;
+
+/**
+ * Module containing various helper functions used throughout Tilt.
+ */
+this.TiltUtils = {};
+module.exports = this.TiltUtils;
+
+/**
+ * Various console/prompt output functions required by the engine.
+ */
+TiltUtils.Output = {
+
+ /**
+ * Logs a message to the console.
+ *
+ * @param {String} aMessage
+ * the message to be logged
+ */
+ log: function TUO_log(aMessage)
+ {
+ if (this.suppressLogs) {
+ return;
+ }
+ // get the console service
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+
+ // log the message
+ consoleService.logStringMessage(aMessage);
+ },
+
+ /**
+ * Logs an error to the console.
+ *
+ * @param {String} aMessage
+ * the message to be logged
+ * @param {Object} aProperties
+ * and object containing script error initialization details
+ */
+ error: function TUO_error(aMessage, aProperties)
+ {
+ if (this.suppressErrors) {
+ return;
+ }
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // get the console service
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+
+ // get the script error service
+ let scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+
+ // initialize a script error
+ scriptError.init(aMessage,
+ aProperties.sourceName || "",
+ aProperties.sourceLine || "",
+ aProperties.lineNumber || 0,
+ aProperties.columnNumber || 0,
+ aProperties.flags || 0,
+ aProperties.category || "");
+
+ // log the error
+ consoleService.logMessage(scriptError);
+ },
+
+ /**
+ * Shows a modal alert message popup.
+ *
+ * @param {String} aTitle
+ * the title of the popup
+ * @param {String} aMessage
+ * the message to be logged
+ */
+ alert: function TUO_alert(aTitle, aMessage)
+ {
+ if (this.suppressAlerts) {
+ return;
+ }
+ if (!aMessage) {
+ aMessage = aTitle;
+ aTitle = "";
+ }
+
+ // get the prompt service
+ let prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+ // show the alert message
+ prompt.alert(null, aTitle, aMessage);
+ }
+};
+
+/**
+ * Helper functions for managing preferences.
+ */
+TiltUtils.Preferences = {
+
+ /**
+ * Gets a custom Tilt preference.
+ * If the preference does not exist, undefined is returned. If it does exist,
+ * but the type is not correctly specified, null is returned.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ *
+ * @return {Boolean | String | Number} the requested preference
+ */
+ get: function TUP_get(aPref, aType)
+ {
+ if (!aPref || !aType) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ switch(aType) {
+ case "boolean":
+ return prefs.getBoolPref(aPref);
+ case "string":
+ return prefs.getCharPref(aPref);
+ case "integer":
+ return prefs.getIntPref(aPref);
+ }
+ return null;
+
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ return undefined;
+ }
+ },
+
+ /**
+ * Sets a custom Tilt preference.
+ * If the preference already exists, it is overwritten.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ * @param {String} aValue
+ * a new preference value
+ *
+ * @return {Boolean} true if the preference was set successfully
+ */
+ set: function TUP_set(aPref, aType, aValue)
+ {
+ if (!aPref || !aType || aValue === undefined || aValue === null) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ switch(aType) {
+ case "boolean":
+ return prefs.setBoolPref(aPref, aValue);
+ case "string":
+ return prefs.setCharPref(aPref, aValue);
+ case "integer":
+ return prefs.setIntPref(aPref, aValue);
+ }
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ }
+ return false;
+ },
+
+ /**
+ * Creates a custom Tilt preference.
+ * If the preference already exists, it is left unchanged.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ * @param {String} aValue
+ * the initial preference value
+ *
+ * @return {Boolean} true if the preference was initialized successfully
+ */
+ create: function TUP_create(aPref, aType, aValue)
+ {
+ if (!aPref || !aType || aValue === undefined || aValue === null) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ if (!prefs.prefHasUserValue(aPref)) {
+ switch(aType) {
+ case "boolean":
+ return prefs.setBoolPref(aPref, aValue);
+ case "string":
+ return prefs.setCharPref(aPref, aValue);
+ case "integer":
+ return prefs.setIntPref(aPref, aValue);
+ }
+ }
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ }
+ return false;
+ },
+
+ /**
+ * The preferences branch for this extension.
+ */
+ _branch: (function(aBranch) {
+ return Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .getBranch(aBranch);
+
+ }("devtools.tilt."))
+};
+
+/**
+ * Easy way to access the string bundle.
+ */
+TiltUtils.L10n = {
+
+ /**
+ * The string bundle element.
+ */
+ stringBundle: null,
+
+ /**
+ * Returns a string in the string bundle.
+ * If the string bundle is not found, null is returned.
+ *
+ * @param {String} aName
+ * the string name in the bundle
+ *
+ * @return {String} the equivalent string from the bundle
+ */
+ get: function TUL_get(aName)
+ {
+ // check to see if the parent string bundle document element is valid
+ if (!this.stringBundle || !aName) {
+ return null;
+ }
+ return this.stringBundle.GetStringFromName(aName);
+ },
+
+ /**
+ * Returns a formatted string using the string bundle.
+ * If the string bundle is not found, null is returned.
+ *
+ * @param {String} aName
+ * the string name in the bundle
+ * @param {Array} aArgs
+ * an array of arguments for the formatted string
+ *
+ * @return {String} the equivalent formatted string from the bundle
+ */
+ format: function TUL_format(aName, aArgs)
+ {
+ // check to see if the parent string bundle document element is valid
+ if (!this.stringBundle || !aName || !aArgs) {
+ return null;
+ }
+ return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
+ }
+};
+
+/**
+ * Utilities for accessing and manipulating a document.
+ */
+TiltUtils.DOM = {
+
+ /**
+ * Current parent node object used when creating canvas elements.
+ */
+ parentNode: null,
+
+ /**
+ * Helper method, allowing to easily create and manage a canvas element.
+ * If the width and height params are falsy, they default to the parent node
+ * client width and height.
+ *
+ * @param {Document} aParentNode
+ * the parent node used to create the canvas
+ * if not specified, it will be reused from the cache
+ * @param {Object} aProperties
+ * optional, object containing some of the following props:
+ * {Boolean} focusable
+ * optional, true to make the canvas focusable
+ * {Boolean} append
+ * optional, true to append the canvas to the parent node
+ * {Number} width
+ * optional, specifies the width of the canvas
+ * {Number} height
+ * optional, specifies the height of the canvas
+ * {String} id
+ * optional, id for the created canvas element
+ *
+ * @return {HTMLCanvasElement} the newly created canvas element
+ */
+ initCanvas: function TUD_initCanvas(aParentNode, aProperties)
+ {
+ // check to see if the parent node element is valid
+ if (!(aParentNode = aParentNode || this.parentNode)) {
+ return null;
+ }
+
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // cache this parent node so that it can be reused
+ this.parentNode = aParentNode;
+
+ // create the canvas element
+ let canvas = aParentNode.ownerDocument.
+ createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+
+ let width = aProperties.width || aParentNode.clientWidth;
+ let height = aProperties.height || aParentNode.clientHeight;
+ let id = aProperties.id || null;
+
+ canvas.setAttribute("style", "min-width: 1px; min-height: 1px;");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ canvas.setAttribute("id", id);
+
+ // the canvas is unfocusable by default, we may require otherwise
+ if (aProperties.focusable) {
+ canvas.setAttribute("tabindex", "1");
+ canvas.style.outline = "none";
+ }
+
+ // append the canvas element to the current parent node, if specified
+ if (aProperties.append) {
+ aParentNode.appendChild(canvas);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Gets the full webpage dimensions (width and height).
+ *
+ * @param {Window} aContentWindow
+ * the content window holding the document
+ *
+ * @return {Object} an object containing the width and height coords
+ */
+ getContentWindowDimensions: function TUD_getContentWindowDimensions(
+ aContentWindow)
+ {
+ return {
+ width: aContentWindow.innerWidth + aContentWindow.scrollMaxX,
+ height: aContentWindow.innerHeight + aContentWindow.scrollMaxY
+ };
+ },
+
+ /**
+ * Calculates the position and depth to display a node, this can be overriden
+ * to change the visualization.
+ *
+ * @param {Window} aContentWindow
+ * the window content holding the document
+ * @param {Node} aNode
+ * the node to get the position for
+ * @param {Object} aParentPosition
+ * the position of the parent node, as returned by this
+ * function
+ *
+ * @return {Object} an object describing the node's position in 3D space
+ * containing the following properties:
+ * {Number} top
+ * distance along the x axis
+ * {Number} left
+ * distance along the y axis
+ * {Number} depth
+ * distance along the z axis
+ * {Number} width
+ * width of the node
+ * {Number} height
+ * height of the node
+ * {Number} thickness
+ * thickness of the node
+ */
+ getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
+ aParentPosition) {
+ // get the x, y, width and height coordinates of the node
+ let coord = LayoutHelpers.getRect(aNode, aContentWindow);
+ if (!coord) {
+ return null;
+ }
+
+ coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
+ coord.thickness = STACK_THICKNESS;
+
+ return coord;
+ },
+
+ /**
+ * Traverses a document object model & calculates useful info for each node.
+ *
+ * @param {Window} aContentWindow
+ * the window content holding the document
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {Function} nodeCallback
+ * a function to call instead of TiltUtils.DOM.getNodePosition
+ * to get the position and depth to display nodes
+ * {Object} invisibleElements
+ * elements which should be ignored
+ * {Number} minSize
+ * the minimum dimensions needed for a node to be traversed
+ * {Number} maxX
+ * the maximum left position of an element
+ * {Number} maxY
+ * the maximum top position of an element
+ *
+ * @return {Array} list containing nodes positions and local names
+ */
+ traverse: function TUD_traverse(aContentWindow, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ let aInvisibleElements = aProperties.invisibleElements || {};
+ let aMinSize = aProperties.minSize || -1;
+ let aMaxX = aProperties.maxX || Number.MAX_VALUE;
+ let aMaxY = aProperties.maxY || Number.MAX_VALUE;
+
+ let nodeCallback = aProperties.nodeCallback || this.getNodePosition.bind(this);
+
+ let nodes = aContentWindow.document.childNodes;
+ let store = { info: [], nodes: [] };
+ let depth = 0;
+
+ let queue = [
+ { parentPosition: null, nodes: aContentWindow.document.childNodes }
+ ]
+
+ while (queue.length) {
+ let { nodes, parentPosition } = queue.shift();
+
+ for (let node of nodes) {
+ // skip some nodes to avoid visualization meshes that are too bloated
+ let name = node.localName;
+ if (!name || aInvisibleElements[name]) {
+ continue;
+ }
+
+ let coord = nodeCallback(aContentWindow, node, parentPosition);
+ if (!coord) {
+ continue;
+ }
+
+ // the maximum size slices the traversal where needed
+ if (coord.left > aMaxX || coord.top > aMaxY) {
+ continue;
+ }
+
+ // use this node only if it actually has visible dimensions
+ if (coord.width > aMinSize && coord.height > aMinSize) {
+
+ // save the necessary details into a list to be returned later
+ store.info.push({ coord: coord, name: name });
+ store.nodes.push(node);
+ }
+
+ let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes;
+ if (childNodes.length > 0)
+ queue.push({ parentPosition: coord, nodes: childNodes });
+ }
+ }
+
+ return store;
+ }
+};
+
+/**
+ * Binds a new owner object to the child functions.
+ * If the new parent is not specified, it will default to the passed scope.
+ *
+ * @param {Object} aScope
+ * the object from which all functions will be rebound
+ * @param {String} aRegex
+ * a regular expression to identify certain functions
+ * @param {Object} aParent
+ * the new parent for the object's functions
+ */
+TiltUtils.bindObjectFunc = function TU_bindObjectFunc(aScope, aRegex, aParent)
+{
+ if (!aScope) {
+ return;
+ }
+
+ for (let i in aScope) {
+ try {
+ if ("function" === typeof aScope[i] && (aRegex ? i.match(aRegex) : 1)) {
+ aScope[i] = aScope[i].bind(aParent || aScope);
+ }
+ } catch(e) {
+ TiltUtils.Output.error(e);
+ }
+ }
+};
+
+/**
+ * Destroys an object and deletes all members.
+ *
+ * @param {Object} aScope
+ * the object from which all children will be destroyed
+ */
+TiltUtils.destroyObject = function TU_destroyObject(aScope)
+{
+ if (!aScope) {
+ return;
+ }
+
+ // objects in Tilt usually use a function to handle internal destruction
+ if ("function" === typeof aScope._finalize) {
+ aScope._finalize();
+ }
+ for (let i in aScope) {
+ if (aScope.hasOwnProperty(i)) {
+ delete aScope[i];
+ }
+ }
+};
+
+/**
+ * Retrieve the unique ID of a window object.
+ *
+ * @param {Window} aWindow
+ * the window to get the ID from
+ *
+ * @return {Number} the window ID
+ */
+TiltUtils.getWindowId = function TU_getWindowId(aWindow)
+{
+ if (!aWindow) {
+ return;
+ }
+
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+};
+
+/**
+ * Sets the markup document viewer zoom for the currently selected browser.
+ *
+ * @param {Window} aChromeWindow
+ * the top-level browser window
+ *
+ * @param {Number} the zoom ammount
+ */
+TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) {
+ aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
+};
+
+/**
+ * Performs a garbage collection.
+ *
+ * @param {Window} aChromeWindow
+ * the top-level browser window
+ */
+TiltUtils.gc = function TU_gc(aChromeWindow)
+{
+ aChromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+};
+
+/**
+ * Clears the cache and sets all the variables to null.
+ */
+TiltUtils.clearCache = function TU_clearCache()
+{
+ TiltUtils.DOM.parentNode = null;
+};
+
+// bind the owner object to the necessary functions
+TiltUtils.bindObjectFunc(TiltUtils.Output);
+TiltUtils.bindObjectFunc(TiltUtils.Preferences);
+TiltUtils.bindObjectFunc(TiltUtils.L10n);
+TiltUtils.bindObjectFunc(TiltUtils.DOM);
+
+// set the necessary string bundle
+XPCOMUtils.defineLazyGetter(TiltUtils.L10n, "stringBundle", function() {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/tilt.properties");
+});
diff --git a/browser/devtools/tilt/tilt-visualizer-style.js b/browser/devtools/tilt/tilt-visualizer-style.js
new file mode 100644
index 000000000..7078a02dd
--- /dev/null
+++ b/browser/devtools/tilt/tilt-visualizer-style.js
@@ -0,0 +1,46 @@
+/* -*- Mode: javascript, tab-width: 2, indent-tabs-mode: nil, c-basic-offset: 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";
+
+let {TiltMath} = require("devtools/tilt/tilt-math");
+let rgba = TiltMath.hex2rgba;
+
+/**
+ * Various colors and style settings used throughout Tilt.
+ */
+module.exports = {
+ canvas: {
+ background: "linear-gradient(#454545 0%, #000 100%)",
+ },
+
+ nodes: {
+ highlight: {
+ defaultFill: rgba("#555"),
+ defaultStroke: rgba("#000"),
+ defaultStrokeWeight: 1
+ },
+
+ html: rgba("#8880"),
+ body: rgba("#fff0"),
+ h1: rgba("#e667af"),
+ h2: rgba("#c667af"),
+ h3: rgba("#a667af"),
+ h4: rgba("#8667af"),
+ h5: rgba("#8647af"),
+ h6: rgba("#8627af"),
+ div: rgba("#5dc8cd"),
+ span: rgba("#67e46f"),
+ table: rgba("#ff0700"),
+ tr: rgba("#ff4540"),
+ td: rgba("#ff7673"),
+ ul: rgba("#4671d5"),
+ li: rgba("#6c8cd5"),
+ p: rgba("#aaa"),
+ a: rgba("#123eab"),
+ img: rgba("#ffb473"),
+ iframe: rgba("#85004b")
+ }
+};
diff --git a/browser/devtools/tilt/tilt-visualizer.js b/browser/devtools/tilt/tilt-visualizer.js
new file mode 100644
index 000000000..0fa1bee40
--- /dev/null
+++ b/browser/devtools/tilt/tilt-visualizer.js
@@ -0,0 +1,2260 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 {Cu, Ci, ChromeWorker} = require("chrome");
+
+let TiltGL = require("devtools/tilt/tilt-gl");
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let TiltVisualizerStyle = require("devtools/tilt/tilt-visualizer-style");
+let {EPSILON, TiltMath, vec3, mat4, quat4} = require("devtools/tilt/tilt-math");
+let {TargetFactory} = require("devtools/framework/target");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+const ELEMENT_MIN_SIZE = 4;
+const INVISIBLE_ELEMENTS = {
+ "head": true,
+ "base": true,
+ "basefont": true,
+ "isindex": true,
+ "link": true,
+ "meta": true,
+ "option": true,
+ "script": true,
+ "style": true,
+ "title": true
+};
+
+// a node is represented in the visualization mesh as a rectangular stack
+// of 5 quads composed of 12 vertices; we draw these as triangles using an
+// index buffer of 12 unsigned int elements, obviously one for each vertex;
+// if a webpage has enough nodes to overflow the index buffer elements size,
+// weird things may happen; thus, when necessary, we'll split into groups
+const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
+
+const WIREFRAME_COLOR = [0, 0, 0, 0.25];
+const INTRO_TRANSITION_DURATION = 1000;
+const OUTRO_TRANSITION_DURATION = 800;
+const INITIAL_Z_TRANSLATION = 400;
+const MOVE_INTO_VIEW_ACCURACY = 50;
+
+const MOUSE_CLICK_THRESHOLD = 10;
+const MOUSE_INTRO_DELAY = 200;
+const ARCBALL_SENSITIVITY = 0.5;
+const ARCBALL_ROTATION_STEP = 0.15;
+const ARCBALL_TRANSLATION_STEP = 35;
+const ARCBALL_ZOOM_STEP = 0.1;
+const ARCBALL_ZOOM_MIN = -3000;
+const ARCBALL_ZOOM_MAX = 500;
+const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1;
+const ARCBALL_RESET_LINEAR_FACTOR = 0.01;
+
+const TILT_CRAFTER = "resource:///modules/devtools/tilt/TiltWorkerCrafter.js";
+const TILT_PICKER = "resource:///modules/devtools/tilt/TiltWorkerPicker.js";
+
+
+/**
+ * Initializes the visualization presenter and controller.
+ *
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Window} chromeWindow: a reference to the top level window
+ * {Window} contentWindow: the content window holding the visualized doc
+ * {Element} parentNode: the parent node to hold the visualization
+ * {Object} notifications: necessary notifications for Tilt
+ * {Function} onError: optional, function called if initialization failed
+ * {Function} onLoad: optional, function called if initialization worked
+ */
+function TiltVisualizer(aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * Save a reference to the top-level window.
+ */
+ this.chromeWindow = aProperties.chromeWindow;
+ this.tab = aProperties.tab;
+
+ /**
+ * The canvas element used for rendering the visualization.
+ */
+ this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, {
+ focusable: true,
+ append: true
+ });
+
+ /**
+ * Visualization logic and drawing loop.
+ */
+ this.presenter = new TiltVisualizer.Presenter(this.canvas,
+ aProperties.chromeWindow,
+ aProperties.contentWindow,
+ aProperties.notifications,
+ aProperties.onError || null,
+ aProperties.onLoad || null);
+
+ /**
+ * Visualization mouse and keyboard controller.
+ */
+ this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter);
+}
+
+exports.TiltVisualizer = TiltVisualizer;
+
+TiltVisualizer.prototype = {
+
+ /**
+ * Initializes the visualizer.
+ */
+ init: function TV_init()
+ {
+ this.presenter.init();
+ this.bindToInspector(this.tab);
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TV_isInitialized()
+ {
+ return this.presenter && this.presenter.isInitialized() &&
+ this.controller && this.controller.isInitialized();
+ },
+
+ /**
+ * Removes the overlay canvas used for rendering the visualization.
+ */
+ removeOverlay: function TV_removeOverlay()
+ {
+ if (this.canvas && this.canvas.parentNode) {
+ this.canvas.parentNode.removeChild(this.canvas);
+ }
+ },
+
+ /**
+ * Explicitly cleans up this visualizer and sets everything to null.
+ */
+ cleanup: function TV_cleanup()
+ {
+ this.unbindInspector();
+
+ if (this.controller) {
+ TiltUtils.destroyObject(this.controller);
+ }
+ if (this.presenter) {
+ TiltUtils.destroyObject(this.presenter);
+ }
+
+ let chromeWindow = this.chromeWindow;
+
+ TiltUtils.destroyObject(this);
+ TiltUtils.clearCache();
+ TiltUtils.gc(chromeWindow);
+ },
+
+ /**
+ * Listen to the inspector activity.
+ */
+ bindToInspector: function TV_bindToInspector(aTab)
+ {
+ this._browserTab = aTab;
+
+ this.onNewNodeFromInspector = this.onNewNodeFromInspector.bind(this);
+ this.onNewNodeFromTilt = this.onNewNodeFromTilt.bind(this);
+ this.onInspectorReady = this.onInspectorReady.bind(this);
+ this.onToolboxDestroyed = this.onToolboxDestroyed.bind(this);
+
+ gDevTools.on("inspector-ready", this.onInspectorReady);
+ gDevTools.on("toolbox-destroyed", this.onToolboxDestroyed);
+
+ Services.obs.addObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.HIGHLIGHTING,
+ false);
+ Services.obs.addObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.UNHIGHLIGHTING,
+ false);
+
+ let target = TargetFactory.forTab(aTab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ let panel = toolbox.getPanel("inspector");
+ if (panel) {
+ this.inspector = panel;
+ this.inspector.selection.on("new-node", this.onNewNodeFromInspector);
+ this.onNewNodeFromInspector();
+ }
+ }
+ },
+
+ /**
+ * Unregister inspector event listeners.
+ */
+ unbindInspector: function TV_unbindInspector()
+ {
+ this._browserTab = null;
+
+ if (this.inspector) {
+ if (this.inspector.selection) {
+ this.inspector.selection.off("new-node", this.onNewNodeFromInspector);
+ }
+ this.inspector = null;
+ }
+
+ gDevTools.off("inspector-ready", this.onInspectorReady);
+ gDevTools.off("toolbox-destroyed", this.onToolboxDestroyed);
+
+ Services.obs.removeObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.HIGHLIGHTING);
+ Services.obs.removeObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.UNHIGHLIGHTING);
+ },
+
+ /**
+ * When a new inspector is started.
+ */
+ onInspectorReady: function TV_onInspectorReady(event, toolbox, panel)
+ {
+ if (toolbox.target.tab === this._browserTab) {
+ this.inspector = panel;
+ this.inspector.selection.on("new-node", this.onNewNodeFromInspector);
+ this.onNewNodeFromTilt();
+ }
+ },
+
+ /**
+ * When the toolbox, therefor the inspector, is closed.
+ */
+ onToolboxDestroyed: function TV_onToolboxDestroyed(event, tab)
+ {
+ if (tab === this._browserTab &&
+ this.inspector) {
+ if (this.inspector.selection) {
+ this.inspector.selection.off("new-node", this.onNewNodeFromInspector);
+ }
+ this.inspector = null;
+ }
+ },
+
+ /**
+ * When a new node is selected in the inspector.
+ */
+ onNewNodeFromInspector: function TV_onNewNodeFromInspector()
+ {
+ if (this.inspector &&
+ this.inspector.selection.reason != "tilt") {
+ let selection = this.inspector.selection;
+ let canHighlightNode = selection.isNode() &&
+ selection.isConnected() &&
+ selection.isElementNode();
+ if (canHighlightNode) {
+ this.presenter.highlightNode(selection.node);
+ } else {
+ this.presenter.highlightNodeFor(-1);
+ }
+ }
+ },
+
+ /**
+ * When a new node is selected in Tilt.
+ */
+ onNewNodeFromTilt: function TV_onNewNodeFromTilt()
+ {
+ if (!this.inspector) {
+ return;
+ }
+ let nodeIndex = this.presenter._currentSelection;
+ if (nodeIndex < 0) {
+ this.inspector.selection.setNode(null, "tilt");
+ }
+ let node = this.presenter._traverseData.nodes[nodeIndex];
+ this.inspector.selection.setNode(node, "tilt");
+ },
+};
+
+/**
+ * This object manages the visualization logic and drawing loop.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas element used for rendering
+ * @param {Window} aChromeWindow
+ * a reference to the top-level window
+ * @param {Window} aContentWindow
+ * the content window holding the document to be visualized
+ * @param {Object} aNotifications
+ * necessary notifications for Tilt
+ * @param {Function} onError
+ * function called if initialization failed
+ * @param {Function} onLoad
+ * function called if initialization worked
+ */
+TiltVisualizer.Presenter = function TV_Presenter(
+ aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad)
+{
+ /**
+ * A canvas overlay used for drawing the visualization.
+ */
+ this.canvas = aCanvas;
+
+ /**
+ * Save a reference to the top-level window, to access Tilt.
+ */
+ this.chromeWindow = aChromeWindow;
+
+ /**
+ * The content window generating the visualization
+ */
+ this.contentWindow = aContentWindow;
+
+ /**
+ * Shortcut for accessing notifications strings.
+ */
+ this.NOTIFICATIONS = aNotifications;
+
+ /**
+ * Use the default node callback function
+ */
+ this.nodeCallback = null;
+
+ /**
+ * Create the renderer, containing useful functions for easy drawing.
+ */
+ this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad);
+
+ /**
+ * A custom shader used for drawing the visualization mesh.
+ */
+ this._visualizationProgram = null;
+
+ /**
+ * The combined mesh representing the document visualization.
+ */
+ this._texture = null;
+ this._meshData = null;
+ this._meshStacks = null;
+ this._meshWireframe = null;
+ this._traverseData = null;
+
+ /**
+ * A highlight quad drawn over a stacked dom node.
+ */
+ this._highlight = {
+ disabled: true,
+ v0: vec3.create(),
+ v1: vec3.create(),
+ v2: vec3.create(),
+ v3: vec3.create()
+ };
+
+ /**
+ * Scene transformations, exposing offset, translation and rotation.
+ * Modified by events in the controller through delegate functions.
+ */
+ this.transforms = {
+ zoom: 1,
+ offset: vec3.create(), // mesh offset, aligned to the viewport center
+ translation: vec3.create(), // scene translation, on the [x, y, z] axis
+ rotation: quat4.create() // scene rotation, expressed as a quaternion
+ };
+
+ /**
+ * Variables holding information about the initial and current node selected.
+ */
+ this._currentSelection = -1; // the selected node index
+ this._initialMeshConfiguration = false; // true if the 3D mesh was configured
+
+ /**
+ * Variable specifying if the scene should be redrawn.
+ * This should happen usually when the visualization is translated/rotated.
+ */
+ this._redraw = true;
+
+ /**
+ * Total time passed since the rendering started.
+ * If the rendering is paused, this property won't get updated.
+ */
+ this._time = 0;
+
+ /**
+ * Frame delta time (the ammount of time passed for each frame).
+ * This is used to smoothly interpolate animation transfroms.
+ */
+ this._delta = 0;
+ this._prevFrameTime = 0;
+ this._currFrameTime = 0;
+};
+
+TiltVisualizer.Presenter.prototype = {
+
+ /**
+ * Initializes the presenter and starts the animation loop
+ */
+ init: function TVP_init()
+ {
+ this._setup();
+ this._loop();
+ },
+
+ /**
+ * The initialization logic.
+ */
+ _setup: function TVP__setup()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // create the visualization shaders and program to draw the stacks mesh
+ this._visualizationProgram = new renderer.Program({
+ vs: TiltVisualizer.MeshShader.vs,
+ fs: TiltVisualizer.MeshShader.fs,
+ attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"],
+ uniforms: ["mvMatrix", "projMatrix", "sampler"]
+ });
+
+ // get the document zoom to properly scale the visualization
+ this.transforms.zoom = this._getPageZoom();
+
+ // bind the owner object to the necessary functions
+ TiltUtils.bindObjectFunc(this, "^_on");
+ TiltUtils.bindObjectFunc(this, "_loop");
+
+ this._setupTexture();
+ this._setupMeshData();
+ this._setupEventListeners();
+ this.canvas.focus();
+ },
+
+ /**
+ * Get page zoom factor.
+ * @return {Number}
+ */
+ _getPageZoom: function TVP__getPageZoom() {
+ return this.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .fullZoom;
+ },
+
+ /**
+ * The animation logic.
+ */
+ _loop: function TVP__loop()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue rendering
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // prepare for the next frame of the animation loop
+ this.chromeWindow.mozRequestAnimationFrame(this._loop);
+
+ // only redraw if we really have to
+ if (this._redraw) {
+ this._redraw = false;
+ this._drawVisualization();
+ }
+
+ // update the current presenter transfroms from the controller
+ if ("function" === typeof this._controllerUpdate) {
+ this._controllerUpdate(this._time, this._delta);
+ }
+
+ this._handleFrameDelta();
+ this._handleKeyframeNotifications();
+ },
+
+ /**
+ * Calculates the current frame delta time.
+ */
+ _handleFrameDelta: function TVP__handleFrameDelta()
+ {
+ this._prevFrameTime = this._currFrameTime;
+ this._currFrameTime = this.chromeWindow.mozAnimationStartTime;
+ this._delta = this._currFrameTime - this._prevFrameTime;
+ },
+
+ /**
+ * Draws the visualization mesh and highlight quad.
+ */
+ _drawVisualization: function TVP__drawVisualization()
+ {
+ let renderer = this._renderer;
+ let transforms = this.transforms;
+ let w = renderer.width;
+ let h = renderer.height;
+ let ih = renderer.initialHeight;
+
+ // if the mesh wasn't created yet, don't continue rendering
+ if (!this._meshStacks || !this._meshWireframe) {
+ return;
+ }
+
+ // clear the context to an opaque black background
+ renderer.clear();
+ renderer.perspective();
+
+ // apply a transition transformation using an ortho and perspective matrix
+ let ortho = mat4.ortho(0, w, h, 0, -1000, 1000);
+
+ if (!this._isExecutingDestruction) {
+ let f = this._time / INTRO_TRANSITION_DURATION;
+ renderer.lerp(renderer.projMatrix, ortho, f, 8);
+ } else {
+ let f = this._time / OUTRO_TRANSITION_DURATION;
+ renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8);
+ }
+
+ // apply the preliminary transformations to the model view
+ renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION);
+
+ // calculate the camera matrix using the rotation and translation
+ renderer.translate(transforms.translation[0], 0,
+ transforms.translation[2]);
+
+ renderer.transform(quat4.toMat4(transforms.rotation));
+
+ // offset the visualization mesh to center
+ renderer.translate(transforms.offset[0],
+ transforms.offset[1] + transforms.translation[1], 0);
+
+ renderer.scale(transforms.zoom, transforms.zoom);
+
+ // draw the visualization mesh
+ renderer.strokeWeight(2);
+ renderer.depthTest(true);
+ this._drawMeshStacks();
+ this._drawMeshWireframe();
+ this._drawHighlight();
+
+ // make sure the initial transition is drawn until finished
+ if (this._time < INTRO_TRANSITION_DURATION ||
+ this._time < OUTRO_TRANSITION_DURATION) {
+ this._redraw = true;
+ }
+ this._time += this._delta;
+ },
+
+ /**
+ * Draws the meshStacks object.
+ */
+ _drawMeshStacks: function TVP__drawMeshStacks()
+ {
+ let renderer = this._renderer;
+ let mesh = this._meshStacks;
+
+ let visualizationProgram = this._visualizationProgram;
+ let texture = this._texture;
+ let mvMatrix = renderer.mvMatrix;
+ let projMatrix = renderer.projMatrix;
+
+ // use the necessary shader
+ visualizationProgram.use();
+
+ for (let i = 0, len = mesh.length; i < len; i++) {
+ let group = mesh[i];
+
+ // bind the attributes and uniforms as necessary
+ visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices);
+ visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord);
+ visualizationProgram.bindVertexBuffer("vertexColor", group.color);
+
+ visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix);
+ visualizationProgram.bindUniformMatrix("projMatrix", projMatrix);
+ visualizationProgram.bindTexture("sampler", texture);
+
+ // draw the vertices as TRIANGLES indexed elements
+ renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices);
+ }
+
+ // save the current model view and projection matrices
+ mesh.mvMatrix = mat4.create(mvMatrix);
+ mesh.projMatrix = mat4.create(projMatrix);
+ },
+
+ /**
+ * Draws the meshWireframe object.
+ */
+ _drawMeshWireframe: function TVP__drawMeshWireframe()
+ {
+ let renderer = this._renderer;
+ let mesh = this._meshWireframe;
+
+ for (let i = 0, len = mesh.length; i < len; i++) {
+ let group = mesh[i];
+
+ // use the necessary shader
+ renderer.useColorShader(group.vertices, WIREFRAME_COLOR);
+
+ // draw the vertices as LINES indexed elements
+ renderer.drawIndexedVertices(renderer.context.LINES, group.indices);
+ }
+ },
+
+ /**
+ * Draws a highlighted quad around a currently selected node.
+ */
+ _drawHighlight: function TVP__drawHighlight()
+ {
+ // check if there's anything to highlight (i.e any node is selected)
+ if (!this._highlight.disabled) {
+
+ // set the corresponding state to draw the highlight quad
+ let renderer = this._renderer;
+ let highlight = this._highlight;
+
+ renderer.depthTest(false);
+ renderer.fill(highlight.fill, 0.5);
+ renderer.stroke(highlight.stroke);
+ renderer.strokeWeight(highlight.strokeWeight);
+ renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3);
+ }
+ },
+
+ /**
+ * Creates or refreshes the texture applied to the visualization mesh.
+ */
+ _setupTexture: function TVP__setupTexture()
+ {
+ let renderer = this._renderer;
+
+ // destroy any previously created texture
+ TiltUtils.destroyObject(this._texture); this._texture = null;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // get the maximum texture size
+ this._maxTextureSize =
+ renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE);
+
+ // use a simple shim to get the image representation of the document
+ // this will be removed once the MOZ_window_region_texture bug #653656
+ // is finished; currently just converting the document image to a texture
+ // applied to the mesh
+ this._texture = new renderer.Texture({
+ source: TiltGL.TextureUtils.createContentImage(this.contentWindow,
+ this._maxTextureSize),
+ format: "RGB"
+ });
+
+ if ("function" === typeof this._onSetupTexture) {
+ this._onSetupTexture();
+ this._onSetupTexture = null;
+ }
+ },
+
+ /**
+ * Create the combined mesh representing the document visualization by
+ * traversing the document & adding a stack for each node that is drawable.
+ *
+ * @param {Object} aMeshData
+ * object containing the necessary mesh verts, texcoord etc.
+ */
+ _setupMesh: function TVP__setupMesh(aMeshData)
+ {
+ let renderer = this._renderer;
+
+ // destroy any previously created mesh
+ TiltUtils.destroyObject(this._meshStacks); this._meshStacks = [];
+ TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = [];
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // save the mesh data for future use
+ this._meshData = aMeshData;
+
+ // create a sub-mesh for each group in the mesh data
+ for (let i = 0, len = aMeshData.groups.length; i < len; i++) {
+ let group = aMeshData.groups[i];
+
+ // create the visualization mesh using the vertices, texture coordinates
+ // and indices computed when traversing the document object model
+ this._meshStacks.push({
+ vertices: new renderer.VertexBuffer(group.vertices, 3),
+ texCoord: new renderer.VertexBuffer(group.texCoord, 2),
+ color: new renderer.VertexBuffer(group.color, 3),
+ indices: new renderer.IndexBuffer(group.stacksIndices)
+ });
+
+ // additionally, create a wireframe representation to make the
+ // visualization a bit more pretty
+ this._meshWireframe.push({
+ vertices: this._meshStacks[i].vertices,
+ indices: new renderer.IndexBuffer(group.wireframeIndices)
+ });
+ }
+
+ // configure the required mesh transformations and background only once
+ if (!this._initialMeshConfiguration) {
+ this._initialMeshConfiguration = true;
+
+ // set the necessary mesh offsets
+ this.transforms.offset[0] = -renderer.width * 0.5;
+ this.transforms.offset[1] = -renderer.height * 0.5;
+
+ // make sure the canvas is opaque now that the initialization is finished
+ this.canvas.style.background = TiltVisualizerStyle.canvas.background;
+
+ this._drawVisualization();
+ this._redraw = true;
+ }
+
+ if ("function" === typeof this._onSetupMesh) {
+ this._onSetupMesh();
+ this._onSetupMesh = null;
+ }
+ },
+
+ /**
+ * Computes the mesh vertices, texture coordinates etc. by groups of nodes.
+ */
+ _setupMeshData: function TVP__setupMeshData()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // traverse the document and get the depths, coordinates and local names
+ this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, {
+ nodeCallback: this.nodeCallback,
+ invisibleElements: INVISIBLE_ELEMENTS,
+ minSize: ELEMENT_MIN_SIZE,
+ maxX: this._texture.width,
+ maxY: this._texture.height
+ });
+
+ let worker = new ChromeWorker(TILT_CRAFTER);
+
+ worker.addEventListener("message", function TVP_onMessage(event) {
+ this._setupMesh(event.data);
+ }.bind(this), false);
+
+ // calculate necessary information regarding vertices, texture coordinates
+ // etc. in a separate thread, as this process may take a while
+ worker.postMessage({
+ maxGroupNodes: MAX_GROUP_NODES,
+ style: TiltVisualizerStyle.nodes,
+ texWidth: this._texture.width,
+ texHeight: this._texture.height,
+ nodesInfo: this._traverseData.info
+ });
+ },
+
+ /**
+ * Sets up event listeners necessary for the presenter.
+ */
+ _setupEventListeners: function TVP__setupEventListeners()
+ {
+ this.contentWindow.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Called when the content window of the current browser is resized.
+ */
+ _onResize: function TVP_onResize(e)
+ {
+ let zoom = this._getPageZoom();
+ let width = e.target.innerWidth * zoom;
+ let height = e.target.innerHeight * zoom;
+
+ // handle aspect ratio changes to update the projection matrix
+ this._renderer.width = width;
+ this._renderer.height = height;
+
+ this._redraw = true;
+ },
+
+ /**
+ * Highlights a specific node.
+ *
+ * @param {Element} aNode
+ * the html node to be highlighted
+ * @param {String} aFlags
+ * flags specifying highlighting options
+ */
+ highlightNode: function TVP_highlightNode(aNode, aFlags)
+ {
+ this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags);
+ },
+
+ /**
+ * Picks a stacked dom node at the x and y screen coordinates and highlights
+ * the selected node in the mesh.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Function} onpick: function to be called after picking succeeded
+ * {Function} onfail: function to be called after picking failed
+ */
+ highlightNodeAt: function TVP_highlightNodeAt(x, y, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // try to pick a mesh node using the current x, y coordinates
+ this.pickNode(x, y, {
+
+ /**
+ * Mesh picking failed (nothing was found for the picked point).
+ */
+ onfail: function TVP_onHighlightFail()
+ {
+ this.highlightNodeFor(-1);
+
+ if ("function" === typeof aProperties.onfail) {
+ aProperties.onfail();
+ }
+ }.bind(this),
+
+ /**
+ * Mesh picking succeeded.
+ *
+ * @param {Object} aIntersection
+ * object containing the intersection details
+ */
+ onpick: function TVP_onHighlightPick(aIntersection)
+ {
+ this.highlightNodeFor(aIntersection.index);
+
+ if ("function" === typeof aProperties.onpick) {
+ aProperties.onpick();
+ }
+ }.bind(this)
+ });
+ },
+
+ /**
+ * Sets the corresponding highlight coordinates and color based on the
+ * information supplied.
+ *
+ * @param {Number} aNodeIndex
+ * the index of the node in the this._traverseData array
+ * @param {String} aFlags
+ * flags specifying highlighting options
+ */
+ highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags)
+ {
+ this._redraw = true;
+
+ // if the node was already selected, don't do anything
+ if (this._currentSelection === aNodeIndex) {
+ return;
+ }
+
+ // if an invalid or nonexisted node is specified, disable the highlight
+ if (aNodeIndex < 0) {
+ this._currentSelection = -1;
+ this._highlight.disabled = true;
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.UNHIGHLIGHTING, null);
+ return;
+ }
+
+ let highlight = this._highlight;
+ let info = this._traverseData.info[aNodeIndex];
+ let style = TiltVisualizerStyle.nodes;
+
+ highlight.disabled = false;
+ highlight.fill = style[info.name] || style.highlight.defaultFill;
+ highlight.stroke = style.highlight.defaultStroke;
+ highlight.strokeWeight = style.highlight.defaultStrokeWeight;
+
+ let x = info.coord.left;
+ let y = info.coord.top;
+ let w = info.coord.width;
+ let h = info.coord.height;
+ let z = info.coord.depth + info.coord.thickness;
+
+ vec3.set([x, y, z], highlight.v0);
+ vec3.set([x + w, y, z], highlight.v1);
+ vec3.set([x + w, y + h, z], highlight.v2);
+ vec3.set([x, y + h, z], highlight.v3);
+
+ this._currentSelection = aNodeIndex;
+
+ // if something is highlighted, make sure it's inside the current viewport;
+ // the point which should be moved into view is considered the center [x, y]
+ // position along the top edge of the currently selected node
+
+ if (aFlags && aFlags.indexOf("moveIntoView") !== -1)
+ {
+ this.controller.arcball.moveIntoView(vec3.lerp(
+ vec3.scale(this._highlight.v0, this.transforms.zoom, []),
+ vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5));
+ }
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.HIGHLIGHTING, null);
+ },
+
+ /**
+ * Deletes a node from the visualization mesh.
+ *
+ * @param {Number} aNodeIndex
+ * the index of the node in the this._traverseData array;
+ * if not specified, it will default to the current selection
+ */
+ deleteNode: function TVP_deleteNode(aNodeIndex)
+ {
+ // we probably don't want to delete the html or body node.. just sayin'
+ if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) {
+ return;
+ }
+
+ let renderer = this._renderer;
+
+ let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES);
+ let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES);
+ let group = this._meshStacks[groupIndex];
+ let vertices = group.vertices.components;
+
+ for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+ vertices[i + k] = 0;
+ }
+
+ group.vertices = new renderer.VertexBuffer(vertices, 3);
+ this._highlight.disabled = true;
+ this._redraw = true;
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.NODE_REMOVED, null);
+ },
+
+ /**
+ * Picks a stacked dom node at the x and y screen coordinates and issues
+ * a callback function with the found intersection.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Function} onpick: function to be called at intersection
+ * {Function} onfail: function to be called if no intersections
+ */
+ pickNode: function TVP_pickNode(x, y, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // if the mesh wasn't created yet, don't continue picking
+ if (!this._meshStacks || !this._meshWireframe) {
+ return;
+ }
+
+ let worker = new ChromeWorker(TILT_PICKER);
+
+ worker.addEventListener("message", function TVP_onMessage(event) {
+ if (event.data) {
+ if ("function" === typeof aProperties.onpick) {
+ aProperties.onpick(event.data);
+ }
+ } else {
+ if ("function" === typeof aProperties.onfail) {
+ aProperties.onfail();
+ }
+ }
+ }, false);
+
+ let zoom = this._getPageZoom();
+ let width = this._renderer.width * zoom;
+ let height = this._renderer.height * zoom;
+ x *= zoom;
+ y *= zoom;
+
+ // create a ray following the mouse direction from the near clipping plane
+ // to the far clipping plane, to check for intersections with the mesh,
+ // and do all the heavy lifting in a separate thread
+ worker.postMessage({
+ vertices: this._meshData.allVertices,
+
+ // create the ray destined for 3D picking
+ ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height],
+ this._meshStacks.mvMatrix,
+ this._meshStacks.projMatrix)
+ });
+ },
+
+ /**
+ * Delegate translation method, used by the controller.
+ *
+ * @param {Array} aTranslation
+ * the new translation on the [x, y, z] axis
+ */
+ setTranslation: function TVP_setTranslation(aTranslation)
+ {
+ let x = aTranslation[0];
+ let y = aTranslation[1];
+ let z = aTranslation[2];
+ let transforms = this.transforms;
+
+ // only update the translation if it's not already set
+ if (transforms.translation[0] !== x ||
+ transforms.translation[1] !== y ||
+ transforms.translation[2] !== z) {
+
+ vec3.set(aTranslation, transforms.translation);
+ this._redraw = true;
+ }
+ },
+
+ /**
+ * Delegate rotation method, used by the controller.
+ *
+ * @param {Array} aQuaternion
+ * the rotation quaternion, as [x, y, z, w]
+ */
+ setRotation: function TVP_setRotation(aQuaternion)
+ {
+ let x = aQuaternion[0];
+ let y = aQuaternion[1];
+ let z = aQuaternion[2];
+ let w = aQuaternion[3];
+ let transforms = this.transforms;
+
+ // only update the rotation if it's not already set
+ if (transforms.rotation[0] !== x ||
+ transforms.rotation[1] !== y ||
+ transforms.rotation[2] !== z ||
+ transforms.rotation[3] !== w) {
+
+ quat4.set(aQuaternion, transforms.rotation);
+ this._redraw = true;
+ }
+ },
+
+ /**
+ * Handles notifications at specific frame counts.
+ */
+ _handleKeyframeNotifications: function TV__handleKeyframeNotifications()
+ {
+ if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) {
+ this._time = INTRO_TRANSITION_DURATION;
+ }
+ if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) {
+ this._time = OUTRO_TRANSITION_DURATION;
+ }
+
+ if (this._time >= INTRO_TRANSITION_DURATION &&
+ !this._isInitializationFinished &&
+ !this._isExecutingDestruction) {
+
+ this._isInitializationFinished = true;
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.INITIALIZED, null);
+
+ if ("function" === typeof this._onInitializationFinished) {
+ this._onInitializationFinished();
+ }
+ }
+
+ if (this._time >= OUTRO_TRANSITION_DURATION &&
+ !this._isDestructionFinished &&
+ this._isExecutingDestruction) {
+
+ this._isDestructionFinished = true;
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.BEFORE_DESTROYED, null);
+
+ if ("function" === typeof this._onDestructionFinished) {
+ this._onDestructionFinished();
+ }
+ }
+ },
+
+ /**
+ * Starts executing the destruction sequence and issues a callback function
+ * when finished.
+ *
+ * @param {Function} aCallback
+ * the destruction finished callback
+ */
+ executeDestruction: function TV_executeDestruction(aCallback)
+ {
+ if (!this._isExecutingDestruction) {
+ this._isExecutingDestruction = true;
+ this._onDestructionFinished = aCallback;
+
+ // if we execute the destruction after the initialization finishes,
+ // proceed normally; otherwise, skip everything and immediately issue
+ // the callback
+
+ if (this._time > OUTRO_TRANSITION_DURATION) {
+ this._time = 0;
+ this._redraw = true;
+ } else {
+ aCallback();
+ }
+ }
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TVP_isInitialized()
+ {
+ return this._renderer && this._renderer.context;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVP__finalize()
+ {
+ TiltUtils.destroyObject(this._visualizationProgram);
+ TiltUtils.destroyObject(this._texture);
+
+ if (this._meshStacks) {
+ this._meshStacks.forEach(function(group) {
+ TiltUtils.destroyObject(group.vertices);
+ TiltUtils.destroyObject(group.texCoord);
+ TiltUtils.destroyObject(group.color);
+ TiltUtils.destroyObject(group.indices);
+ });
+ }
+ if (this._meshWireframe) {
+ this._meshWireframe.forEach(function(group) {
+ TiltUtils.destroyObject(group.indices);
+ });
+ }
+
+ TiltUtils.destroyObject(this._renderer);
+
+ // Closing the tab would result in contentWindow being a dead object,
+ // so operations like removing event listeners won't work anymore.
+ if (this.contentWindow == this.chromeWindow.content) {
+ this.contentWindow.removeEventListener("resize", this._onResize, false);
+ }
+ }
+};
+
+/**
+ * A mouse and keyboard controller implementation.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the visualization canvas element
+ * @param {TiltVisualizer.Presenter} aPresenter
+ * the presenter instance to control
+ */
+TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter)
+{
+ /**
+ * A canvas overlay on which mouse and keyboard event listeners are attached.
+ */
+ this.canvas = aCanvas;
+
+ /**
+ * Save a reference to the presenter to modify its model-view transforms.
+ */
+ this.presenter = aPresenter;
+ this.presenter.controller = this;
+
+ /**
+ * The initial controller dimensions and offset, in pixels.
+ */
+ this._zoom = aPresenter.transforms.zoom;
+ this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom;
+ this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom;
+ this._width = aCanvas.width;
+ this._height = aCanvas.height;
+
+ /**
+ * Arcball used to control the visualization using the mouse.
+ */
+ this.arcball = new TiltVisualizer.Arcball(
+ this.presenter.chromeWindow, this._width, this._height, 0,
+ [
+ this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0,
+ this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0
+ ]);
+
+ /**
+ * Object containing the rotation quaternion and the translation amount.
+ */
+ this._coordinates = null;
+
+ // bind the owner object to the necessary functions
+ TiltUtils.bindObjectFunc(this, "_update");
+ TiltUtils.bindObjectFunc(this, "^_on");
+
+ // add the necessary event listeners
+ this.addEventListeners();
+
+ // attach this controller's update function to the presenter ondraw event
+ this.presenter._controllerUpdate = this._update;
+};
+
+TiltVisualizer.Controller.prototype = {
+
+ /**
+ * Adds events listeners required by this controller.
+ */
+ addEventListeners: function TVC_addEventListeners()
+ {
+ let canvas = this.canvas;
+ let presenter = this.presenter;
+
+ // bind commonly used mouse and keyboard events with the controller
+ canvas.addEventListener("mousedown", this._onMouseDown, false);
+ canvas.addEventListener("mouseup", this._onMouseUp, false);
+ canvas.addEventListener("mousemove", this._onMouseMove, false);
+ canvas.addEventListener("mouseover", this._onMouseOver, false);
+ canvas.addEventListener("mouseout", this._onMouseOut, false);
+ canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false);
+ canvas.addEventListener("keydown", this._onKeyDown, false);
+ canvas.addEventListener("keyup", this._onKeyUp, false);
+ canvas.addEventListener("keypress", this._onKeyPress, true);
+ canvas.addEventListener("blur", this._onBlur, false);
+
+ // handle resize events to change the arcball dimensions
+ presenter.contentWindow.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Removes all added events listeners required by this controller.
+ */
+ removeEventListeners: function TVC_removeEventListeners()
+ {
+ let canvas = this.canvas;
+ let presenter = this.presenter;
+
+ canvas.removeEventListener("mousedown", this._onMouseDown, false);
+ canvas.removeEventListener("mouseup", this._onMouseUp, false);
+ canvas.removeEventListener("mousemove", this._onMouseMove, false);
+ canvas.removeEventListener("mouseover", this._onMouseOver, false);
+ canvas.removeEventListener("mouseout", this._onMouseOut, false);
+ canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false);
+ canvas.removeEventListener("keydown", this._onKeyDown, false);
+ canvas.removeEventListener("keyup", this._onKeyUp, false);
+ canvas.removeEventListener("keypress", this._onKeyPress, true);
+ canvas.removeEventListener("blur", this._onBlur, false);
+
+ // Closing the tab would result in contentWindow being a dead object,
+ // so operations like removing event listeners won't work anymore.
+ if (presenter.contentWindow == presenter.chromeWindow.content) {
+ presenter.contentWindow.removeEventListener("resize", this._onResize, false);
+ }
+ },
+
+ /**
+ * Function called each frame, updating the visualization camera transforms.
+ *
+ * @param {Number} aTime
+ * total time passed since rendering started
+ * @param {Number} aDelta
+ * the current animation frame delta
+ */
+ _update: function TVC__update(aTime, aDelta)
+ {
+ this._time = aTime;
+ this._coordinates = this.arcball.update(aDelta);
+
+ this.presenter.setRotation(this._coordinates.rotation);
+ this.presenter.setTranslation(this._coordinates.translation);
+ },
+
+ /**
+ * Called once after every time a mouse button is pressed.
+ */
+ _onMouseDown: function TVC__onMouseDown(e)
+ {
+ e.target.focus();
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let button = e.which;
+ this._downX = e.clientX - e.target.offsetLeft;
+ this._downY = e.clientY - e.target.offsetTop;
+
+ this.arcball.mouseDown(this._downX, this._downY, button);
+ },
+
+ /**
+ * Called every time a mouse button is released.
+ */
+ _onMouseUp: function TVC__onMouseUp(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let button = e.which;
+ let upX = e.clientX - e.target.offsetLeft;
+ let upY = e.clientY - e.target.offsetTop;
+
+ // a click in Tilt is issued only when the mouse pointer stays in
+ // relatively the same position
+ if (Math.abs(this._downX - upX) < MOUSE_CLICK_THRESHOLD &&
+ Math.abs(this._downY - upY) < MOUSE_CLICK_THRESHOLD) {
+
+ this.presenter.highlightNodeAt(upX, upY);
+ }
+
+ this.arcball.mouseUp(upX, upY, button);
+ },
+
+ /**
+ * Called every time the mouse moves.
+ */
+ _onMouseMove: function TVC__onMouseMove(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let moveX = e.clientX - e.target.offsetLeft;
+ let moveY = e.clientY - e.target.offsetTop;
+
+ this.arcball.mouseMove(moveX, moveY);
+ },
+
+ /**
+ * Called when the mouse leaves the visualization bounds.
+ */
+ _onMouseOver: function TVC__onMouseOver(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.mouseOver();
+ },
+
+ /**
+ * Called when the mouse leaves the visualization bounds.
+ */
+ _onMouseOut: function TVC__onMouseOut(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.mouseOut();
+ },
+
+ /**
+ * Called when the mouse wheel is used.
+ */
+ _onMozScroll: function TVC__onMozScroll(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.zoom(e.detail);
+ },
+
+ /**
+ * Called when a key is pressed.
+ */
+ _onKeyDown: function TVC__onKeyDown(e)
+ {
+ let code = e.keyCode || e.which;
+
+ if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.arcball.keyDown(code);
+ } else {
+ this.arcball.cancelKeyEvents();
+ }
+ },
+
+ /**
+ * Called when a key is released.
+ */
+ _onKeyUp: function TVC__onKeyUp(e)
+ {
+ let code = e.keyCode || e.which;
+
+ if (code === e.DOM_VK_X) {
+ this.presenter.deleteNode();
+ }
+ if (code === e.DOM_VK_F) {
+ let highlight = this.presenter._highlight;
+ let zoom = this.presenter.transforms.zoom;
+
+ this.arcball.moveIntoView(vec3.lerp(
+ vec3.scale(highlight.v0, zoom, []),
+ vec3.scale(highlight.v1, zoom, []), 0.5));
+ }
+ if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.arcball.keyUp(code);
+ }
+ },
+
+ /**
+ * Called when a key is pressed.
+ */
+ _onKeyPress: function TVC__onKeyPress(e)
+ {
+ if (e.keyCode === e.DOM_VK_ESCAPE) {
+ let {TiltManager} = require("devtools/tilt/tilt");
+ let tilt =
+ TiltManager.getTiltForBrowser(this.presenter.chromeWindow);
+ e.preventDefault();
+ e.stopPropagation();
+ tilt.destroy(tilt.currentWindowId, true);
+ }
+ },
+
+ /**
+ * Called when the canvas looses focus.
+ */
+ _onBlur: function TVC__onBlur(e) {
+ this.arcball.cancelKeyEvents();
+ },
+
+ /**
+ * Called when the content window of the current browser is resized.
+ */
+ _onResize: function TVC__onResize(e)
+ {
+ let zoom = this.presenter._getPageZoom();
+ let width = e.target.innerWidth * zoom;
+ let height = e.target.innerHeight * zoom;
+
+ this.arcball.resize(width, height);
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TVC_isInitialized()
+ {
+ return this.arcball ? true : false;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVC__finalize()
+ {
+ TiltUtils.destroyObject(this.arcball);
+ TiltUtils.destroyObject(this._coordinates);
+
+ this.removeEventListeners();
+ this.presenter.controller = null;
+ this.presenter._controllerUpdate = null;
+ }
+};
+
+/**
+ * This is a general purpose 3D rotation controller described by Ken Shoemake
+ * in the Graphics Interface ’92 Proceedings. It features good behavior
+ * easy implementation, cheap execution.
+ *
+ * @param {Window} aChromeWindow
+ * a reference to the top-level window
+ * @param {Number} aWidth
+ * the width of canvas
+ * @param {Number} aHeight
+ * the height of canvas
+ * @param {Number} aRadius
+ * optional, the radius of the arcball
+ * @param {Array} aInitialTrans
+ * optional, initial vector translation
+ * @param {Array} aInitialRot
+ * optional, initial quaternion rotation
+ */
+TiltVisualizer.Arcball = function TV_Arcball(
+ aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot)
+{
+ /**
+ * Save a reference to the top-level window to set/remove intervals.
+ */
+ this.chromeWindow = aChromeWindow;
+
+ /**
+ * Values retaining the current horizontal and vertical mouse coordinates.
+ */
+ this._mousePress = vec3.create();
+ this._mouseRelease = vec3.create();
+ this._mouseMove = vec3.create();
+ this._mouseLerp = vec3.create();
+ this._mouseButton = -1;
+
+ /**
+ * Object retaining the current pressed key codes.
+ */
+ this._keyCode = {};
+
+ /**
+ * The vectors representing the mouse coordinates mapped on the arcball
+ * and their perpendicular converted from (x, y) to (x, y, z) at specific
+ * events like mousePressed and mouseDragged.
+ */
+ this._startVec = vec3.create();
+ this._endVec = vec3.create();
+ this._pVec = vec3.create();
+
+ /**
+ * The corresponding rotation quaternions.
+ */
+ this._lastRot = quat4.create();
+ this._deltaRot = quat4.create();
+ this._currentRot = quat4.create(aInitialRot);
+
+ /**
+ * The current camera translation coordinates.
+ */
+ this._lastTrans = vec3.create();
+ this._deltaTrans = vec3.create();
+ this._currentTrans = vec3.create(aInitialTrans);
+ this._zoomAmount = 0;
+
+ /**
+ * Additional rotation and translation vectors.
+ */
+ this._additionalRot = vec3.create();
+ this._additionalTrans = vec3.create();
+ this._deltaAdditionalRot = quat4.create();
+ this._deltaAdditionalTrans = vec3.create();
+
+ // load the keys controlling the arcball
+ this._loadKeys();
+
+ // set the current dimensions of the arcball
+ this.resize(aWidth, aHeight, aRadius);
+};
+
+TiltVisualizer.Arcball.prototype = {
+
+ /**
+ * Call this function whenever you need the updated rotation quaternion
+ * and the zoom amount. These values will be returned as "rotation" and
+ * "translation" properties inside an object.
+ *
+ * @param {Number} aDelta
+ * the current animation frame delta
+ *
+ * @return {Object} the rotation quaternion and the translation amount
+ */
+ update: function TVA_update(aDelta)
+ {
+ let mousePress = this._mousePress;
+ let mouseRelease = this._mouseRelease;
+ let mouseMove = this._mouseMove;
+ let mouseLerp = this._mouseLerp;
+ let mouseButton = this._mouseButton;
+
+ // smoothly update the mouse coordinates
+ mouseLerp[0] += (mouseMove[0] - mouseLerp[0]) * ARCBALL_SENSITIVITY;
+ mouseLerp[1] += (mouseMove[1] - mouseLerp[1]) * ARCBALL_SENSITIVITY;
+
+ // cache the interpolated mouse coordinates
+ let x = mouseLerp[0];
+ let y = mouseLerp[1];
+
+ // the smoothed arcball rotation may not be finished when the mouse is
+ // pressed again, so cancel the rotation if other events occur or the
+ // animation finishes
+ if (mouseButton === 3 || x === mouseRelease[0] && y === mouseRelease[1]) {
+ this._rotating = false;
+ }
+
+ let startVec = this._startVec;
+ let endVec = this._endVec;
+ let pVec = this._pVec;
+
+ let lastRot = this._lastRot;
+ let deltaRot = this._deltaRot;
+ let currentRot = this._currentRot;
+
+ // left mouse button handles rotation
+ if (mouseButton === 1 || this._rotating) {
+ // the rotation doesn't stop immediately after the left mouse button is
+ // released, so add a flag to smoothly continue it until it ends
+ this._rotating = true;
+
+ // find the sphere coordinates of the mouse positions
+ this._pointToSphere(x, y, this.width, this.height, this.radius, endVec);
+
+ // compute the vector perpendicular to the start & end vectors
+ vec3.cross(startVec, endVec, pVec);
+
+ // if the begin and end vectors don't coincide
+ if (vec3.length(pVec) > 0) {
+ deltaRot[0] = pVec[0];
+ deltaRot[1] = pVec[1];
+ deltaRot[2] = pVec[2];
+
+ // in the quaternion values, w is cosine (theta / 2),
+ // where theta is the rotation angle
+ deltaRot[3] = -vec3.dot(startVec, endVec);
+ } else {
+ // return an identity rotation quaternion
+ deltaRot[0] = 0;
+ deltaRot[1] = 0;
+ deltaRot[2] = 0;
+ deltaRot[3] = 1;
+ }
+
+ // calculate the current rotation based on the mouse click events
+ quat4.multiply(lastRot, deltaRot, currentRot);
+ } else {
+ // save the current quaternion to stack rotations
+ quat4.set(currentRot, lastRot);
+ }
+
+ let lastTrans = this._lastTrans;
+ let deltaTrans = this._deltaTrans;
+ let currentTrans = this._currentTrans;
+
+ // right mouse button handles panning
+ if (mouseButton === 3) {
+ // calculate a delta translation between the new and old mouse position
+ // and save it to the current translation
+ deltaTrans[0] = mouseMove[0] - mousePress[0];
+ deltaTrans[1] = mouseMove[1] - mousePress[1];
+
+ currentTrans[0] = lastTrans[0] + deltaTrans[0];
+ currentTrans[1] = lastTrans[1] + deltaTrans[1];
+ } else {
+ // save the current panning to stack translations
+ lastTrans[0] = currentTrans[0];
+ lastTrans[1] = currentTrans[1];
+ }
+
+ let zoomAmount = this._zoomAmount;
+ let keyCode = this._keyCode;
+
+ // mouse wheel handles zooming
+ deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP;
+ currentTrans[2] += deltaTrans[2];
+
+ let additionalRot = this._additionalRot;
+ let additionalTrans = this._additionalTrans;
+ let deltaAdditionalRot = this._deltaAdditionalRot;
+ let deltaAdditionalTrans = this._deltaAdditionalTrans;
+
+ let rotateKeys = this.rotateKeys;
+ let panKeys = this.panKeys;
+ let zoomKeys = this.zoomKeys;
+ let resetKey = this.resetKey;
+
+ // handle additional rotation and translation by the keyboard
+ if (keyCode[rotateKeys.left]) {
+ additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.right]) {
+ additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.up]) {
+ additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.down]) {
+ additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[panKeys.left]) {
+ additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.right]) {
+ additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.up]) {
+ additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.down]) {
+ additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[zoomKeys["in"][0]] ||
+ keyCode[zoomKeys["in"][1]] ||
+ keyCode[zoomKeys["in"][2]]) {
+ this.zoom(-ARCBALL_TRANSLATION_STEP);
+ }
+ if (keyCode[zoomKeys["out"][0]] ||
+ keyCode[zoomKeys["out"][1]]) {
+ this.zoom(ARCBALL_TRANSLATION_STEP);
+ }
+ if (keyCode[zoomKeys["unzoom"]]) {
+ this._zoomAmount = 0;
+ }
+ if (keyCode[resetKey]) {
+ this.reset();
+ }
+
+ // update the delta key rotations and translations
+ deltaAdditionalRot[0] +=
+ (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalRot[1] +=
+ (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalRot[2] +=
+ (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY;
+
+ deltaAdditionalTrans[0] +=
+ (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalTrans[1] +=
+ (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY;
+
+ // create an additional rotation based on the key events
+ quat4.fromEuler(
+ deltaAdditionalRot[0],
+ deltaAdditionalRot[1],
+ deltaAdditionalRot[2], deltaRot);
+
+ // create an additional translation based on the key events
+ vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans);
+
+ // handle the reset animation steps if necessary
+ if (this._resetInProgress) {
+ this._nextResetStep(aDelta || 1);
+ }
+
+ // return the current rotation and translation
+ return {
+ rotation: quat4.multiply(deltaRot, currentRot),
+ translation: vec3.add(deltaTrans, currentTrans)
+ };
+ },
+
+ /**
+ * Function handling the mouseDown event.
+ * Call this when the mouse was pressed.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Number} aButton
+ * which mouse button was pressed
+ */
+ mouseDown: function TVA_mouseDown(x, y, aButton)
+ {
+ // save the mouse down state and prepare for rotations or translations
+ this._mousePress[0] = x;
+ this._mousePress[1] = y;
+ this._mouseButton = aButton;
+ this._cancelReset();
+ this._save();
+
+ // find the sphere coordinates of the mouse positions
+ this._pointToSphere(
+ x, y, this.width, this.height, this.radius, this._startVec);
+
+ quat4.set(this._currentRot, this._lastRot);
+ },
+
+ /**
+ * Function handling the mouseUp event.
+ * Call this when a mouse button was released.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ */
+ mouseUp: function TVA_mouseUp(x, y)
+ {
+ // save the mouse up state and prepare for rotations or translations
+ this._mouseRelease[0] = x;
+ this._mouseRelease[1] = y;
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the mouseMove event.
+ * Call this when the mouse was moved.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ */
+ mouseMove: function TVA_mouseMove(x, y)
+ {
+ // save the mouse move state and prepare for rotations or translations
+ // only if the mouse is pressed
+ if (this._mouseButton !== -1) {
+ this._mouseMove[0] = x;
+ this._mouseMove[1] = y;
+ }
+ },
+
+ /**
+ * Function handling the mouseOver event.
+ * Call this when the mouse enters the context bounds.
+ */
+ mouseOver: function TVA_mouseOver()
+ {
+ // if the mouse just entered the parent bounds, stop the animation
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the mouseOut event.
+ * Call this when the mouse leaves the context bounds.
+ */
+ mouseOut: function TVA_mouseOut()
+ {
+ // if the mouse leaves the parent bounds, stop the animation
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the arcball zoom amount.
+ * Call this, for example, when the mouse wheel was scrolled or zoom keys
+ * were pressed.
+ *
+ * @param {Number} aZoom
+ * the zoom direction and speed
+ */
+ zoom: function TVA_zoom(aZoom)
+ {
+ this._cancelReset();
+ this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom,
+ ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX);
+ },
+
+ /**
+ * Function handling the keyDown event.
+ * Call this when a key was pressed.
+ *
+ * @param {Number} aCode
+ * the code corresponding to the key pressed
+ */
+ keyDown: function TVA_keyDown(aCode)
+ {
+ this._cancelReset();
+ this._keyCode[aCode] = true;
+ },
+
+ /**
+ * Function handling the keyUp event.
+ * Call this when a key was released.
+ *
+ * @param {Number} aCode
+ * the code corresponding to the key released
+ */
+ keyUp: function TVA_keyUp(aCode)
+ {
+ this._keyCode[aCode] = false;
+ },
+
+ /**
+ * Maps the 2d coordinates of the mouse location to a 3d point on a sphere.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Number} aWidth
+ * the width of canvas
+ * @param {Number} aHeight
+ * the height of canvas
+ * @param {Number} aRadius
+ * optional, the radius of the arcball
+ * @param {Array} aSphereVec
+ * a 3d vector to store the sphere coordinates
+ */
+ _pointToSphere: function TVA__pointToSphere(
+ x, y, aWidth, aHeight, aRadius, aSphereVec)
+ {
+ // adjust point coords and scale down to range of [-1..1]
+ x = (x - aWidth * 0.5) / aRadius;
+ y = (y - aHeight * 0.5) / aRadius;
+
+ // compute the square length of the vector to the point from the center
+ let normal = 0;
+ let sqlength = x * x + y * y;
+
+ // if the point is mapped outside of the sphere
+ if (sqlength > 1) {
+ // calculate the normalization factor
+ normal = 1 / Math.sqrt(sqlength);
+
+ // set the normalized vector (a point on the sphere)
+ aSphereVec[0] = x * normal;
+ aSphereVec[1] = y * normal;
+ aSphereVec[2] = 0;
+ } else {
+ // set the vector to a point mapped inside the sphere
+ aSphereVec[0] = x;
+ aSphereVec[1] = y;
+ aSphereVec[2] = Math.sqrt(1 - sqlength);
+ }
+ },
+
+ /**
+ * Cancels all pending transformations caused by key events.
+ */
+ cancelKeyEvents: function TVA_cancelKeyEvents()
+ {
+ this._keyCode = {};
+ },
+
+ /**
+ * Cancels all pending transformations caused by mouse events.
+ */
+ cancelMouseEvents: function TVA_cancelMouseEvents()
+ {
+ this._rotating = false;
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Incremental translation method.
+ *
+ * @param {Array} aTranslation
+ * the translation ammount on the [x, y] axis
+ */
+ translate: function TVP_translate(aTranslation)
+ {
+ this._additionalTrans[0] += aTranslation[0];
+ this._additionalTrans[1] += aTranslation[1];
+ },
+
+ /**
+ * Incremental rotation method.
+ *
+ * @param {Array} aRotation
+ * the rotation ammount along the [x, y, z] axis
+ */
+ rotate: function TVP_rotate(aRotation)
+ {
+ // explicitly rotate along y, x, z values because they're eulerian angles
+ this._additionalRot[0] += TiltMath.radians(aRotation[1]);
+ this._additionalRot[1] += TiltMath.radians(aRotation[0]);
+ this._additionalRot[2] += TiltMath.radians(aRotation[2]);
+ },
+
+ /**
+ * Moves a target point into view only if it's outside the currently visible
+ * area bounds (in which case it also resets any additional transforms).
+ *
+ * @param {Arary} aPoint
+ * the [x, y] point which should be brought into view
+ */
+ moveIntoView: function TVA_moveIntoView(aPoint) {
+ let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]);
+ let visiblePointY = -(this._currentTrans[1] + this._additionalTrans[1]);
+
+ if (aPoint[1] - visiblePointY - MOVE_INTO_VIEW_ACCURACY > this.height ||
+ aPoint[1] - visiblePointY + MOVE_INTO_VIEW_ACCURACY < 0 ||
+ aPoint[0] - visiblePointX > this.width ||
+ aPoint[0] - visiblePointX < 0) {
+ this.reset([0, -aPoint[1]]);
+ }
+ },
+
+ /**
+ * Resize this implementation to use different bounds.
+ * This function is automatically called when the arcball is created.
+ *
+ * @param {Number} newWidth
+ * the new width of canvas
+ * @param {Number} newHeight
+ * the new height of canvas
+ * @param {Number} newRadius
+ * optional, the new radius of the arcball
+ */
+ resize: function TVA_resize(newWidth, newHeight, newRadius)
+ {
+ if (!newWidth || !newHeight) {
+ return;
+ }
+
+ // set the new width, height and radius dimensions
+ this.width = newWidth;
+ this.height = newHeight;
+ this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight);
+ this._save();
+ },
+
+ /**
+ * Starts an animation resetting the arcball transformations to identity.
+ *
+ * @param {Array} aFinalTranslation
+ * optional, final vector translation
+ * @param {Array} aFinalRotation
+ * optional, final quaternion rotation
+ */
+ reset: function TVA_reset(aFinalTranslation, aFinalRotation)
+ {
+ if ("function" === typeof this._onResetStart) {
+ this._onResetStart();
+ this._onResetStart = null;
+ }
+
+ this.cancelMouseEvents();
+ this.cancelKeyEvents();
+ this._cancelReset();
+
+ this._save();
+ this._resetFinalTranslation = vec3.create(aFinalTranslation);
+ this._resetFinalRotation = quat4.create(aFinalRotation);
+ this._resetInProgress = true;
+ },
+
+ /**
+ * Cancels the current arcball reset animation if there is one.
+ */
+ _cancelReset: function TVA__cancelReset()
+ {
+ if (this._resetInProgress) {
+ this._resetInProgress = false;
+ this._save();
+
+ if ("function" === typeof this._onResetFinish) {
+ this._onResetFinish();
+ this._onResetFinish = null;
+ this._onResetStep = null;
+ }
+ }
+ },
+
+ /**
+ * Executes the next step in the arcball reset animation.
+ *
+ * @param {Number} aDelta
+ * the current animation frame delta
+ */
+ _nextResetStep: function TVA__nextResetStep(aDelta)
+ {
+ // a very large animation frame delta (in case of seriously low framerate)
+ // would cause all the interpolations to become highly unstable
+ aDelta = TiltMath.clamp(aDelta, 1, 100);
+
+ let fNearZero = EPSILON * EPSILON;
+ let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta;
+ let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR;
+ let fTran = this._resetFinalTranslation;
+ let fRot = this._resetFinalRotation;
+
+ let t = vec3.create(fTran);
+ let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot);
+
+ // reset the rotation quaternion and translation vector
+ vec3.lerp(this._currentTrans, t, fInterpLin);
+ quat4.slerp(this._currentRot, r, fInterpSph);
+
+ // also reset any additional transforms by the keyboard or mouse
+ vec3.scale(this._additionalTrans, fInterpLin);
+ vec3.scale(this._additionalRot, fInterpLin);
+ this._zoomAmount *= fInterpLin;
+
+ // clear the loop if the all values are very close to zero
+ if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero &&
+ vec3.length(this._additionalRot) < fNearZero &&
+ vec3.length(this._additionalTrans) < fNearZero) {
+
+ this._cancelReset();
+ }
+
+ if ("function" === typeof this._onResetStep) {
+ this._onResetStep();
+ }
+ },
+
+ /**
+ * Loads the keys to control this arcball.
+ */
+ _loadKeys: function TVA__loadKeys()
+ {
+ this.rotateKeys = {
+ "up": Ci.nsIDOMKeyEvent["DOM_VK_W"],
+ "down": Ci.nsIDOMKeyEvent["DOM_VK_S"],
+ "left": Ci.nsIDOMKeyEvent["DOM_VK_A"],
+ "right": Ci.nsIDOMKeyEvent["DOM_VK_D"],
+ };
+ this.panKeys = {
+ "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"],
+ "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"],
+ "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"],
+ "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"],
+ };
+ this.zoomKeys = {
+ "in": [
+ Ci.nsIDOMKeyEvent["DOM_VK_I"],
+ Ci.nsIDOMKeyEvent["DOM_VK_ADD"],
+ Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"],
+ ],
+ "out": [
+ Ci.nsIDOMKeyEvent["DOM_VK_O"],
+ Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"],
+ ],
+ "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"]
+ };
+ this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"];
+ },
+
+ /**
+ * Saves the current arcball state, typically after resize or mouse events.
+ */
+ _save: function TVA__save()
+ {
+ if (this._mousePress) {
+ let x = this._mousePress[0];
+ let y = this._mousePress[1];
+
+ this._mouseMove[0] = x;
+ this._mouseMove[1] = y;
+ this._mouseRelease[0] = x;
+ this._mouseRelease[1] = y;
+ this._mouseLerp[0] = x;
+ this._mouseLerp[1] = y;
+ }
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVA__finalize()
+ {
+ this._cancelReset();
+ }
+};
+
+/**
+ * Tilt configuration preferences.
+ */
+TiltVisualizer.Prefs = {
+
+ /**
+ * Specifies if Tilt is enabled or not.
+ */
+ get enabled()
+ {
+ return this._enabled;
+ },
+
+ set enabled(value)
+ {
+ TiltUtils.Preferences.set("enabled", "boolean", value);
+ this._enabled = value;
+ },
+
+ get introTransition()
+ {
+ return this._introTransition;
+ },
+
+ set introTransition(value)
+ {
+ TiltUtils.Preferences.set("intro_transition", "boolean", value);
+ this._introTransition = value;
+ },
+
+ get outroTransition()
+ {
+ return this._outroTransition;
+ },
+
+ set outroTransition(value)
+ {
+ TiltUtils.Preferences.set("outro_transition", "boolean", value);
+ this._outroTransition = value;
+ },
+
+ /**
+ * Loads the preferences.
+ */
+ load: function TVC_load()
+ {
+ let prefs = TiltVisualizer.Prefs;
+ let get = TiltUtils.Preferences.get;
+
+ prefs._enabled = get("enabled", "boolean");
+ prefs._introTransition = get("intro_transition", "boolean");
+ prefs._outroTransition = get("outro_transition", "boolean");
+ }
+};
+
+/**
+ * A custom visualization shader.
+ *
+ * @param {Attribute} vertexPosition: the vertex position
+ * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler
+ * @param {Attribute} vertexColor: specific [r, g, b] color for each vertex
+ * @param {Uniform} mvMatrix: the model view matrix
+ * @param {Uniform} projMatrix: the projection matrix
+ * @param {Uniform} sampler: the texture sampler to fetch the pixels from
+ */
+TiltVisualizer.MeshShader = {
+
+ /**
+ * Vertex shader.
+ */
+ vs: [
+ "attribute vec3 vertexPosition;",
+ "attribute vec2 vertexTexCoord;",
+ "attribute vec3 vertexColor;",
+
+ "uniform mat4 mvMatrix;",
+ "uniform mat4 projMatrix;",
+
+ "varying vec2 texCoord;",
+ "varying vec3 color;",
+
+ "void main() {",
+ " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);",
+ " texCoord = vertexTexCoord;",
+ " color = vertexColor;",
+ "}"
+ ].join("\n"),
+
+ /**
+ * Fragment shader.
+ */
+ fs: [
+ "#ifdef GL_ES",
+ "precision lowp float;",
+ "#endif",
+
+ "uniform sampler2D sampler;",
+
+ "varying vec2 texCoord;",
+ "varying vec3 color;",
+
+ "void main() {",
+ " if (texCoord.x < 0.0) {",
+ " gl_FragColor = vec4(color, 1.0);",
+ " } else {",
+ " gl_FragColor = vec4(texture2D(sampler, texCoord).rgb, 1.0);",
+ " }",
+ "}"
+ ].join("\n")
+};
diff --git a/browser/devtools/tilt/tilt.js b/browser/devtools/tilt/tilt.js
new file mode 100644
index 000000000..bd41f0432
--- /dev/null
+++ b/browser/devtools/tilt/tilt.js
@@ -0,0 +1,263 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 {Cu} = require("chrome");
+
+let {TiltVisualizer} = require("devtools/tilt/tilt-visualizer");
+let TiltGL = require("devtools/tilt/tilt-gl");
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let EventEmitter = require("devtools/shared/event-emitter");
+let Telemetry = require("devtools/shared/telemetry");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Tilt notifications dispatched through the nsIObserverService.
+const TILT_NOTIFICATIONS = {
+ // Called early in the startup of a new tilt instance
+ STARTUP: "tilt-startup",
+
+ // Fires when Tilt starts the initialization.
+ INITIALIZING: "tilt-initializing",
+
+ // Fires immediately after initialization is complete.
+ // (when the canvas overlay is visible and the 3D mesh is completely created)
+ INITIALIZED: "tilt-initialized",
+
+ // Fires immediately before the destruction is started.
+ DESTROYING: "tilt-destroying",
+
+ // Fires immediately before the destruction is finished.
+ // (just before the canvas overlay is removed from its parent node)
+ BEFORE_DESTROYED: "tilt-before-destroyed",
+
+ // Fires when Tilt is completely destroyed.
+ DESTROYED: "tilt-destroyed",
+
+ // Fires when Tilt is shown (after a tab-switch).
+ SHOWN: "tilt-shown",
+
+ // Fires when Tilt is hidden (after a tab-switch).
+ HIDDEN: "tilt-hidden",
+
+ // Fires once Tilt highlights an element in the page.
+ HIGHLIGHTING: "tilt-highlighting",
+
+ // Fires once Tilt stops highlighting any element.
+ UNHIGHLIGHTING: "tilt-unhighlighting",
+
+ // Fires when a node is removed from the 3D mesh.
+ NODE_REMOVED: "tilt-node-removed"
+};
+
+let TiltManager = {
+ _instances: new WeakMap(),
+ getTiltForBrowser: function(aChromeWindow)
+ {
+ if (this._instances.has(aChromeWindow)) {
+ return this._instances.get(aChromeWindow);
+ } else {
+ let tilt = new Tilt(aChromeWindow);
+ this._instances.set(aChromeWindow, tilt);
+ return tilt;
+ }
+ },
+}
+
+exports.TiltManager = TiltManager;
+
+/**
+ * Object managing instances of the visualizer.
+ *
+ * @param {Window} aWindow
+ * the chrome window used by each visualizer instance
+ */
+function Tilt(aWindow)
+{
+ /**
+ * Save a reference to the top-level window.
+ */
+ this.chromeWindow = aWindow;
+
+ /**
+ * All the instances of TiltVisualizer.
+ */
+ this.visualizers = {};
+
+ /**
+ * Shortcut for accessing notifications strings.
+ */
+ this.NOTIFICATIONS = TILT_NOTIFICATIONS;
+
+ EventEmitter.decorate(this);
+
+ this.setup();
+
+ this._telemetry = new Telemetry();
+}
+
+Tilt.prototype = {
+
+ /**
+ * Initializes a visualizer for the current tab or closes it if already open.
+ */
+ toggle: function T_toggle()
+ {
+ let contentWindow = this.chromeWindow.gBrowser.selectedBrowser.contentWindow;
+ let id = this.currentWindowId;
+ let self = this;
+
+ contentWindow.addEventListener("beforeunload", function onUnload() {
+ contentWindow.removeEventListener("beforeunload", onUnload, false);
+ self.destroy(id, true);
+ }, false);
+
+ // if the visualizer for the current tab is already open, destroy it now
+ if (this.visualizers[id]) {
+ this.destroy(id, true);
+ this._telemetry.toolClosed("tilt");
+ return;
+ } else {
+ this._telemetry.toolOpened("tilt");
+ }
+
+ // create a visualizer instance for the current tab
+ this.visualizers[id] = new TiltVisualizer({
+ chromeWindow: this.chromeWindow,
+ contentWindow: contentWindow,
+ parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
+ notifications: this.NOTIFICATIONS,
+ tab: this.chromeWindow.gBrowser.selectedTab
+ });
+
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.STARTUP, null);
+ this.visualizers[id].init();
+
+ // make sure the visualizer object was initialized properly
+ if (!this.visualizers[id].isInitialized()) {
+ this.destroy(id);
+ this.failureCallback && this.failureCallback();
+ return;
+ }
+
+ this.lastInstanceId = id;
+ this.emit("change", this.chromeWindow.gBrowser.selectedTab);
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.INITIALIZING, null);
+ },
+
+ /**
+ * Starts destroying a specific instance of the visualizer.
+ *
+ * @param {String} aId
+ * the identifier of the instance in the visualizers array
+ * @param {Boolean} aAnimateFlag
+ * optional, set to true to display a destruction transition
+ */
+ destroy: function T_destroy(aId, aAnimateFlag)
+ {
+ // if the visualizer is destroyed or destroying, don't do anything
+ if (!this.visualizers[aId] || this._isDestroying) {
+ return;
+ }
+ this._isDestroying = true;
+
+ let controller = this.visualizers[aId].controller;
+ let presenter = this.visualizers[aId].presenter;
+
+ let content = presenter.contentWindow;
+ let pageXOffset = content.pageXOffset * presenter.transforms.zoom;
+ let pageYOffset = content.pageYOffset * presenter.transforms.zoom;
+ TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom);
+
+ // if we're not doing any outro animation, just finish destruction directly
+ if (!aAnimateFlag) {
+ this._finish(aId);
+ return;
+ }
+
+ // otherwise, trigger the outro animation and notify necessary observers
+ Services.obs.notifyObservers(content, TILT_NOTIFICATIONS.DESTROYING, null);
+
+ controller.removeEventListeners();
+ controller.arcball.reset([-pageXOffset, -pageYOffset]);
+ presenter.executeDestruction(this._finish.bind(this, aId));
+ },
+
+ /**
+ * Finishes detroying a specific instance of the visualizer.
+ *
+ * @param {String} aId
+ * the identifier of the instance in the visualizers array
+ */
+ _finish: function T__finish(aId)
+ {
+ let contentWindow = this.visualizers[aId].presenter.contentWindow;
+ this.visualizers[aId].removeOverlay();
+ this.visualizers[aId].cleanup();
+ this.visualizers[aId] = null;
+
+ this._isDestroying = false;
+ this.chromeWindow.gBrowser.selectedBrowser.focus();
+ this.emit("change", this.chromeWindow.gBrowser.selectedTab);
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.DESTROYED, null);
+ },
+
+ /**
+ * Handles the event fired when a tab is selected.
+ */
+ _onTabSelect: function T__onTabSelect()
+ {
+ if (this.visualizers[this.lastInstanceId]) {
+ let contentWindow = this.visualizers[this.lastInstanceId].presenter.contentWindow;
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.HIDDEN, null);
+ }
+
+ if (this.currentInstance) {
+ let contentWindow = this.currentInstance.presenter.contentWindow;
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.SHOWN, null);
+ }
+
+ this.lastInstanceId = this.currentWindowId;
+ },
+
+ /**
+ * Add the browser event listeners to handle state changes.
+ */
+ setup: function T_setup()
+ {
+ // load the preferences from the devtools.tilt branch
+ TiltVisualizer.Prefs.load();
+
+ this.chromeWindow.gBrowser.tabContainer.addEventListener(
+ "TabSelect", this._onTabSelect.bind(this), false);
+ },
+
+ /**
+ * Returns true if this tool is enabled.
+ */
+ get enabled()
+ {
+ return (TiltVisualizer.Prefs.enabled &&
+ (TiltGL.isWebGLForceEnabled() || TiltGL.isWebGLSupported()));
+ },
+
+ /**
+ * Gets the ID of the current window object to identify the visualizer.
+ */
+ get currentWindowId()
+ {
+ return TiltUtils.getWindowId(
+ this.chromeWindow.gBrowser.selectedBrowser.contentWindow);
+ },
+
+ /**
+ * Gets the visualizer instance for the current tab.
+ */
+ get currentInstance()
+ {
+ return this.visualizers[this.currentWindowId];
+ },
+};
diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm
new file mode 100644
index 000000000..9133495dc
--- /dev/null
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -0,0 +1,744 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* 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 = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource:///modules/devtools/gDevTools.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
+ "resource://gre/modules/devtools/dbg-server.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
+ "resource://gre/modules/devtools/dbg-client.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Heritage",
+ "resource:///modules/devtools/ViewHelpers.jsm");
+
+let Telemetry = devtools.require("devtools/shared/telemetry");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let 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.";
+
+this.EXPORTED_SYMBOLS = ["HUDService"];
+
+///////////////////////////////////////////////////////////////////////////
+//// The HUD service
+
+function HUD_SERVICE()
+{
+ this.hudReferences = {};
+}
+
+HUD_SERVICE.prototype =
+{
+ /**
+ * Keeps a reference for each HeadsUpDisplay that is created
+ * @type object
+ */
+ hudReferences: null,
+
+ /**
+ * getter for UI commands to be used by the frontend
+ *
+ * @returns object
+ */
+ get consoleUI() {
+ return HeadsUpDisplayUICommands;
+ },
+
+ /**
+ * Firefox-specific current tab getter
+ *
+ * @returns nsIDOMWindow
+ */
+ currentContext: function HS_currentContext() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ },
+
+ /**
+ * 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.hudReferences[hud.hudId] = hud;
+ return hud.init();
+ },
+
+ /**
+ * Open a Browser Console for the given target.
+ *
+ * @see devtools/framework/Target.jsm 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.hudReferences[hud.hudId] = hud;
+ return hud.init();
+ },
+
+ /**
+ * Returns the HeadsUpDisplay object associated to a content window.
+ *
+ * @param nsIDOMWindow aContentWindow
+ * @returns object
+ */
+ getHudByWindow: function HS_getHudByWindow(aContentWindow)
+ {
+ for each (let hud in this.hudReferences) {
+ let target = hud.target;
+ if (target && target.tab && target.window === aContentWindow) {
+ return hud;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the hudId that is corresponding to the hud activated for the
+ * passed aContentWindow. If there is no matching hudId null is returned.
+ *
+ * @param nsIDOMWindow aContentWindow
+ * @returns string or null
+ */
+ getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow)
+ {
+ let hud = this.getHudByWindow(aContentWindow);
+ return hud ? hud.hudId : null;
+ },
+
+ /**
+ * Returns the hudReference for a given id.
+ *
+ * @param string aId
+ * @returns Object
+ */
+ getHudReferenceById: function HS_getHudReferenceById(aId)
+ {
+ return aId in this.hudReferences ? this.hudReferences[aId] : 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 function
+ */
+ lastFinishedRequestCallback: null,
+};
+
+
+/**
+ * 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_" + Date.now();
+ this.target = aTarget;
+
+ this.browserWindow = this.chromeWindow.top;
+
+ let element = this.browserWindow.document.documentElement;
+ if (element.getAttribute("windowtype") != "navigator:browser") {
+ this.browserWindow = HUDService.currentContext();
+ }
+
+ this.ui = new this.iframeWindow.WebConsoleFrame(this);
+}
+
+WebConsole.prototype = {
+ iframeWindow: null,
+ chromeWindow: null,
+ browserWindow: null,
+ hudId: null,
+ target: null,
+ ui: null,
+ _browserConsole: false,
+ _destroyer: null,
+
+ /**
+ * Getter for HUDService.lastFinishedRequestCallback.
+ *
+ * @see HUDService.lastFinishedRequestCallback
+ * @type function
+ */
+ get lastFinishedRequestCallback() HUDService.lastFinishedRequestCallback,
+
+ /**
+ * Getter for the xul:popupset that holds any popups we open.
+ * @type nsIDOMElement
+ */
+ get mainPopupSet()
+ {
+ return this.browserWindow.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() this.browserWindow.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.browserWindow.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)
+ {
+ this.gViewSourceUtils.viewSource(aSourceURL, null,
+ this.iframeWindow.document, aSourceLine);
+ },
+
+ /**
+ * 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.
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ * TODO: This function breaks the client-server boundaries.
+ * To be fixed in bug 793259.
+ */
+ viewSourceInStyleEditor:
+ function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+
+ gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
+ try {
+ toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
+ } catch(e) {
+ // Open view source if style editor fails.
+ this.viewSource(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.
+ *
+ * @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 self = this;
+ let panelWin = null;
+ let debuggerWasOpen = true;
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ self.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+
+ if (!toolbox.getPanel("jsdebugger")) {
+ debuggerWasOpen = false;
+ let toolboxWin = toolbox.doc.defaultView;
+ toolboxWin.addEventListener("Debugger:AfterSourcesAdded",
+ function afterSourcesAdded() {
+ toolboxWin.removeEventListener("Debugger:AfterSourcesAdded",
+ afterSourcesAdded);
+ loadScript();
+ });
+ }
+
+ toolbox.selectTool("jsdebugger").then(function onDebuggerOpen(dbg) {
+ panelWin = dbg.panelWin;
+ if (debuggerWasOpen) {
+ loadScript();
+ }
+ });
+
+ function loadScript() {
+ let debuggerView = panelWin.DebuggerView;
+ if (!debuggerView.Sources.containsValue(aSourceURL)) {
+ toolbox.selectTool("webconsole");
+ self.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+ if (debuggerWasOpen && debuggerView.Sources.selectedValue == aSourceURL) {
+ debuggerView.editor.setCaretPosition(aSourceLine - 1);
+ return;
+ }
+
+ panelWin.addEventListener("Debugger:SourceShown", onSource, false);
+ debuggerView.Sources.preferredSource = aSourceURL;
+ }
+
+ function onSource(aEvent) {
+ if (aEvent.detail.url != aSourceURL) {
+ return;
+ }
+ panelWin.removeEventListener("Debugger:SourceShown", onSource, false);
+ panelWin.DebuggerView.editor.setCaretPosition(aSourceLine - 1);
+ }
+ },
+
+ /**
+ * 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;
+ }
+ let framesController = panel.panelWin.gStackFrames;
+ let thread = framesController.activeThread;
+ if (thread && thread.paused) {
+ return {
+ frames: thread.cachedFrames,
+ selected: framesController.currentFrame,
+ };
+ }
+ return null;
+ },
+
+ /**
+ * 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;
+ }
+
+ delete HUDService.hudReferences[this.hudId];
+
+ this._destroyer = Promise.defer();
+
+ let popupset = this.mainPopupSet;
+ let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
+ for (let panel of panels) {
+ panel.hidePopup();
+ }
+
+ let onDestroy = function WC_onDestroyUI() {
+ try {
+ let tabWindow = this.target.isLocalTab ? this.target.window : null;
+ tabWindow && tabWindow.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 = Heritage.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);
+ this.destroy();
+ };
+ window.addEventListener("unload", onClose);
+
+ // Make sure Ctrl-W closes the Browser Console window.
+ window.document.getElementById("cmd_close").removeAttribute("disabled");
+
+ this._telemetry.toolOpened("browserconsole");
+
+ 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(() => {
+ HeadsUpDisplayUICommands._browserConsoleID = null;
+ chromeWindow.close();
+ this._bc_destroyer.resolve(null);
+ }));
+
+ return this._bc_destroyer.promise;
+ },
+});
+
+
+//////////////////////////////////////////////////////////////////////////
+// HeadsUpDisplayUICommands
+//////////////////////////////////////////////////////////////////////////
+
+var HeadsUpDisplayUICommands = {
+ _browserConsoleID: null,
+ _browserConsoleDefer: null,
+
+ /**
+ * Toggle the Web Console for the current tab.
+ *
+ * @return object
+ * A Promise for either the opening of the toolbox that holds the Web
+ * Console, or a Promise for the closing of the toolbox.
+ */
+ toggleHUD: function UIC_toggleHUD()
+ {
+ let window = HUDService.currentContext();
+ let target = devtools.TargetFactory.forTab(window.gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ return toolbox && toolbox.currentToolId == "webconsole" ?
+ toolbox.destroy() :
+ gDevTools.showToolbox(target, "webconsole");
+ },
+
+ /**
+ * 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.
+ */
+ getOpenHUD: function UIC_getOpenHUD()
+ {
+ let tab = HUDService.currentContext().gBrowser.selectedTab;
+ if (!tab || !devtools.TargetFactory.isKnownTab(tab)) {
+ return null;
+ }
+ let target = devtools.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 UIC_toggleBrowserConsole()
+ {
+ if (this._browserConsoleID) {
+ let hud = HUDService.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();
+ }
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ client.connect(() =>
+ client.listTabs((aResponse) => {
+ // Add Global Process debugging...
+ let globals = JSON.parse(JSON.stringify(aResponse));
+ delete globals.tabs;
+ delete globals.selected;
+ // ...only if there are appropriate actors (a 'from' property will
+ // always be there).
+ if (Object.keys(globals).length > 1) {
+ deferred.resolve({ form: globals, client: client, chrome: true });
+ } else {
+ deferred.reject("Global console not found!");
+ }
+ }));
+
+ return deferred.promise;
+ }
+
+ let target;
+ function getTarget(aConnection)
+ {
+ let options = {
+ form: aConnection.form,
+ client: aConnection.client,
+ chrome: true,
+ };
+
+ return devtools.TargetFactory.forRemoteTab(options);
+ }
+
+ function openWindow(aTarget)
+ {
+ target = aTarget;
+
+ let deferred = Promise.defer();
+
+ let win = Services.ww.openWindow(null, devtools.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) =>
+ HUDService.openBrowserConsole(target, aWindow, aWindow)
+ .then((aBrowserConsole) => {
+ this._browserConsoleID = aBrowserConsole.hudId;
+ this._browserConsoleDefer.resolve(aBrowserConsole);
+ this._browserConsoleDefer = null;
+ }));
+
+ return this._browserConsoleDefer.promise;
+ },
+
+ get browserConsole() {
+ return HUDService.getHudReferenceById(this._browserConsoleID);
+ },
+};
+
+const HUDService = new HUD_SERVICE();
+
diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in
new file mode 100644
index 000000000..0bd782f05
--- /dev/null
+++ b/browser/devtools/webconsole/Makefile.in
@@ -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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_JS_MODULES = \
+ HUDService.jsm \
+ NetworkPanel.jsm \
+ WebConsolePanel.jsm \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/framework
diff --git a/browser/devtools/webconsole/NetworkPanel.jsm b/browser/devtools/webconsole/NetworkPanel.jsm
new file mode 100644
index 000000000..e6d634f7b
--- /dev/null
+++ b/browser/devtools/webconsole/NetworkPanel.jsm
@@ -0,0 +1,847 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* 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 = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1",
+ "nsIMIMEService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+this.EXPORTED_SYMBOLS = ["NetworkPanel"];
+
+/**
+ * Creates a new NetworkPanel.
+ *
+ * @constructor
+ * @param nsIDOMNode aParent
+ * Parent node to append the created panel to.
+ * @param object aHttpActivity
+ * HttpActivity to display in the panel.
+ * @param object aWebConsoleFrame
+ * The parent WebConsoleFrame object that owns this network panel
+ * instance.
+ */
+this.NetworkPanel =
+function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame)
+{
+ let doc = aParent.ownerDocument;
+ this.httpActivity = aHttpActivity;
+ this.webconsole = aWebConsoleFrame;
+ this._longStringClick = this._longStringClick.bind(this);
+ this._responseBodyFetch = this._responseBodyFetch.bind(this);
+ this._requestBodyFetch = this._requestBodyFetch.bind(this);
+
+ // Create the underlaying panel
+ this.panel = createElement(doc, "panel", {
+ label: l10n.getStr("NetworkPanel.label"),
+ titlebar: "normal",
+ noautofocus: "true",
+ noautohide: "true",
+ close: "true"
+ });
+
+ // Create the iframe that displays the NetworkPanel XHTML.
+ this.iframe = createAndAppendElement(this.panel, "iframe", {
+ src: "chrome://browser/content/devtools/NetworkPanel.xhtml",
+ type: "content",
+ flex: "1"
+ });
+
+ let self = this;
+
+ // Destroy the panel when it's closed.
+ this.panel.addEventListener("popuphidden", function onPopupHide() {
+ self.panel.removeEventListener("popuphidden", onPopupHide, false);
+ self.panel.parentNode.removeChild(self.panel);
+ self.panel = null;
+ self.iframe = null;
+ self.httpActivity = null;
+ self.webconsole = null;
+
+ if (self.linkNode) {
+ self.linkNode._panelOpen = false;
+ self.linkNode = null;
+ }
+ }, false);
+
+ // Set the document object and update the content once the panel is loaded.
+ this.iframe.addEventListener("load", function onLoad() {
+ if (!self.iframe) {
+ return;
+ }
+
+ self.iframe.removeEventListener("load", onLoad, true);
+ self.update();
+ }, true);
+
+ this.panel.addEventListener("popupshown", function onPopupShown() {
+ self.panel.removeEventListener("popupshown", onPopupShown, true);
+ self.update();
+ }, true);
+
+ // Create the footer.
+ let footer = createElement(doc, "hbox", { align: "end" });
+ createAndAppendElement(footer, "spacer", { flex: 1 });
+
+ createAndAppendElement(footer, "resizer", { dir: "bottomend" });
+ this.panel.appendChild(footer);
+
+ aParent.appendChild(this.panel);
+}
+
+NetworkPanel.prototype =
+{
+ /**
+ * The current state of the output.
+ */
+ _state: 0,
+
+ /**
+ * State variables.
+ */
+ _INIT: 0,
+ _DISPLAYED_REQUEST_HEADER: 1,
+ _DISPLAYED_REQUEST_BODY: 2,
+ _DISPLAYED_RESPONSE_HEADER: 3,
+ _TRANSITION_CLOSED: 4,
+
+ _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,
+
+ _contentType: null,
+
+ /**
+ * Function callback invoked whenever the panel content is updated. This is
+ * used only by tests.
+ *
+ * @private
+ * @type function
+ */
+ _onUpdate: null,
+
+ get document() {
+ return this.iframe && this.iframe.contentWindow ?
+ this.iframe.contentWindow.document : null;
+ },
+
+ /**
+ * Small helper function that is nearly equal to l10n.getFormatStr
+ * except that it prefixes aName with "NetworkPanel.".
+ *
+ * @param string aName
+ * The name of an i10n string to format. This string is prefixed with
+ * "NetworkPanel." before calling the HUDService.getFormatStr function.
+ * @param array aArray
+ * Values used as placeholder for the i10n string.
+ * @returns string
+ * The i10n formated string.
+ */
+ _format: function NP_format(aName, aArray)
+ {
+ return l10n.getFormatStr("NetworkPanel." + aName, aArray);
+ },
+
+ /**
+ * Returns the content type of the response body. This is based on the
+ * response.content.mimeType property. If this value is not available, then
+ * the content type is guessed by the file extension of the request URL.
+ *
+ * @return string
+ * Content type or empty string if no content type could be figured
+ * out.
+ */
+ get contentType()
+ {
+ if (this._contentType) {
+ return this._contentType;
+ }
+
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+
+ let contentType = "";
+ let types = response.content ?
+ (response.content.mimeType || "").split(/,|;/) : [];
+ for (let i = 0; i < types.length; i++) {
+ if (types[i] in NetworkHelper.mimeCategoryMap) {
+ contentType = types[i];
+ break;
+ }
+ }
+
+ if (contentType) {
+ this._contentType = contentType;
+ return contentType;
+ }
+
+ // Try to get the content type from the request file extension.
+ let uri = NetUtil.newURI(request.url);
+ if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
+ try {
+ contentType = mimeService.getTypeFromExtension(uri.fileExtension);
+ }
+ catch(ex) {
+ // Added to prevent failures on OS X 64. No Flash?
+ Cu.reportError(ex);
+ }
+ }
+
+ this._contentType = contentType;
+ return contentType;
+ },
+
+ /**
+ *
+ * @returns boolean
+ * True if the response is an image, false otherwise.
+ */
+ get _responseIsImage()
+ {
+ return this.contentType &&
+ NetworkHelper.mimeCategoryMap[this.contentType] == "image";
+ },
+
+ /**
+ *
+ * @returns boolean
+ * True if the response body contains text, false otherwise.
+ */
+ get _isResponseBodyTextData()
+ {
+ return this.contentType ?
+ NetworkHelper.isTextMimeType(this.contentType) : false;
+ },
+
+ /**
+ * Tells if the server response is cached.
+ *
+ * @returns boolean
+ * Returns true if the server responded that the request is already
+ * in the browser's cache, false otherwise.
+ */
+ get _isResponseCached()
+ {
+ return this.httpActivity.response.status == 304;
+ },
+
+ /**
+ * Tells if the request body includes form data.
+ *
+ * @returns boolean
+ * Returns true if the posted body contains form data.
+ */
+ get _isRequestBodyFormData()
+ {
+ let requestBody = this.httpActivity.request.postData.text;
+ if (typeof requestBody == "object" && requestBody.type == "longString") {
+ requestBody = requestBody.initial;
+ }
+ return this._fromDataRegExp.test(requestBody);
+ },
+
+ /**
+ * Appends the node with id=aId by the text aValue.
+ *
+ * @private
+ * @param string aId
+ * @param string aValue
+ * @return nsIDOMElement
+ * The DOM element with id=aId.
+ */
+ _appendTextNode: function NP__appendTextNode(aId, aValue)
+ {
+ let textNode = this.document.createTextNode(aValue);
+ let elem = this.document.getElementById(aId);
+ elem.appendChild(textNode);
+ return elem;
+ },
+
+ /**
+ * Generates some HTML to display the key-value pair of the aList data. The
+ * generated HTML is added to node with id=aParentId.
+ *
+ * @param string aParentId
+ * Id of the parent node to append the list to.
+ * @oaram array aList
+ * Array that holds the objects you want to display. Each object must
+ * have two properties: name and value.
+ * @param boolean aIgnoreCookie
+ * If true, the key-value named "Cookie" is not added to the list.
+ * @returns void
+ */
+ _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
+ {
+ let parent = this.document.getElementById(aParentId);
+ let doc = this.document;
+
+ aList.sort(function(a, b) {
+ return a.name.toLowerCase() < b.name.toLowerCase();
+ });
+
+ aList.forEach(function(aItem) {
+ let name = aItem.name;
+ if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) {
+ return;
+ }
+
+ let value = aItem.value;
+ let longString = null;
+ if (typeof value == "object" && value.type == "longString") {
+ value = value.initial;
+ longString = true;
+ }
+
+ /**
+ * The following code creates the HTML:
+ * <tr>
+ * <th scope="row" class="property-name">${line}:</th>
+ * <td class="property-value">${aList[line]}</td>
+ * </tr>
+ * and adds it to parent.
+ */
+ let row = doc.createElement("tr");
+ let textNode = doc.createTextNode(name + ":");
+ let th = doc.createElement("th");
+ th.setAttribute("scope", "row");
+ th.setAttribute("class", "property-name");
+ th.appendChild(textNode);
+ row.appendChild(th);
+
+ textNode = doc.createTextNode(value);
+ let td = doc.createElement("td");
+ td.setAttribute("class", "property-value");
+ td.appendChild(textNode);
+
+ if (longString) {
+ let a = doc.createElement("a");
+ a.href = "#";
+ a.className = "longStringEllipsis";
+ a.addEventListener("mousedown", this._longStringClick.bind(this, aItem));
+ a.textContent = l10n.getStr("longStringEllipsis");
+ td.appendChild(a);
+ }
+
+ row.appendChild(td);
+
+ parent.appendChild(row);
+ }.bind(this));
+ },
+
+ /**
+ * The click event handler for the ellipsis which allows the user to retrieve
+ * the full header value.
+ *
+ * @private
+ * @param object aHeader
+ * The header object with the |name| and |value| properties.
+ * @param nsIDOMEvent aEvent
+ * The DOM click event object.
+ */
+ _longStringClick: function NP__longStringClick(aHeader, aEvent)
+ {
+ aEvent.preventDefault();
+
+ let longString = this.webconsole.webConsoleClient.longString(aHeader.value);
+
+ longString.substring(longString.initial.length, longString.length,
+ function NP__onLongStringSubstring(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ aHeader.value = aHeader.value.initial + aResponse.substring;
+
+ let textNode = aEvent.target.previousSibling;
+ textNode.textContent += aResponse.substring;
+ textNode.parentNode.removeChild(aEvent.target);
+ });
+ },
+
+ /**
+ * Displays the node with id=aId.
+ *
+ * @private
+ * @param string aId
+ * @return nsIDOMElement
+ * The element with id=aId.
+ */
+ _displayNode: function NP__displayNode(aId)
+ {
+ let elem = this.document.getElementById(aId);
+ elem.style.display = "block";
+ },
+
+ /**
+ * Sets the request URL, request method, the timing information when the
+ * request started and the request header content on the NetworkPanel.
+ * If the request header contains cookie data, a list of sent cookies is
+ * generated and a special sent cookie section is displayed + the cookie list
+ * added to it.
+ *
+ * @returns void
+ */
+ _displayRequestHeader: function NP__displayRequestHeader()
+ {
+ let request = this.httpActivity.request;
+ let requestTime = new Date(this.httpActivity.startedDateTime);
+
+ this._appendTextNode("headUrl", request.url);
+ this._appendTextNode("headMethod", request.method);
+ this._appendTextNode("requestHeadersInfo",
+ l10n.timestampString(requestTime));
+
+ this._appendList("requestHeadersContent", request.headers, true);
+
+ if (request.cookies.length > 0) {
+ this._displayNode("requestCookie");
+ this._appendList("requestCookieContent", request.cookies);
+ }
+ },
+
+ /**
+ * Displays the request body section of the NetworkPanel and set the request
+ * body content on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayRequestBody: function NP__displayRequestBody()
+ {
+ let postData = this.httpActivity.request.postData;
+ this._displayNode("requestBody");
+ this._appendTextNode("requestBodyContent", postData.text);
+ },
+
+ /*
+ * Displays the `sent form data` section. Parses the request header for the
+ * submitted form data displays it inside of the `sent form data` section.
+ *
+ * @returns void
+ */
+ _displayRequestForm: function NP__processRequestForm()
+ {
+ let postData = this.httpActivity.request.postData.text;
+ let requestBodyLines = postData.split("\n");
+ let formData = requestBodyLines[requestBodyLines.length - 1].
+ replace(/\+/g, " ").split("&");
+
+ function unescapeText(aText)
+ {
+ try {
+ return decodeURIComponent(aText);
+ }
+ catch (ex) {
+ return decodeURIComponent(unescape(aText));
+ }
+ }
+
+ let formDataArray = [];
+ for (let i = 0; i < formData.length; i++) {
+ let data = formData[i];
+ let idx = data.indexOf("=");
+ let key = data.substring(0, idx);
+ let value = data.substring(idx + 1);
+ formDataArray.push({
+ name: unescapeText(key),
+ value: unescapeText(value)
+ });
+ }
+
+ this._appendList("requestFormDataContent", formDataArray);
+ this._displayNode("requestFormData");
+ },
+
+ /**
+ * Displays the response section of the NetworkPanel, sets the response status,
+ * the duration between the start of the request and the receiving of the
+ * response header as well as the response header content on the the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseHeader: function NP__displayResponseHeader()
+ {
+ let timing = this.httpActivity.timings;
+ let response = this.httpActivity.response;
+
+ this._appendTextNode("headStatus",
+ [response.httpVersion, response.status,
+ response.statusText].join(" "));
+
+ // Calculate how much time it took from the request start, until the
+ // response started to be received.
+ let deltaDuration = 0;
+ ["dns", "connect", "send", "wait"].forEach(function (aValue) {
+ let ms = timing[aValue];
+ if (ms > -1) {
+ deltaDuration += ms;
+ }
+ });
+
+ this._appendTextNode("responseHeadersInfo",
+ this._format("durationMS", [deltaDuration]));
+
+ this._displayNode("responseContainer");
+ this._appendList("responseHeadersContent", response.headers, true);
+
+ if (response.cookies.length > 0) {
+ this._displayNode("responseCookie");
+ this._appendList("responseCookieContent", response.cookies);
+ }
+ },
+
+ /**
+ * Displays the respones image section, sets the source of the image displayed
+ * in the image response section to the request URL and the duration between
+ * the receiving of the response header and the end of the request. Once the
+ * image is loaded, the size of the requested image is set.
+ *
+ * @returns void
+ */
+ _displayResponseImage: function NP__displayResponseImage()
+ {
+ let self = this;
+ let timing = this.httpActivity.timings;
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+ let cached = "";
+
+ if (this._isResponseCached) {
+ cached = "Cached";
+ }
+
+ let imageNode = this.document.getElementById("responseImage" +
+ cached + "Node");
+
+ let text = response.content.text;
+ if (typeof text == "object" && text.type == "longString") {
+ this._showResponseBodyFetchLink();
+ }
+ else {
+ imageNode.setAttribute("src",
+ "data:" + this.contentType + ";base64," + text);
+ }
+
+ // This function is called to set the imageInfo.
+ function setImageInfo() {
+ self._appendTextNode("responseImage" + cached + "Info",
+ self._format("imageSizeDeltaDurationMS",
+ [ imageNode.width, imageNode.height, timing.receive ]
+ )
+ );
+ }
+
+ // Check if the image is already loaded.
+ if (imageNode.width != 0) {
+ setImageInfo();
+ }
+ else {
+ // Image is not loaded yet therefore add a load event.
+ imageNode.addEventListener("load", function imageNodeLoad() {
+ imageNode.removeEventListener("load", imageNodeLoad, false);
+ setImageInfo();
+ }, false);
+ }
+
+ this._displayNode("responseImage" + cached);
+ },
+
+ /**
+ * Displays the response body section, sets the the duration between
+ * the receiving of the response header and the end of the request as well as
+ * the content of the response body on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseBody: function NP__displayResponseBody()
+ {
+ let timing = this.httpActivity.timings;
+ let response = this.httpActivity.response;
+ let cached = this._isResponseCached ? "Cached" : "";
+
+ this._appendTextNode("responseBody" + cached + "Info",
+ this._format("durationMS", [timing.receive]));
+
+ this._displayNode("responseBody" + cached);
+
+ let text = response.content.text;
+ if (typeof text == "object") {
+ text = text.initial;
+ this._showResponseBodyFetchLink();
+ }
+
+ this._appendTextNode("responseBody" + cached + "Content", text);
+ },
+
+ /**
+ * Show the "fetch response body" link.
+ * @private
+ */
+ _showResponseBodyFetchLink: function NP__showResponseBodyFetchLink()
+ {
+ let content = this.httpActivity.response.content;
+
+ let elem = this._appendTextNode("responseBodyFetchLink",
+ this._format("fetchRemainingResponseContentLink",
+ [content.text.length - content.text.initial.length]));
+
+ elem.style.display = "block";
+ elem.addEventListener("mousedown", this._responseBodyFetch);
+ },
+
+ /**
+ * Click event handler for the link that allows users to fetch the remaining
+ * response body.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _responseBodyFetch: function NP__responseBodyFetch(aEvent)
+ {
+ aEvent.target.style.display = "none";
+ aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
+
+ let content = this.httpActivity.response.content;
+ let longString = this.webconsole.webConsoleClient.longString(content.text);
+ longString.substring(longString.initial.length, longString.length,
+ function NP__onLongStringSubstring(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ content.text = content.text.initial + aResponse.substring;
+ let cached = this._isResponseCached ? "Cached" : "";
+
+ if (this._responseIsImage) {
+ let imageNode = this.document.getElementById("responseImage" +
+ cached + "Node");
+ imageNode.src =
+ "data:" + this.contentType + ";base64," + content.text;
+ }
+ else {
+ this._appendTextNode("responseBody" + cached + "Content",
+ aResponse.substring);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Displays the `Unknown Content-Type hint` and sets the duration between the
+ * receiving of the response header on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType()
+ {
+ let timing = this.httpActivity.timings;
+
+ this._displayNode("responseBodyUnknownType");
+ this._appendTextNode("responseBodyUnknownTypeInfo",
+ this._format("durationMS", [timing.receive]));
+
+ this._appendTextNode("responseBodyUnknownTypeContent",
+ this._format("responseBodyUnableToDisplay.content", [this.contentType]));
+ },
+
+ /**
+ * Displays the `no response body` section and sets the the duration between
+ * the receiving of the response header and the end of the request.
+ *
+ * @returns void
+ */
+ _displayNoResponseBody: function NP_displayNoResponseBody()
+ {
+ let timing = this.httpActivity.timings;
+
+ this._displayNode("responseNoBody");
+ this._appendTextNode("responseNoBodyInfo",
+ this._format("durationMS", [timing.receive]));
+ },
+
+ /**
+ * Updates the content of the NetworkPanel's iframe.
+ *
+ * @returns void
+ */
+ update: function NP_update()
+ {
+ if (!this.document || this.document.readyState != "complete") {
+ return;
+ }
+
+ let updates = this.httpActivity.updates;
+ let timing = this.httpActivity.timings;
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+
+ switch (this._state) {
+ case this._INIT:
+ this._displayRequestHeader();
+ this._state = this._DISPLAYED_REQUEST_HEADER;
+ // FALL THROUGH
+
+ case this._DISPLAYED_REQUEST_HEADER:
+ // Process the request body if there is one.
+ if (!this.httpActivity.discardRequestBody && request.postData.text) {
+ this._updateRequestBody();
+ this._state = this._DISPLAYED_REQUEST_BODY;
+ }
+ // FALL THROUGH
+
+ case this._DISPLAYED_REQUEST_BODY:
+ if (!response.headers.length || !Object.keys(timing).length) {
+ break;
+ }
+ this._displayResponseHeader();
+ this._state = this._DISPLAYED_RESPONSE_HEADER;
+ // FALL THROUGH
+
+ case this._DISPLAYED_RESPONSE_HEADER:
+ if (updates.indexOf("responseContent") == -1 ||
+ updates.indexOf("eventTimings") == -1) {
+ break;
+ }
+
+ this._state = this._TRANSITION_CLOSED;
+ if (this.httpActivity.discardResponseBody) {
+ break;
+ }
+
+ if (!response.content || !response.content.text) {
+ this._displayNoResponseBody();
+ }
+ else if (this._responseIsImage) {
+ this._displayResponseImage();
+ }
+ else if (!this._isResponseBodyTextData) {
+ this._displayResponseBodyUnknownType();
+ }
+ else if (response.content.text) {
+ this._displayResponseBody();
+ }
+ break;
+ }
+
+ if (this._onUpdate) {
+ this._onUpdate();
+ }
+ },
+
+ /**
+ * Update the panel to hold the current information we have about the request
+ * body.
+ * @private
+ */
+ _updateRequestBody: function NP__updateRequestBody()
+ {
+ let postData = this.httpActivity.request.postData;
+ if (typeof postData.text == "object" && postData.text.type == "longString") {
+ let elem = this._appendTextNode("requestBodyFetchLink",
+ this._format("fetchRemainingRequestContentLink",
+ [postData.text.length - postData.text.initial.length]));
+
+ elem.style.display = "block";
+ elem.addEventListener("mousedown", this._requestBodyFetch);
+ return;
+ }
+
+ // Check if we send some form data. If so, display the form data special.
+ if (this._isRequestBodyFormData) {
+ this._displayRequestForm();
+ }
+ else {
+ this._displayRequestBody();
+ }
+ },
+
+ /**
+ * Click event handler for the link that allows users to fetch the remaining
+ * request body.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _requestBodyFetch: function NP__requestBodyFetch(aEvent)
+ {
+ aEvent.target.style.display = "none";
+ aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
+
+ let postData = this.httpActivity.request.postData;
+ let longString = this.webconsole.webConsoleClient.longString(postData.text);
+ longString.substring(longString.initial.length, longString.length,
+ function NP__onLongStringSubstring(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ postData.text = postData.text.initial + aResponse.substring;
+ this._updateRequestBody();
+ }.bind(this));
+ },
+};
+
+/**
+ * Creates a DOMNode and sets all the attributes of aAttributes on the created
+ * element.
+ *
+ * @param nsIDOMDocument aDocument
+ * Document to create the new DOMNode.
+ * @param string aTag
+ * Name of the tag for the DOMNode.
+ * @param object aAttributes
+ * Attributes set on the created DOMNode.
+ *
+ * @returns nsIDOMNode
+ */
+function createElement(aDocument, aTag, aAttributes)
+{
+ let node = aDocument.createElement(aTag);
+ if (aAttributes) {
+ for (let attr in aAttributes) {
+ node.setAttribute(attr, aAttributes[attr]);
+ }
+ }
+ return node;
+}
+
+/**
+ * Creates a new DOMNode and appends it to aParent.
+ *
+ * @param nsIDOMNode aParent
+ * A parent node to append the created element.
+ * @param string aTag
+ * Name of the tag for the DOMNode.
+ * @param object aAttributes
+ * Attributes set on the created DOMNode.
+ *
+ * @returns nsIDOMNode
+ */
+function createAndAppendElement(aParent, aTag, aAttributes)
+{
+ let node = createElement(aParent.ownerDocument, aTag, aAttributes);
+ aParent.appendChild(node);
+ return node;
+}
diff --git a/browser/devtools/webconsole/NetworkPanel.xhtml b/browser/devtools/webconsole/NetworkPanel.xhtml
new file mode 100644
index 000000000..62753ae4f
--- /dev/null
+++ b/browser/devtools/webconsole/NetworkPanel.xhtml
@@ -0,0 +1,124 @@
+<?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" [
+<!ENTITY % webConsoleDTD SYSTEM "chrome://browser/locale/devtools/webConsole.dtd" >
+%webConsoleDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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" xml:lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/webconsole_networkpanel.css" type="text/css"/>
+</head>
+<body role="application">
+<table id="header">
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.requestURLColon;</th>
+ <td class="property-value"
+ id="headUrl"></td>
+ </tr>
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.requestMethodColon;</th>
+ <td class="property-value"
+ id="headMethod"></td>
+ </tr>
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.statusCodeColon;</th>
+ <td class="property-value"
+ id="headStatus"></td>
+ </tr>
+</table>
+
+<div class="group">
+ <h1>
+ &networkPanel.requestHeaders;
+ <span id="requestHeadersInfo" class="info"></span>
+ </h1>
+ <table class="property-table" id="requestHeadersContent"></table>
+
+ <div id="requestCookie" style="display:none">
+ <h1>&networkPanel.requestCookie;</h1>
+ <table class="property-table" id="requestCookieContent"></table>
+ </div>
+
+ <div id="requestBody" style="display:none">
+ <h1>&networkPanel.requestBody;</h1>
+ <table class="property-table" id="requestBodyContent"></table>
+ </div>
+ <div id="requestFormData" style="display:none">
+ <h1>&networkPanel.requestFormData;</h1>
+ <table class="property-table" id="requestFormDataContent"></table>
+ </div>
+ <p id="requestBodyFetchLink" style="display:none"></p>
+</div>
+
+<div class="group" id="responseContainer" style="display:none">
+ <h1>
+ &networkPanel.responseHeaders;
+ <span id="responseHeadersInfo" class="info">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseHeadersContent"></table>
+
+ <div id="responseCookie" style="display:none">
+ <h1>&networkPanel.responseCookie;</h1>
+ <table class="property-table" id="responseCookieContent"></table>
+ </div>
+
+ <div id="responseBody" style="display:none">
+ <h1>
+ &networkPanel.responseBody;
+ <span class="info" id="responseBodyInfo">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyContent"></table>
+ </div>
+ <div id="responseBodyCached" style="display:none">
+ <h1>
+ &networkPanel.responseBodyCached;
+ <span class="info" id="responseBodyCachedInfo">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyCachedContent"></table>
+ </div>
+ <div id="responseNoBody" style="display:none">
+ <h1>
+ &networkPanel.responseNoBody;
+ <span id="responseNoBodyInfo" class="info">&Delta;</span>
+ </h1>
+ </div>
+ <div id="responseBodyUnknownType" style="display:none">
+ <h1>
+ &networkPanel.responseBodyUnknownType;
+ <span id="responseBodyUnknownTypeInfo" class="info">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyUnknownTypeContent"></table>
+ </div>
+ <div id="responseImage" style="display:none">
+ <h1>
+ &networkPanel.responseImage;
+ <span id="responseImageInfo" class="info"></span>
+ </h1>
+ <div id="responseImageNodeDiv">
+ <img id="responseImageNode" />
+ </div>
+ </div>
+ <div id="responseImageCached" style="display:none">
+ <h1>
+ &networkPanel.responseImageCached;
+ <span id="responseImageCachedInfo" class="info"></span>
+ </h1>
+ <div id="responseImageNodeDiv">
+ <img id="responseImageCachedNode" />
+ </div>
+ </div>
+ <p id="responseBodyFetchLink" style="display:none"></p>
+</div>
+</body>
+</html>
diff --git a/browser/devtools/webconsole/WebConsolePanel.jsm b/browser/devtools/webconsole/WebConsolePanel.jsm
new file mode 100644
index 000000000..c170d91f2
--- /dev/null
+++ b/browser/devtools/webconsole/WebConsolePanel.jsm
@@ -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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "WebConsolePanel" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+ "resource:///modules/HUDService.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource:///modules/devtools/shared/event-emitter.js");
+
+/**
+ * A DevToolPanel that controls the Web Console.
+ */
+function WebConsolePanel(iframeWindow, toolbox) {
+ this._frameWindow = iframeWindow;
+ this._toolbox = toolbox;
+ EventEmitter.decorate(this);
+}
+
+WebConsolePanel.prototype = {
+ hud: null,
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A Promise that is resolved when the Web Console completes opening.
+ */
+ open: function WCP_open()
+ {
+ let parentDoc = this._toolbox.doc;
+ let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole");
+ iframe.className = "web-console-frame";
+
+ // 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((aTarget) => {
+ this._frameWindow._remoteTarget = aTarget;
+
+ let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
+ let chromeWindow = iframe.ownerDocument.defaultView;
+ return HUDService.openWebConsole(this.target, webConsoleUIWindow,
+ chromeWindow);
+ })
+ .then((aWebConsole) => {
+ this.hud = aWebConsole;
+ this._isReady = true;
+ this.emit("ready");
+ return this;
+ }, (aReason) => {
+ let msg = "WebConsolePanel open failed. " +
+ aReason.error + ": " + aReason.message;
+ dump(msg + "\n");
+ Cu.reportError(msg);
+ });
+ },
+
+ get target() this._toolbox.target,
+
+ _isReady: false,
+ get isReady() this._isReady,
+
+ destroy: function WCP_destroy()
+ {
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ this._destroyer = this.hud.destroy();
+ this._destroyer.then(() => this.emit("destroyed"));
+
+ return this._destroyer;
+ },
+};
diff --git a/browser/devtools/webconsole/moz.build b/browser/devtools/webconsole/moz.build
new file mode 100644
index 000000000..5abe8b3be
--- /dev/null
+++ b/browser/devtools/webconsole/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
+
diff --git a/browser/devtools/webconsole/test/Makefile.in b/browser/devtools/webconsole/test/Makefile.in
new file mode 100644
index 000000000..f4a8ca682
--- /dev/null
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -0,0 +1,245 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_webconsole_notifications.js \
+ browser_webconsole_message_node_id.js \
+ browser_webconsole_bug_580030_errors_after_page_reload.js \
+ browser_webconsole_basic_net_logging.js \
+ browser_webconsole_bug_579412_input_focus.js \
+ browser_webconsole_bug_580001_closing_after_completion.js \
+ browser_webconsole_bug_580400_groups.js \
+ browser_webconsole_bug_588730_text_node_insertion.js \
+ browser_webconsole_bug_601667_filter_buttons.js \
+ browser_webconsole_bug_597136_external_script_errors.js \
+ browser_webconsole_bug_597136_network_requests_from_chrome.js \
+ browser_webconsole_completion.js \
+ browser_webconsole_console_logging_api.js \
+ browser_webconsole_change_font_size.js \
+ browser_webconsole_chrome.js \
+ browser_webconsole_execution_scope.js \
+ browser_webconsole_for_of.js \
+ browser_webconsole_history.js \
+ browser_webconsole_js_input_and_output_styling.js \
+ browser_webconsole_js_input_expansion.js \
+ browser_webconsole_live_filtering_of_message_types.js \
+ browser_webconsole_live_filtering_on_search_strings.js \
+ browser_warn_user_about_replaced_api.js \
+ browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js \
+ browser_webconsole_bug_586388_select_all.js \
+ browser_webconsole_bug_588967_input_expansion.js \
+ browser_webconsole_log_node_classes.js \
+ browser_webconsole_network_panel.js \
+ browser_webconsole_jsterm.js \
+ browser_webconsole_null_and_undefined_output.js \
+ browser_webconsole_output_order.js \
+ browser_webconsole_property_provider.js \
+ browser_webconsole_bug_587617_output_copy.js \
+ browser_webconsole_bug_585237_line_limit.js \
+ browser_webconsole_bug_582201_duplicate_errors.js \
+ browser_webconsole_bug_580454_timestamp_l10n.js \
+ browser_webconsole_netlogging.js \
+ browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js \
+ browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js \
+ browser_webconsole_bug_594477_clickable_output.js \
+ browser_webconsole_bug_589162_css_filter.js \
+ browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js \
+ browser_webconsole_bug_595350_multiple_windows_and_tabs.js \
+ browser_webconsole_bug_594497_history_arrow_keys.js \
+ browser_webconsole_bug_588342_document_focus.js \
+ browser_webconsole_bug_595934_message_categories.js \
+ browser_webconsole_bug_601352_scroll.js \
+ browser_webconsole_bug_592442_closing_brackets.js \
+ browser_webconsole_bug_593003_iframe_wrong_hud.js \
+ browser_webconsole_bug_613013_console_api_iframe.js \
+ browser_webconsole_bug_597756_reopen_closed_tab.js \
+ browser_webconsole_bug_600183_charset.js \
+ browser_webconsole_bug_601177_log_levels.js \
+ browser_webconsole_bug_597460_filter_scroll.js \
+ browser_webconsole_console_extras.js \
+ browser_webconsole_bug_598357_jsterm_output.js \
+ browser_webconsole_bug_603750_websocket.js \
+ browser_webconsole_abbreviate_source_url.js \
+ browser_webconsole_view_source.js \
+ browser_webconsole_bug_602572_log_bodies_checkbox.js \
+ browser_webconsole_bug_614793_jsterm_scroll.js \
+ browser_webconsole_bug_599725_response_headers.js \
+ browser_webconsole_bug_613642_maintain_scroll.js \
+ browser_webconsole_bug_613642_prune_scroll.js \
+ browser_webconsole_bug_618078_network_exceptions.js \
+ browser_webconsole_bug_613280_jsterm_copy.js \
+ browser_webconsole_bug_630733_response_redirect_headers.js \
+ browser_webconsole_bug_621644_jsterm_dollar.js \
+ browser_webconsole_bug_632817.js \
+ browser_webconsole_bug_611795.js \
+ browser_webconsole_bug_618311_close_panels.js \
+ browser_webconsole_bug_626484_output_copy_order.js \
+ browser_webconsole_bug_632347_iterators_generators.js \
+ browser_webconsole_bug_642108_pruneTest.js \
+ browser_webconsole_bug_585956_console_trace.js \
+ browser_webconsole_bug_595223_file_uri.js \
+ browser_webconsole_bug_632275_getters_document_width.js \
+ browser_webconsole_bug_644419_log_limits.js \
+ browser_webconsole_bug_646025_console_file_location.js \
+ browser_webconsole_bug_642615_autocomplete.js \
+ browser_webconsole_bug_585991_autocomplete_popup.js \
+ browser_webconsole_bug_585991_autocomplete_keys.js \
+ browser_webconsole_bug_660806_history_nav.js \
+ browser_webconsole_bug_651501_document_body_autocomplete.js \
+ browser_webconsole_bug_653531_highlighter_console_helper.js \
+ browser_webconsole_bug_659907_console_dir.js \
+ browser_webconsole_bug_664131_console_group.js \
+ browser_webconsole_bug_704295.js \
+ browser_webconsole_bug_658368_time_methods.js \
+ browser_webconsole_bug_764572_output_open_url.js \
+ browser_webconsole_bug_622303_persistent_filters.js \
+ browser_webconsole_bug_770099_bad_policyuri.js \
+ browser_webconsole_bug_770099_violation.js \
+ browser_webconsole_bug_766001_JS_Console_in_Debugger.js \
+ browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js \
+ browser_cached_messages.js \
+ browser_bug664688_sandbox_update_after_navigation.js \
+ browser_result_format_as_string.js \
+ browser_webconsole_bug_737873_mixedcontent.js \
+ browser_output_breaks_after_console_dir_uninspectable.js \
+ browser_console_log_inspectable_object.js \
+ browser_bug_638949_copy_link_location.js \
+ browser_output_longstring_expand.js \
+ browser_netpanel_longstring_expand.js \
+ browser_repeated_messages_accuracy.js \
+ browser_webconsole_bug_821877_csp_errors.js \
+ browser_eval_in_debugger_stackframe.js \
+ browser_console_variables_view.js \
+ browser_console_variables_view_while_debugging.js \
+ browser_console.js \
+ browser_longstring_hang.js \
+ browser_console_consolejsm_output.js \
+ browser_webconsole_bug_837351_securityerrors.js \
+ browser_bug_865871_variables_view_close_on_esc_key.js \
+ browser_bug_865288_repeat_different_objects.js \
+ browser_jsterm_inspect.js \
+ browser_bug_869003_inspect_cross_domain_object.js \
+ browser_bug_862916_console_dir_and_filter_off.js \
+ browser_console_native_getters.js \
+ browser_bug_871156_ctrlw_close_tab.js \
+ browser_console_private_browsing.js \
+ browser_console_nsiconsolemessage.js \
+ browser_webconsole_bug_817834_add_edited_input_to_history.js \
+ browser_console_addonsdk_loader_exception.js \
+ browser_console_error_source_click.js \
+ browser_console_clear_on_reload.js \
+ browser_console_keyboard_accessibility.js \
+ browser_console_filters.js \
+ browser_console_dead_objects.js \
+ browser_console_variables_view_while_debugging_and_inspecting.js \
+ head.js \
+ $(NULL)
+
+ifeq ($(OS_ARCH), Darwin)
+MOCHITEST_BROWSER_FILES += \
+ browser_webconsole_bug_804845_ctrl_key_nav.js \
+ $(NULL)
+endif
+
+ifeq ($(OS_ARCH),WINNT)
+MOCHITEST_BROWSER_FILES += \
+ browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js \
+ $(NULL)
+endif
+
+MOCHITEST_BROWSER_FILES += \
+ test-console.html \
+ test-network.html \
+ test-network-request.html \
+ test-mutation.html \
+ testscript.js \
+ test-filter.html \
+ test-observe-http-ajax.html \
+ test-data.json \
+ test-data.json^headers^ \
+ test-property-provider.html \
+ test-error.html \
+ test-duplicate-error.html \
+ test-image.png \
+ test-encoding-ISO-8859-1.html \
+ test-bug-593003-iframe-wrong-hud.html \
+ test-bug-593003-iframe-wrong-hud-iframe.html \
+ test-console-replaced-api.html \
+ test-own-console.html \
+ test-bug-595934-css-loader.html \
+ test-bug-595934-css-loader.css \
+ test-bug-595934-css-loader.css^headers^ \
+ test-bug-595934-imagemap.html \
+ test-bug-595934-html.html \
+ test-bug-595934-malformedxml.xhtml \
+ test-bug-595934-svg.xhtml \
+ test-bug-595934-workers.html \
+ test-bug-595934-workers.js \
+ test-bug-595934-canvas.html \
+ test-bug-595934-canvas.js \
+ test-bug-595934-css-parser.html \
+ test-bug-595934-css-parser.css \
+ test-bug-595934-canvas-css.html \
+ test-bug-595934-canvas-css.js \
+ test-bug-595934-malformedxml-external.html \
+ test-bug-595934-malformedxml-external.xml \
+ test-bug-595934-empty-getelementbyid.html \
+ test-bug-595934-empty-getelementbyid.js \
+ test-bug-595934-image.html \
+ test-bug-595934-image.jpg \
+ test-bug-597136-external-script-errors.html \
+ test-bug-597136-external-script-errors.js \
+ test-bug-613013-console-api-iframe.html \
+ test-bug-597756-reopen-closed-tab.html \
+ test-bug-600183-charset.html \
+ test-bug-600183-charset.html^headers^ \
+ test-bug-601177-log-levels.html \
+ test-bug-601177-log-levels.js \
+ test-console-extras.html \
+ test-bug-603750-websocket.html \
+ test-bug-603750-websocket.js \
+ test-bug-599725-response-headers.sjs \
+ test-bug-618078-network-exceptions.html \
+ test-bug-630733-response-redirect-headers.sjs \
+ test-bug-621644-jsterm-dollar.html \
+ test-bug-632347-iterators-generators.html \
+ test-bug-585956-console-trace.html \
+ test-bug-644419-log-limits.html \
+ test-bug-632275-getters.html \
+ test-bug-646025-console-file-location.html \
+ test-bug-782653-css-errors.html \
+ test-bug-782653-css-errors-1.css \
+ test-bug-782653-css-errors-2.css \
+ test-file-location.js \
+ test-bug-658368-time-methods.html \
+ test-webconsole-error-observer.html \
+ test-for-of.html \
+ test_bug_770099_violation.html \
+ test_bug_770099_violation.html^headers^ \
+ test_bug_770099_bad_policy_uri.html \
+ test_bug_770099_bad_policy_uri.html^headers^ \
+ test-result-format-as-string.html \
+ test-bug-737873-mixedcontent.html \
+ test-repeated-messages.html \
+ test-bug-766001-console-log.js \
+ test-bug-766001-js-console-links.html \
+ test-bug-766001-js-errors.js \
+ test-bug-821877-csperrors.html \
+ test-bug-821877-csperrors.html^headers^ \
+ test-eval-in-stackframe.html \
+ test-bug-859170-longstring-hang.html \
+ test-bug-837351-security-errors.html \
+ test-bug-869003-top-window.html \
+ test-bug-869003-iframe.html \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
new file mode 100644
index 000000000..7a9947103
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -0,0 +1,113 @@
+/* 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 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.
+
+function test()
+{
+ const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+ const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/test/test-console.html";
+
+ let hud;
+ let msgForLocation1;
+
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URI1);
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(gBrowser.selectedTab, pageLoad1);
+ }, true);
+
+ function pageLoad1(aHud)
+ {
+ hud = aHud;
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href");
+
+ 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,
+ },
+ ]
+ };
+
+ waitForMessages(msgForLocation1).then(() => {
+ gBrowser.selectedBrowser.addEventListener("load", onPageLoad2, true);
+ content.location = TEST_URI2;
+ });
+ }
+
+ function onPageLoad2() {
+ gBrowser.selectedBrowser.removeEventListener("load", onPageLoad2, true);
+
+ 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");
+
+ 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,
+ },
+ ]
+ }).then(() => {
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+
+ gBrowser.goBack();
+ waitForSuccess(waitForBack);
+ });
+ }
+
+ let waitForBack = {
+ name: "go back",
+ validatorFn: function()
+ {
+ return content.location.href == TEST_URI1;
+ },
+ successFn: function()
+ {
+ hud.jsterm.clearOutput();
+ executeSoon(() => {
+ hud.jsterm.execute("window.location.href");
+ });
+
+ info("wait for window.location.href after goBack()");
+ waitForMessages(msgForLocation1).then(() => executeSoon(() => {
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+ finishTest();
+ }));
+ },
+ failureFn: finishTest,
+ };
+}
diff --git a/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
new file mode 100644
index 000000000..3853d531b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
@@ -0,0 +1,107 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
+ "test/test-console.html?_date=" + Date.now();
+const COMMAND_NAME = "consoleCmd_copyURL";
+const CONTEXT_MENU_ID = "#menu_copyURL";
+
+let HUD = null;
+let output = null;
+let menu = null;
+
+function test() {
+ registerCleanupFunction(() => {
+ HUD = output = menu = null;
+ });
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function (aHud) {
+ HUD = aHud;
+ output = aHud.outputNode;
+ menu = HUD.iframeWindow.document.getElementById("output-contextmenu");
+
+ executeSoon(testWithoutNetActivity);
+ });
+ }, true);
+}
+
+// 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);
+}
+
+function testWithoutNetActivity() {
+ HUD.jsterm.clearOutput();
+ content.console.log("bug 638949");
+
+ // Test that the "Copy Link Location" command is disabled for non-network
+ // messages.
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug 638949",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onConsoleMessage);
+}
+
+function onConsoleMessage(aResults) {
+ output.focus();
+ output.selectedItem = [...aResults[0].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.
+ waitForContextMenu(menu, output.selectedItem, () => {
+ let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " is hidden");
+ }, testWithNetActivity);
+}
+
+function testWithNetActivity() {
+ HUD.jsterm.clearOutput();
+ content.location.reload(); // Reloading will produce network logging
+
+ // 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.
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onNetworkMessage);
+}
+
+function onNetworkMessage(aResults) {
+ output.focus();
+ output.selectedItem = [...aResults[0].matched][0];
+
+ goUpdateCommand(COMMAND_NAME);
+ ok(isEnabled(), COMMAND_NAME + " is enabled");
+
+ waitForClipboard(output.selectedItem.url, () => goDoCommand(COMMAND_NAME),
+ testMenuWithNetActivity, testMenuWithNetActivity);
+}
+
+function testMenuWithNetActivity() {
+ // Test that the "Copy Link Location" menu item is visible for network-related
+ // messages.
+ waitForContextMenu(menu, output.selectedItem, () => {
+ let isVisible = !menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isVisible, CONTEXT_MENU_ID + " is visible");
+ }, finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js b/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
new file mode 100644
index 000000000..6dfc00b8a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
@@ -0,0 +1,34 @@
+/*
+ * 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.
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 862916";
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ ok(hud, "web console opened");
+
+ hud.setFilterState("log", false);
+ registerCleanupFunction(() => hud.setFilterState("log", true));
+
+ content.wrappedJSObject.fooBarz = "bug862916";
+ hud.jsterm.execute("console.dir(window)");
+ hud.jsterm.once("variablesview-fetched", (aEvent, aVar) => {
+ ok(aVar, "variables view object");
+ findVariableViewProperties(aVar, [
+ { name: "fooBarz", value: "bug862916" },
+ ], { webconsole: hud }).then(finishTest);
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
new file mode 100644
index 000000000..df3335082
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
@@ -0,0 +1,82 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that makes sure messages are not considered repeated when console.log()
+// is invoked with different objects, see bug 865288.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-repeated-messages.html";
+
+let hud = null;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ hud = aHud;
+
+ // Check that css warnings are not coalesced if they come from different lines.
+ info("waiting for 3 console.log objects");
+
+ hud.jsterm.clearOutput(true);
+ content.wrappedJSObject.testConsoleObjects();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "3 console.log messages",
+ text: "abba",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 3,
+ repeats: 1,
+ objects: true,
+ }],
+ }).then(checkMessages);
+}
+
+function checkMessages(aResults)
+{
+ let result = aResults[0];
+ let msgs = [...result.matched];
+ is(msgs.length, 3, "3 message elements");
+ let m = -1;
+
+ function nextMessage()
+ {
+ let msg = msgs[++m];
+ if (msg) {
+ ok(msg, "message element #" + m);
+
+ let clickable = msg.querySelector(".hud-clickable");
+ ok(clickable, "clickable object #" + m);
+
+ scrollOutputToNode(msg);
+ clickObject(clickable);
+ }
+ else {
+ finishTest();
+ }
+ }
+
+ nextMessage();
+
+ function clickObject(aObject)
+ {
+ hud.jsterm.once("variablesview-fetched", onObjectFetch);
+ EventUtils.synthesizeMouse(aObject, 2, 2, {}, hud.iframeWindow);
+ }
+
+ function onObjectFetch(aEvent, aVar)
+ {
+ findVariableViewProperties(aVar, [
+ { name: "id", value: "abba" + m },
+ ], { webconsole: hud }).then(nextMessage);
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
new file mode 100644
index 000000000..7e47d38c3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -0,0 +1,98 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gVariablesView;
+
+function test()
+{
+ registerCleanupFunction(() => {
+ gWebConsole = gJSTerm = gVariablesView = null;
+ });
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("fooObj", onExecuteFooObj);
+}
+
+function onExecuteFooObj()
+{
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "output message found");
+ isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+ gJSTerm.once("variablesview-fetched", onFooObjFetch);
+ EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow)
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp", value: "testValue" },
+ ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+ "|fooObj.testProp| value is correct");
+
+ gVariablesView.window.focus();
+ gJSTerm.once("sidebar-closed", onSidebarClosed);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
+}
+
+function onSidebarClosed()
+{
+ gJSTerm.clearOutput();
+ gJSTerm.execute("window", onExecuteWindow);
+}
+
+function onExecuteWindow()
+{
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "output message found");
+ isnot(msg.textContent.indexOf("[object Window]"), -1, "message text check");
+
+ gJSTerm.once("variablesview-fetched", onWindowFetch);
+ EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow)
+}
+
+function onWindowFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "foo", value: "globalFooBug783499" },
+ ], { webconsole: gWebConsole }).then(onFooFound);
+}
+
+function onFooFound(aResults)
+{
+ gVariablesView.window.focus();
+ gJSTerm.once("sidebar-closed", finishTest);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
new file mode 100644
index 000000000..0f8778e81
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -0,0 +1,94 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-869003-top-window.html";
+
+let gWebConsole, gJSTerm, gVariablesView;
+
+function test()
+{
+ // 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);
+
+ addTab("data:text/html;charset=utf8,<p>hello");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ content.location = TEST_URI;
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log message",
+ text: "foobar",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ objects: true,
+ }],
+ }).then(onConsoleMessage);
+}
+
+function onConsoleMessage(aResults)
+{
+ let clickable = aResults[0].clickableElements[0];
+ ok(clickable, "clickable object found");
+ isnot(clickable.textContent.indexOf("[object Object]"), -1,
+ "message text check");
+
+ gJSTerm.once("variablesview-fetched", onObjFetch);
+
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, gWebConsole.iframeWindow)
+}
+
+function onObjFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "hello", value: "world!" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: gWebConsole }).then(onPropFound);
+}
+
+function onPropFound(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the |hello| property in the variables view");
+
+ // Check that property value updates work.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'omgtest'",
+ webconsole: gWebConsole,
+ callback: onFetchAfterUpdate,
+ });
+}
+
+function onFetchAfterUpdate(aEvent, aVar)
+{
+ info("onFetchAfterUpdate");
+
+ findVariableViewProperties(aVar, [
+ { name: "hello", value: "omgtest" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: gWebConsole }).then(() => {
+ gWebConsole = gJSTerm = gVariablesView = null;
+ finishTest();
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
new file mode 100644
index 000000000..a76bf2bfe
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+
+function test()
+{
+ const TEST_URI = "data:text/html;charset=utf8,<title>bug871156</title>\n" +
+ "<p>hello world";
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+
+ function consoleOpened(hud)
+ {
+ ok(hud, "Web Console opened");
+
+ let tabClosed = false, toolboxDestroyed = false;
+
+ gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
+ gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
+
+ ok(true, "tab closed");
+
+ tabClosed = true;
+ if (toolboxDestroyed) {
+ testBrowserConsole();
+ }
+ });
+
+ let toolbox = gDevTools.getToolbox(hud.target);
+ toolbox.once("destroyed", () => {
+ ok(true, "toolbox destroyed");
+
+ toolboxDestroyed = true;
+ if (tabClosed) {
+ testBrowserConsole();
+ }
+ });
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+ }
+
+ function testBrowserConsole()
+ {
+ info("test the Browser Console");
+
+ HUDConsoleUI.toggleBrowserConsole().then((hud) => {
+ ok(hud, "Browser Console opened");
+
+ Services.obs.addObserver(function onDestroy() {
+ Services.obs.removeObserver(onDestroy, "web-console-destroyed");
+ ok(true, "the Browser Console closed");
+
+ executeSoon(finishTest);
+ }, "web-console-destroyed", false);
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+ });
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_cached_messages.js b/browser/devtools/webconsole/test/browser_cached_messages.js
new file mode 100644
index 000000000..aa28daaef
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_cached_messages.js
@@ -0,0 +1,76 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-webconsole-error-observer.html";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ expectUncaughtException();
+
+ addTab(TEST_URI);
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ testOpenUI(true);
+ }, true);
+}
+
+function testOpenUI(aTestReopen)
+{
+ // test to see if the messages are
+ // displayed when the console UI is opened
+
+ let messages = {
+ "log Bazzle" : false,
+ "error Bazzle" : false,
+ "bazBug611032" : false,
+ "cssColorBug611032" : false,
+ };
+
+ openConsole(null, function(hud) {
+ waitForSuccess({
+ name: "cached messages displayed",
+ validatorFn: function()
+ {
+ let foundAll = true;
+ for (let msg in messages) {
+ let found = messages[msg];
+ if (!found) {
+ found = hud.outputNode.textContent.indexOf(msg) > -1;
+ if (found) {
+ info("found message '" + msg + "'");
+ messages[msg] = found;
+ }
+ }
+ foundAll = foundAll && found;
+ }
+ return foundAll;
+ },
+ successFn: function()
+ {
+ // Make sure the CSS warning is given the correct category - bug 768019.
+ let cssNode = hud.outputNode.querySelector(".webconsole-msg-cssparser");
+ ok(cssNode, "CSS warning message element");
+ isnot(cssNode.textContent.indexOf("cssColorBug611032"), -1,
+ "CSS warning message element content is correct");
+
+ closeConsole(gBrowser.selectedTab, function() {
+ aTestReopen && info("will reopen the Web Console");
+ executeSoon(aTestReopen ? testOpenUI : finishTest);
+ });
+ },
+ failureFn: function()
+ {
+ for (let msg in messages) {
+ if (!messages[msg]) {
+ ok(false, "failed to find '" + msg + "'");
+ }
+ }
+ finishTest();
+ },
+ });
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_console.js b/browser/devtools/webconsole/test/browser_console.js
new file mode 100644
index 000000000..d204c1b2b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console.js
@@ -0,0 +1,102 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html?" + Date.now();
+
+function test()
+{
+ let oldFunction = HUDConsoleUI.toggleBrowserConsole;
+ let functionExecuted = false;
+ HUDConsoleUI.toggleBrowserConsole = () => functionExecuted = true;
+ EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, content);
+
+ ok(functionExecuted,
+ "toggleBrowserConsole() was executed by the Ctrl-Shift-J key shortcut");
+
+ HUDConsoleUI.toggleBrowserConsole = oldFunction;
+ HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
+}
+
+function consoleOpened(hud)
+{
+ hud.jsterm.clearOutput(true);
+
+ expectUncaughtException();
+ executeSoon(() => {
+ foobarExceptionBug587757();
+ });
+
+ // Add a message from a chrome window.
+ hud.iframeWindow.console.log("bug587757a");
+
+ // 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();
+
+ let chromeConsole = -1;
+ let contentConsole = -1;
+ let execValue = -1;
+ let exception = -1;
+ let xhrRequest = false;
+
+ let output = hud.outputNode;
+ function performChecks()
+ {
+ let text = output.textContent;
+ chromeConsole = text.indexOf("bug587757a");
+ contentConsole = text.indexOf("bug587757b");
+ execValue = text.indexOf("browser.xul");
+ exception = text.indexOf("foobarExceptionBug587757");
+
+ xhrRequest = false;
+ let urls = output.querySelectorAll(".webconsole-msg-url");
+ for (let url of urls) {
+ if (url.value.indexOf(TEST_URI) > -1) {
+ xhrRequest = true;
+ break;
+ }
+ }
+ }
+
+ function showResults()
+ {
+ isnot(chromeConsole, -1, "chrome window console.log() is displayed");
+ isnot(contentConsole, -1, "content window console.log() is displayed");
+ isnot(execValue, -1, "jsterm eval result is displayed");
+ isnot(exception, -1, "exception is displayed");
+ ok(xhrRequest, "xhr request is displayed");
+ }
+
+ waitForSuccess({
+ name: "messages displayed",
+ validatorFn: () => {
+ performChecks();
+ return chromeConsole > -1 &&
+ contentConsole > -1 &&
+ execValue > -1 &&
+ exception > -1 &&
+ xhrRequest;
+ },
+ successFn: () => {
+ showResults();
+ executeSoon(finishTest);
+ },
+ failureFn: () => {
+ showResults();
+ info("output: " + output.textContent);
+ executeSoon(finishTest);
+ },
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
new file mode 100644
index 000000000..7a4186761
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -0,0 +1,93 @@
+/*
+ * 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.
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
+
+function test()
+{
+ let webconsole, browserconsole;
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, consoleOpened);
+ }, true);
+
+ function consoleOpened(hud)
+ {
+ ok(hud, "web console opened");
+ webconsole = hud;
+ HUDConsoleUI.toggleBrowserConsole().then(browserConsoleOpened);
+ }
+
+ function browserConsoleOpened(hud)
+ {
+ ok(hud, "browser console opened");
+ browserconsole = hud;
+
+ // Cause an exception in a script loaded with the addon-sdk loader.
+ let toolbox = gDevTools.getToolbox(webconsole.target);
+ let oldPanels = toolbox._toolPanels;
+ toolbox._toolPanels = null;
+ function fixToolbox()
+ {
+ toolbox._toolPanels = oldPanels;
+ }
+
+ info("generate exception and wait for message");
+
+ executeSoon(() => {
+ executeSoon(fixToolbox);
+ expectUncaughtException();
+ toolbox.getToolPanels();
+ });
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "TypeError: this._toolPanels is null",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ }).then((results) => {
+ fixToolbox();
+ onMessageFound(results);
+ });
+ }
+
+ function onMessageFound(results)
+ {
+ let msg = [...results[0].matched][0];
+ ok(msg, "message element found");
+ let locationNode = msg.querySelector(".webconsole-location");
+ ok(locationNode, "message location element found");
+
+ let title = locationNode.getAttribute("title");
+ info("location node title: " + title);
+ isnot(title.indexOf(" -> "), -1, "error comes from a subscript");
+
+ let viewSource = browserconsole.viewSource;
+ let URL = null;
+ browserconsole.viewSource = (aURL) => URL = aURL;
+
+ EventUtils.synthesizeMouse(locationNode, 2, 2, {},
+ browserconsole.iframeWindow);
+
+ info("view-source url: " + URL);
+ isnot(URL.indexOf("toolbox.js"), -1, "expected view source URL");
+ is(URL.indexOf("->"), -1, "no -> in the URL given to view-source");
+
+ browserconsole.viewSource = viewSource;
+
+ finishTest();
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_clear_on_reload.js b/browser/devtools/webconsole/test/browser_console_clear_on_reload.js
new file mode 100644
index 000000000..63103c368
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_clear_on_reload.js
@@ -0,0 +1,73 @@
+/*
+ * 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.
+
+function test()
+{
+ const PREF = "devtools.webconsole.persistlog";
+ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+ let hud = null;
+
+ Services.prefs.setBoolPref(PREF, false);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ addTab(TEST_URI);
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+
+ function consoleOpened(aHud)
+ {
+ hud = aHud;
+ ok(hud, "Web Console opened");
+
+ hud.jsterm.clearOutput();
+ content.console.log("foobarz1");
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onConsoleMessage);
+ }
+
+ function onConsoleMessage()
+ {
+ browser.addEventListener("load", onReload, true);
+ content.location.reload();
+ }
+
+ function onReload()
+ {
+ browser.removeEventListener("load", onReload, true);
+
+ content.console.log("foobarz2");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ },
+ {
+ text: "foobarz2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onConsoleMessageAfterReload);
+ }
+
+ function onConsoleMessageAfterReload()
+ {
+ is(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+ "foobarz1 has been removed from output");
+ finishTest();
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_consolejsm_output.js b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
new file mode 100644
index 000000000..96b8ff42e
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
@@ -0,0 +1,135 @@
+/*
+ * 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.
+
+function test()
+{
+ let storage = Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", {}).ConsoleAPIStorage;
+ storage.clearEvents();
+
+ let console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
+ console.log("bug861338-log-cached");
+
+ HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
+ let hud = null;
+
+ function consoleOpened(aHud)
+ {
+ hud = aHud;
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "cached console.log message",
+ text: "bug861338-log-cached",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onCachedMessage);
+ }
+
+ function onCachedMessage()
+ {
+ hud.jsterm.clearOutput(true);
+
+ 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.trace();
+ console.dir(document);
+ console.timeEnd("foobarTimer");
+
+ info("wait for the Console.jsm messages");
+
+ 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.+\[object Object\]/,
+ 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: "onCachedMessage",
+ },
+ },
+ {
+ name: "console.dir output",
+ consoleDir: "[object XULDocument]",
+ },
+ {
+ name: "console.time output",
+ consoleTime: "foobarTimer",
+ },
+ {
+ name: "console.timeEnd output",
+ consoleTimeEnd: "foobarTimer",
+ },
+ ],
+ }).then((aResults) => {
+ let consoleErrorMsg = aResults[3];
+ ok(consoleErrorMsg, "console.error message element found");
+ let clickable = consoleErrorMsg.clickableElements[0];
+ ok(clickable, "clickable object found for console.error");
+
+ 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);
+
+ ok(aVar, "object inspector opened on click");
+
+ findVariableViewProperties(aVar, [{
+ name: "bug851231prop",
+ value: "bug851231value",
+ }], { webconsole: hud }).then(finishTest);
+ };
+
+ hud.jsterm.on("variablesview-fetched", onFetch);
+
+ scrollOutputToNode(clickable);
+
+ info("wait for variablesview-fetched");
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow));
+ });
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_dead_objects.js b/browser/devtools/webconsole/test/browser_console_dead_objects.js
new file mode 100644
index 000000000..1d68df857
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+
+const TEST_URI = "data:text/html;charset=utf8,<p>dead objects!";
+
+function test()
+{
+ let hud = null;
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ info("open the browser console");
+ HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+ }, true);
+
+ function onBrowserConsoleOpen(aHud)
+ {
+ hud = aHud;
+ ok(hud, "browser console opened");
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("foobarzTezt = content.document", onAddVariable);
+ }
+
+ function onAddVariable()
+ {
+ gBrowser.removeCurrentTab();
+
+ hud.jsterm.execute("foobarzTezt", onReadVariable);
+ }
+
+ function onReadVariable()
+ {
+ isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1,
+ "dead object found");
+
+ hud.jsterm.setInputValue("foobarzTezt");
+
+ for (let c of ".hello") {
+ EventUtils.synthesizeKey(c, {}, hud.iframeWindow);
+ }
+
+ hud.jsterm.execute(null, onReadProperty);
+ }
+
+ function onReadProperty()
+ {
+ isnot(hud.outputNode.textContent.indexOf("can't access dead object"), -1,
+ "'cannot access dead object' message found");
+
+ // Click the second execute output.
+ let clickable = hud.outputNode.querySelectorAll(".webconsole-msg-output")[1]
+ .querySelector(".hud-clickable");
+ ok(clickable, "clickable object found");
+ isnot(clickable.textContent.indexOf("[object DeadObject]"), -1,
+ "message text check");
+
+ hud.jsterm.once("variablesview-fetched", onFetched);
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ }
+
+ function onFetched()
+ {
+ hud.jsterm.execute("delete window.foobarzTezt; 2013-26", onCalcResult);
+ }
+
+ function onCalcResult()
+ {
+ isnot(hud.outputNode.textContent.indexOf("1987"), -1, "result message found");
+
+ // executeSoon() is needed to get out of the execute() event loop.
+ executeSoon(finishTest);
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_error_source_click.js b/browser/devtools/webconsole/test/browser_console_error_source_click.js
new file mode 100644
index 000000000..98fe93dbc
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js
@@ -0,0 +1,75 @@
+/*
+ * 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.
+
+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>";
+function test()
+{
+ let hud;
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ HUDConsoleUI.toggleBrowserConsole().then(browserConsoleOpened);
+ }, true);
+
+ function browserConsoleOpened(aHud)
+ {
+ hud = aHud;
+ ok(hud, "browser console opened");
+
+ let button = content.document.querySelector("button");
+ ok(button, "button element found");
+
+ info("generate exception and wait for the message");
+ executeSoon(() => {
+ expectUncaughtException();
+ button.click();
+ });
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ReferenceError: foobar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "Unknown property 'test-color'",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ }).then(onMessageFound);
+ }
+
+ function onMessageFound(results)
+ {
+ let viewSource = hud.viewSource;
+ let viewSourceCalled = false;
+ hud.viewSource = () => viewSourceCalled = true;
+
+ for (let result of results) {
+ viewSourceCalled = false;
+
+ let msg = [...results[0].matched][0];
+ ok(msg, "message element found for: " + result.text);
+ let locationNode = msg.querySelector(".webconsole-location");
+ ok(locationNode, "message location element found");
+
+ EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow);
+
+ ok(viewSourceCalled, "view source opened");
+ }
+
+ hud.viewSource = viewSource;
+ finishTest();
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_filters.js b/browser/devtools/webconsole/test/browser_console_filters.js
new file mode 100644
index 000000000..d9aa35c81
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_filters.js
@@ -0,0 +1,71 @@
+/*
+ * 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.
+
+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.";
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ info("open the web console");
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ 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);
+
+ executeSoon(() => closeConsole(null, onWebConsoleClose));
+}
+
+function onWebConsoleClose()
+{
+ info("web console closed");
+ HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+}
+
+function onBrowserConsoleOpen(hud)
+{
+ 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);
+
+ executeSoon(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
new file mode 100644
index 000000000..aca00bd5a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
@@ -0,0 +1,69 @@
+/*
+ * 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.
+
+function test()
+{
+ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+ let hud = null;
+
+ addTab(TEST_URI);
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+
+ function consoleOpened(aHud)
+ {
+ hud = aHud;
+ ok(hud, "Web Console opened");
+
+ content.console.log("foobarz1");
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onConsoleMessage);
+ }
+
+ function onConsoleMessage()
+ {
+ hud.jsterm.once("messages-cleared", onClear);
+ info("try ctrl-k to clear output");
+ EventUtils.synthesizeKey("K", { accelKey: true });
+ }
+
+ function onClear()
+ {
+ 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");
+ EventUtils.synthesizeKey("F", { accelKey: true });
+ 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") {
+ EventUtils.synthesizeKey("t", { ctrlKey: true });
+ }
+ 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");
+
+ finishTest();
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
new file mode 100644
index 000000000..230805401
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
@@ -0,0 +1,58 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that objects given to console.log() are inspectable.
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTab("data:text/html;charset=utf8,test for bug 676722 - inspectable objects for window.console");
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(null, performTest);
+ }, true);
+}
+
+function performTest(hud)
+{
+ hud.jsterm.clearOutput(true);
+
+ hud.jsterm.execute("myObj = {abba: 'omgBug676722'}");
+ hud.jsterm.execute("console.log('fooBug676722', myObj)");
+ waitForSuccess({
+ name: "eval results are shown",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("fooBug676722") > -1 &&
+ hud.outputNode.querySelector(".hud-clickable");
+ },
+ successFn: function()
+ {
+ isnot(hud.outputNode.textContent.indexOf("myObj = {"), -1,
+ "myObj = ... is shown");
+
+ let clickable = hud.outputNode.querySelector(".hud-clickable");
+ ok(clickable, "the console.log() object .hud-clickable was found");
+ isnot(clickable.textContent.indexOf("Object"), -1,
+ "clickable node content is correct");
+
+ hud.jsterm.once("variablesview-fetched",
+ (aEvent, aVar) => {
+ ok(aVar, "object inspector opened on click");
+
+ findVariableViewProperties(aVar, [{
+ name: "abba",
+ value: "omgBug676722",
+ }], { webconsole: hud }).then(finishTest);
+ });
+
+ executeSoon(function() {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+ },
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_console_native_getters.js b/browser/devtools/webconsole/test/browser_console_native_getters.js
new file mode 100644
index 000000000..3dd89d298
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_native_getters.js
@@ -0,0 +1,121 @@
+/*
+ * 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.
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug870220</title>\n" +
+ "<p>hello world\n<p>native getters!";
+
+let gWebConsole, gJSTerm, gVariablesView;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+
+ gJSTerm.execute("document");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "[object HTMLDocument]",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ }).then(onEvalResult);
+}
+
+function onEvalResult(aResults)
+{
+ let clickable = aResults[0].clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ gJSTerm.once("variablesview-fetched", onDocumentFetch);
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, gWebConsole.iframeWindow)
+}
+
+function onDocumentFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "title", value: "bug870220" },
+ { name: "bgColor" },
+ ], { webconsole: gWebConsole }).then(onDocumentPropsFound);
+}
+
+function onDocumentPropsFound(aResults)
+{
+ let prop = aResults[1].matchedProp;
+ ok(prop, "matched the |bgColor| property in the variables view");
+
+ // Check that property value updates work.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'red'",
+ webconsole: gWebConsole,
+ callback: onFetchAfterBackgroundUpdate,
+ });
+}
+
+function onFetchAfterBackgroundUpdate(aEvent, aVar)
+{
+ info("onFetchAfterBackgroundUpdate");
+
+ is(content.document.bgColor, "red", "document background color changed");
+
+ findVariableViewProperties(aVar, [
+ { name: "bgColor", value: "red" },
+ ], { webconsole: gWebConsole }).then(testParagraphs);
+}
+
+function testParagraphs()
+{
+ gJSTerm.execute("$$('p')");
+
+ waitForMessages({
+ webconsole: gWebConsole,
+ messages: [{
+ text: "[object NodeList]",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ }).then(onEvalNodeList);
+}
+
+function onEvalNodeList(aResults)
+{
+ let clickable = aResults[0].clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ gJSTerm.once("variablesview-fetched", onNodeListFetch);
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, gWebConsole.iframeWindow)
+}
+
+function onNodeListFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "0.textContent", value: /hello world/ },
+ { name: "1.textContent", value: /native getters/ },
+ ], { webconsole: gWebConsole }).then(() => {
+ gWebConsole = gJSTerm = gVariablesView = null;
+ finishTest();
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js b/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js
new file mode 100644
index 000000000..dcfc19fee
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js
@@ -0,0 +1,95 @@
+/*
+ * 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.
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" +
+ "<p>hello world\n<p>nsIConsoleMessages ftw!";
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ // Test for cached nsIConsoleMessages.
+ Services.console.logStringMessage("test1 for bug859756");
+
+ info("open web console");
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ ok(hud, "web console opened");
+ Services.console.logStringMessage("do-not-show-me");
+ content.console.log("foobarz");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "foobarz",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ ],
+ }).then(() => {
+ 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)");
+ closeConsole(null, onWebConsoleClose);
+ });
+}
+
+function onWebConsoleClose()
+{
+ info("web console closed");
+ HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+}
+
+function onBrowserConsoleOpen(hud)
+{
+ ok(hud, "browser console opened");
+ Services.console.logStringMessage("test2 for bug859756");
+
+ 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,
+ },
+ ],
+ }).then(testFiltering);
+
+ function testFiltering(results)
+ {
+ 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("hud-filtered-by-type"), "element is not filtered");
+
+ hud.setFilterState("jslog", false);
+
+ ok(msg.classList.contains("hud-filtered-by-type"), "element is filtered");
+
+ hud.setFilterState("jslog", true);
+
+ finishTest();
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_console_private_browsing.js b/browser/devtools/webconsole/test/browser_console_private_browsing.js
new file mode 100644
index 000000000..b2932e07b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_private_browsing.js
@@ -0,0 +1,199 @@
+/*
+ * 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.
+
+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 = Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", {}).ConsoleAPIStorage;
+ 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.
+ 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 is 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.isWindowPrivate(privateContent), "tab window is private");
+ openConsole(privateTab, consoleOpened);
+ }, true);
+ }
+
+ function addMessages()
+ {
+ let button = privateContent.document.querySelector("button");
+ ok(button, "button in page");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, privateContent);
+ }
+
+ function consoleOpened(aHud)
+ {
+ hud = aHud;
+ 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, () => {
+ info("web console closed");
+ openConsole(privateTab, consoleReopened);
+ });
+ }
+
+ function consoleReopened(aHud)
+ {
+ hud = aHud;
+ 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, () => {
+ info("web console closed");
+ privateWindow.HUDConsoleUI.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(aHud)
+ {
+ hud = aHud;
+ 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 the 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");
+ privateWindow.HUDConsoleUI.toggleBrowserConsole().then(() => {
+ info("reopen the browser console");
+ executeSoon(() =>
+ HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleReopen));
+ });
+ });
+ privateWindow.BrowserTryToCloseWindow();
+ }
+
+ function onBrowserConsoleReopen(aHud)
+ {
+ hud = aHud;
+ 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/browser/devtools/webconsole/test/browser_console_variables_view.js b/browser/devtools/webconsole/test/browser_console_variables_view.js
new file mode 100644
index 000000000..c1d08f653
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view.js
@@ -0,0 +1,177 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gVariablesView;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("fooObj", onExecuteFooObj);
+}
+
+function onExecuteFooObj()
+{
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "output message found");
+ isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+ gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow)
+ );
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp", value: "testValue" },
+ ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+ "|fooObj.testProp| value is correct");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + window.location + $('p')",
+ webconsole: gWebConsole,
+ callback: onFooObjFetchAfterUpdate,
+ });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+ info("onFooObjFetchAfterUpdate");
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expectedValue = content.document.title + content.location + para;
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp", value: expectedValue },
+ ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+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.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "name",
+ string: "testUpdatedProp",
+ webconsole: gWebConsole,
+ callback: onFooObjFetchAfterPropRename,
+ });
+}
+
+function onFooObjFetchAfterPropRename(aEvent, aVar)
+{
+ info("onFooObjFetchAfterPropRename");
+
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expectedValue = content.document.title + content.location + para;
+
+ // Check that the new value is in the variables view.
+ findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: gWebConsole }).then(onRenamedTestPropFound);
+}
+
+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.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "foobarzFailure()",
+ webconsole: gWebConsole,
+ callback: onPropUpdateError,
+ });
+}
+
+function onPropUpdateError(aEvent, aVar)
+{
+ info("onPropUpdateError");
+
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expectedValue = content.document.title + content.location + para;
+
+ // Make sure the property did not change.
+ findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: gWebConsole }).then(onRenamedTestPropFoundAgain);
+}
+
+function onRenamedTestPropFoundAgain(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the renamed |testProp| property again");
+
+ let outputNode = gWebConsole.outputNode;
+
+ waitForSuccess({
+ name: "exception in property update reported in the web console output",
+ validatorFn: () => outputNode.textContent.indexOf("foobarzFailure") != -1,
+ successFn: testPropDelete.bind(null, prop),
+ failureFn: testPropDelete.bind(null, prop),
+ });
+}
+
+function testPropDelete(aProp)
+{
+ gVariablesView.window.focus();
+ aProp.focus();
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window);
+ gWebConsole = gJSTerm = gVariablesView = null;
+ });
+
+ waitForSuccess({
+ name: "property deleted",
+ validatorFn: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj),
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
new file mode 100644
index 000000000..1ff987c68
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
@@ -0,0 +1,132 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController,
+ gStackframes, gVariablesView;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+
+ executeSoon(() => {
+ info("openDebugger");
+ openDebugger().then(debuggerOpened);
+ });
+}
+
+function debuggerOpened(aResult)
+{
+ gDebuggerWin = aResult.panelWin;
+ gDebuggerController = gDebuggerWin.DebuggerController;
+ gThread = gDebuggerController.activeThread;
+ gStackframes = gDebuggerController.StackFrames;
+
+ executeSoon(() => {
+ gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+ info("firstCall()");
+ content.wrappedJSObject.firstCall();
+ });
+}
+
+function onFramesAdded()
+{
+ info("onFramesAdded");
+
+ executeSoon(() =>
+ openConsole(null, () =>
+ gJSTerm.execute("fooObj", onExecuteFooObj)
+ )
+ );
+}
+
+
+function onExecuteFooObj()
+{
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "output message found");
+ isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+ gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+ executeSoon(() => EventUtils.synthesizeMouse(msg, 2, 2, {},
+ gWebConsole.iframeWindow));
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+ let prop = aResults[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.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: gWebConsole,
+ callback: onFooObjFetchAfterUpdate,
+ });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+ info("onFooObjFetchAfterUpdate");
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expectedValue = content.document.title + "foo2SecondCall" + para;
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+function onUpdatedTestPropFound(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ executeSoon(() => gJSTerm.execute("fooObj.testProp2", onExecuteFooObjTestProp2));
+}
+
+function onExecuteFooObjTestProp2()
+{
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expected = content.document.title + "foo2SecondCall" + para;
+
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "fooObj.testProp2 is correct");
+
+ gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+ gStackframes = gVariablesView = null;
+ executeSoon(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
new file mode 100644
index 000000000..cecc24b72
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
@@ -0,0 +1,127 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController,
+ gStackframes, gVariablesView;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+
+ info("openDebugger");
+ openDebugger().then(debuggerOpened);
+}
+
+function debuggerOpened(aResult)
+{
+ gDebuggerWin = aResult.panelWin;
+ gDebuggerController = gDebuggerWin.DebuggerController;
+ gThread = gDebuggerController.activeThread;
+ gStackframes = gDebuggerController.StackFrames;
+
+ openInspector(inspectorOpened);
+}
+
+function inspectorOpened(aPanel)
+{
+ gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+ info("firstCall()");
+ content.wrappedJSObject.firstCall();
+}
+
+function onFramesAdded()
+{
+ info("onFramesAdded");
+
+ openConsole(null, () => gJSTerm.execute("fooObj", onExecuteFooObj));
+}
+
+function onExecuteFooObj()
+{
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "output message found");
+ isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+ gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+ EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow);
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+ let prop = aResults[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.
+ updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: gWebConsole,
+ callback: onFooObjFetchAfterUpdate,
+ });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+ info("onFooObjFetchAfterUpdate");
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expectedValue = content.document.title + "foo2SecondCall" + para;
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+function onUpdatedTestPropFound(aResults)
+{
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ gJSTerm.execute("fooObj.testProp2", onExecuteFooObjTestProp2);
+}
+
+function onExecuteFooObjTestProp2()
+{
+ let para = content.wrappedJSObject.document.querySelector("p");
+ let expected = content.document.title + "foo2SecondCall" + para;
+
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "fooObj.testProp2 is correct");
+
+ gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+ gStackframes = gVariablesView = null;
+
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js
new file mode 100644
index 000000000..4dd3d6c76
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js
@@ -0,0 +1,150 @@
+/*
+ * 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, gStackframes;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud)
+{
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("foo", 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", onNewFoo2));
+}
+
+function onNewFoo2()
+{
+ is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1,
+ "|undefined| is not displayed after adding |foo2|");
+
+ let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+ 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(null, () =>
+ gJSTerm.execute("foo + foo2", 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()");
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
+
+function onFramesAdded()
+{
+ info("onFramesAdded, openConsole() now");
+ executeSoon(() =>
+ openConsole(null, () =>
+ gJSTerm.execute("foo + foo2", onExecuteFooAndFoo2InSecondCall)
+ )
+ );
+}
+
+function onExecuteFooAndFoo2InSecondCall()
+{
+ let expected = "globalFooBug783499foo2SecondCall";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2| from |secondCall()|");
+
+ executeSoon(() => {
+ gJSTerm.clearOutput();
+
+ info("openDebugger and selectFrame(1)");
+
+ openDebugger().then(() => {
+ gStackframes.selectFrame(1);
+
+ info("openConsole");
+ executeSoon(() =>
+ openConsole(null, () =>
+ gJSTerm.execute("foo + foo2 + foo3", onExecuteFoo23InFirstCall)
+ )
+ );
+ });
+ });
+}
+
+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",
+ onExecuteFooAndFoo3ChangesInFirstCall));
+}
+
+function onExecuteFooAndFoo3ChangesInFirstCall()
+{
+ let expected = "abbabug783499";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo3| updated in |firstCall()|");
+
+ 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/browser/devtools/webconsole/test/browser_jsterm_inspect.js b/browser/devtools/webconsole/test/browser_jsterm_inspect.js
new file mode 100644
index 000000000..4dac58b34
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_jsterm_inspect.js
@@ -0,0 +1,35 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Check that the inspect() jsterm helper function works.
+
+function test()
+{
+ const TEST_URI = "data:text/html;charset=utf8,<p>hello bug 869981";
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+
+ function consoleOpened(hud)
+ {
+ content.wrappedJSObject.testProp = "testValue";
+
+ hud.jsterm.once("variablesview-fetched", onObjFetch);
+ hud.jsterm.execute("inspect(window)");
+ }
+
+ function onObjFetch(aEvent, aVar)
+ {
+ ok(aVar._variablesView, "variables view object");
+
+ findVariableViewProperties(aVar, [
+ { name: "testProp", value: "testValue" },
+ { name: "document", value: "[object HTMLDocument]" },
+ ], { webconsole: hud }).then(finishTest);
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_longstring_hang.js b/browser/devtools/webconsole/test/browser_longstring_hang.js
new file mode 100644
index 000000000..0a3600680
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_longstring_hang.js
@@ -0,0 +1,73 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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.
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let DebuggerServer = Cu.import("resource://gre/modules/devtools/dbg-server.jsm",
+ {}).DebuggerServer;
+
+ addTab("http://example.com/browser/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html");
+
+ let hud = null;
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(null, performTest);
+ }, true);
+
+ function performTest(aHud)
+ {
+ hud = aHud;
+
+ info("wait for the initial long string");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "find 'foobar', no 'foobaz', in long string output",
+ text: "foobar",
+ noText: "foobaz",
+ category: CATEGORY_WEBDEV,
+ longString: true,
+ },
+ ],
+ }).then(onInitialString);
+ }
+
+ function onInitialString(aResults)
+ {
+ let clickable = aResults[0].longStrings[0];
+ ok(clickable, "long string ellipsis is shown");
+
+ scrollOutputToNode(clickable);
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ info("wait for long string expansion");
+
+ 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,
+ },
+ ],
+ }).then(finishTest);
+ });
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js b/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js
new file mode 100644
index 000000000..8488ad37e
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js
@@ -0,0 +1,309 @@
+/* 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 network panel works with LongStringActors.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
+
+const TEST_IMG_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJREFU" +
+ "OI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuFDjq5dxD8" +
+ "FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNUyhsKAChRBQcP" +
+ "FVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQEod+Z5sP1FizDxGgy" +
+ "BqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTsDizDRMPuIOC+zEeTMZo9" +
+ "BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joREN2uqnlBmCwW1hIJagtev4f3z" +
+ "A16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOnidadmwtwsnMu18q83/kHSou+bFND" +
+ "Dr4AAAAASUVORK5CYII=";
+
+let testDriver;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testNetworkPanel);
+ }, true);
+}
+
+function testNetworkPanel() {
+ testDriver = testGen();
+ testDriver.next();
+}
+
+function checkIsVisible(aPanel, aList) {
+ for (let id in aList) {
+ let node = aPanel.document.getElementById(id);
+ let isVisible = aList[id];
+ is(node.style.display, (isVisible ? "block" : "none"), id + " isVisible=" + isVisible);
+ }
+}
+
+function checkNodeContent(aPanel, aId, aContent) {
+ let node = aPanel.document.getElementById(aId);
+ if (node == null) {
+ ok(false, "Tried to access node " + aId + " that doesn't exist!");
+ }
+ else if (node.textContent.indexOf(aContent) != -1) {
+ ok(true, "checking content of " + aId);
+ }
+ else {
+ ok(false, "Got false value for " + aId + ": " + node.textContent + " doesn't have " + aContent);
+ }
+}
+
+function checkNodeKeyValue(aPanel, aId, aKey, aValue) {
+ let node = aPanel.document.getElementById(aId);
+
+ let headers = node.querySelectorAll("th");
+ for (let i = 0; i < headers.length; i++) {
+ if (headers[i].textContent == (aKey + ":")) {
+ is(headers[i].nextElementSibling.textContent, aValue,
+ "checking content of " + aId + " for key " + aKey);
+ return;
+ }
+ }
+
+ ok(false, "content check failed for " + aId + ", key " + aKey);
+}
+
+function testGen() {
+ let hud = HUDService.getHudByWindow(content);
+ let filterBox = hud.ui.filterBox;
+
+ let headerValue = (new Array(456)).join("fooz bar");
+ let headerValueGrip = {
+ type: "longString",
+ initial: headerValue.substr(0, 123),
+ length: headerValue.length,
+ actor: "faktor",
+ _fullString: headerValue,
+ };
+
+ let imageContentGrip = {
+ type: "longString",
+ initial: TEST_IMG_BASE64.substr(0, 143),
+ length: TEST_IMG_BASE64.length,
+ actor: "faktor2",
+ _fullString: TEST_IMG_BASE64,
+ };
+
+ let postDataValue = (new Array(123)).join("post me");
+ let postDataGrip = {
+ type: "longString",
+ initial: postDataValue.substr(0, 172),
+ length: postDataValue.length,
+ actor: "faktor3",
+ _fullString: postDataValue,
+ };
+
+ let httpActivity = {
+ updates: ["responseContent", "eventTimings"],
+ discardRequestBody: false,
+ discardResponseBody: false,
+ startedDateTime: (new Date()).toISOString(),
+ request: {
+ url: TEST_IMG,
+ method: "GET",
+ cookies: [],
+ headers: [
+ { name: "foo", value: "bar" },
+ { name: "loongstring", value: headerValueGrip },
+ ],
+ postData: { text: postDataGrip },
+ },
+ response: {
+ httpVersion: "HTTP/3.14",
+ status: 2012,
+ statusText: "ddahl likes tacos :)",
+ headers: [
+ { name: "Content-Type", value: "image/png" },
+ ],
+ content: { mimeType: "image/png", text: imageContentGrip },
+ cookies: [],
+ },
+ timings: { wait: 15, receive: 23 },
+ };
+
+ let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+
+ is(filterBox._netPanel, networkPanel,
+ "Network panel stored on the anchor object");
+
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ info("test 1: check if a header value is expandable");
+
+ checkIsVisible(networkPanel, {
+ requestCookie: false,
+ requestFormData: false,
+ requestBody: false,
+ requestBodyFetchLink: true,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: true,
+ responseImageCached: false,
+ responseBodyFetchLink: true,
+ });
+
+ checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar");
+ checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring",
+ headerValueGrip.initial + "[\u2026]");
+
+ let webConsoleClient = networkPanel.webconsole.webConsoleClient;
+ let longStringFn = webConsoleClient.longString;
+
+ let expectedGrip = headerValueGrip;
+
+ function longStringClientProvider(aLongString)
+ {
+ is(aLongString, expectedGrip,
+ "longString grip is correct");
+
+ return {
+ initial: expectedGrip.initial,
+ length: expectedGrip.length,
+ substring: function(aStart, aEnd, aCallback) {
+ is(aStart, expectedGrip.initial.length,
+ "substring start is correct");
+ is(aEnd, expectedGrip.length,
+ "substring end is correct");
+
+ executeSoon(function() {
+ aCallback({
+ substring: expectedGrip._fullString.substring(aStart, aEnd),
+ });
+
+ executeSoon(function() {
+ testDriver.next();
+ });
+ });
+ },
+ };
+ }
+
+ webConsoleClient.longString = longStringClientProvider;
+
+ let clickable = networkPanel.document
+ .querySelector("#requestHeadersContent .longStringEllipsis");
+ ok(clickable, "long string ellipsis is shown");
+
+ EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
+ networkPanel.document.defaultView);
+
+ yield;
+
+ clickable = networkPanel.document
+ .querySelector("#requestHeadersContent .longStringEllipsis");
+ ok(!clickable, "long string ellipsis is not shown");
+
+ checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring",
+ expectedGrip._fullString);
+
+ info("test 2: check that response body image fetching works");
+ expectedGrip = imageContentGrip;
+
+ let imgNode = networkPanel.document.getElementById("responseImageNode");
+ ok(!imgNode.getAttribute("src"), "no image is displayed");
+
+ clickable = networkPanel.document.querySelector("#responseBodyFetchLink");
+ EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
+ networkPanel.document.defaultView);
+
+ yield;
+
+ imgNode = networkPanel.document.getElementById("responseImageNode");
+ is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
+ "displayed image is correct");
+ is(clickable.style.display, "none", "#responseBodyFetchLink is not visible");
+
+ info("test 3: expand the request body");
+
+ expectedGrip = postDataGrip;
+
+ clickable = networkPanel.document.querySelector("#requestBodyFetchLink");
+ EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
+ networkPanel.document.defaultView);
+ yield;
+
+ is(clickable.style.display, "none", "#requestBodyFetchLink is not visible");
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestBodyFetchLink: false,
+ });
+
+ checkNodeContent(networkPanel, "requestBodyContent", expectedGrip._fullString);
+
+ webConsoleClient.longString = longStringFn;
+
+ networkPanel.panel.hidePopup();
+
+ info("test 4: reponse body long text");
+
+ httpActivity.response.content.mimeType = "text/plain";
+ httpActivity.response.headers[0].value = "text/plain";
+
+ expectedGrip = imageContentGrip;
+
+ // Reset response.content.text to avoid caching of the full string.
+ httpActivity.response.content.text = expectedGrip;
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ is(filterBox._netPanel, networkPanel,
+ "Network panel stored on httpActivity object");
+
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestCookie: false,
+ requestFormData: false,
+ requestBody: true,
+ requestBodyFetchLink: false,
+ responseContainer: true,
+ responseBody: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false,
+ responseBodyFetchLink: true,
+ });
+
+ checkNodeContent(networkPanel, "responseBodyContent", expectedGrip.initial);
+
+ webConsoleClient.longString = longStringClientProvider;
+
+ clickable = networkPanel.document.querySelector("#responseBodyFetchLink");
+ EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
+ networkPanel.document.defaultView);
+
+ yield;
+
+ webConsoleClient.longString = longStringFn;
+ is(clickable.style.display, "none", "#responseBodyFetchLink is not visible");
+ checkNodeContent(networkPanel, "responseBodyContent", expectedGrip._fullString);
+
+ networkPanel.panel.hidePopup();
+
+ // All done!
+ testDriver = null;
+ executeSoon(finishTest);
+
+ yield;
+}
diff --git a/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
new file mode 100644
index 000000000..935a13932
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
@@ -0,0 +1,56 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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.
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTab("data:text/html;charset=utf8,test for bug 773466");
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(null, performTest);
+ }, true);
+}
+
+function performTest(hud)
+{
+ hud.jsterm.clearOutput(true);
+
+ hud.jsterm.execute("console.log('fooBug773466a')");
+ hud.jsterm.execute("myObj = Object.create(null)");
+ hud.jsterm.execute("console.dir(myObj)");
+ waitForSuccess({
+ name: "eval results are shown",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-inspector");
+ },
+ successFn: function()
+ {
+ isnot(hud.outputNode.textContent.indexOf("fooBug773466a"), -1,
+ "fooBug773466a shows");
+ ok(hud.outputNode.querySelector(".webconsole-msg-inspector"),
+ "the console.dir() tree shows");
+
+ content.console.log("fooBug773466b");
+
+ waitForSuccess(waitForAnotherConsoleLogCall);
+ },
+ failureFn: finishTest,
+ });
+
+ let waitForAnotherConsoleLogCall = {
+ name: "eval result after console.dir()",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("fooBug773466b") > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ };
+}
diff --git a/browser/devtools/webconsole/test/browser_output_longstring_expand.js b/browser/devtools/webconsole/test/browser_output_longstring_expand.js
new file mode 100644
index 000000000..014c6d2bd
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_output_longstring_expand.js
@@ -0,0 +1,153 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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.
+
+function test()
+{
+ waitForExplicitFinish();
+
+ let tempScope = {};
+ Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+ let DebuggerServer = tempScope.DebuggerServer;
+
+ let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a") +
+ "foobar";
+ let initialString =
+ longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+ addTab("data:text/html;charset=utf8,test for bug 787981 - check that long strings can be expanded in the output.");
+
+ let hud = null;
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(null, performTest);
+ }, true);
+
+ function performTest(aHud)
+ {
+ hud = aHud;
+
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("console.log('bazbaz', '" + longString +"', 'boom')");
+
+ waitForSuccess(waitForConsoleLog);
+ }
+
+ let waitForConsoleLog = {
+ name: "console.log output shown",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-console");
+ },
+ successFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-console");
+ is(msg.textContent.indexOf("foobar"), -1,
+ "foobar is not shown");
+ isnot(msg.textContent.indexOf("bazbaz"), -1,
+ "bazbaz is shown");
+ isnot(msg.textContent.indexOf("boom"), -1,
+ "boom is shown");
+ isnot(msg.textContent.indexOf(initialString), -1,
+ "initial string is shown");
+
+ let clickable = msg.querySelector(".longStringEllipsis");
+ ok(clickable, "long string ellipsis is shown");
+
+ scrollToVisible(clickable);
+
+ executeSoon(function() {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ waitForSuccess(waitForFullString);
+ });
+ },
+ failureFn: finishTest,
+ };
+
+ let waitForFullString = {
+ name: "full string shown",
+ validatorFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-log");
+ return msg.textContent.indexOf(longString) > -1;
+ },
+ successFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-log");
+ isnot(msg.textContent.indexOf("bazbaz"), -1,
+ "bazbaz is shown");
+ isnot(msg.textContent.indexOf("boom"), -1,
+ "boom is shown");
+
+ let clickable = msg.querySelector(".longStringEllipsis");
+ ok(!clickable, "long string ellipsis is not shown");
+
+ executeSoon(function() {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("'" + longString +"'");
+ waitForSuccess(waitForExecute);
+ });
+ },
+ failureFn: finishTest,
+ };
+
+ let waitForExecute = {
+ name: "execute() output shown",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-output");
+ isnot(msg.textContent.indexOf(initialString), -1,
+ "initial string is shown");
+ is(msg.textContent.indexOf(longString), -1,
+ "full string is not shown");
+
+ let clickable = msg.querySelector(".longStringEllipsis");
+ ok(clickable, "long string ellipsis is shown");
+
+ scrollToVisible(clickable);
+
+ executeSoon(function() {
+ EventUtils.synthesizeMouse(clickable, 3, 4, {}, hud.iframeWindow);
+ waitForSuccess(waitForFullStringAfterExecute);
+ });
+ },
+ failureFn: finishTest,
+ };
+
+ let waitForFullStringAfterExecute = {
+ name: "full string shown again",
+ validatorFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-output");
+ return msg.textContent.indexOf(longString) > -1;
+ },
+ successFn: function()
+ {
+ let msg = hud.outputNode.querySelector(".webconsole-msg-output");
+ let clickable = msg.querySelector(".longStringEllipsis");
+ ok(!clickable, "long string ellipsis is not shown");
+
+ executeSoon(finishTest);
+ },
+ failureFn: finishTest,
+ };
+
+ function scrollToVisible(aNode)
+ {
+ let richListBoxNode = aNode.parentNode;
+ while (richListBoxNode.tagName != "richlistbox") {
+ richListBoxNode = richListBoxNode.parentNode;
+ }
+
+ let boxObject = richListBoxNode.scrollBoxObject;
+ let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ nsIScrollBoxObject.ensureElementIsVisible(aNode);
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js b/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js
new file mode 100644
index 000000000..753257c19
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js
@@ -0,0 +1,120 @@
+/* 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 makes sure messages are not considered repeated when coming from
+// different lines of code, or from different severities, etc.
+// See bugs 720180 and 800510.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-repeated-messages.html";
+
+function test() {
+ const PREF = "devtools.webconsole.persistlog";
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud) {
+ // Check that css warnings are not coalesced if they come from different lines.
+ info("waiting for 2 css warnings");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings",
+ category: CATEGORY_CSS,
+ count: 2,
+ repeats: 1,
+ }],
+ }).then(testCSSRepeats.bind(null, hud));
+}
+
+function testCSSRepeats(hud) {
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ info("wait for repeats after page reload");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings, repeated twice",
+ category: CATEGORY_CSS,
+ repeats: 2,
+ count: 2,
+ }],
+ }).then(testCSSRepeatsAfterReload.bind(null, hud));
+ }, true);
+ content.location.reload();
+}
+
+function testCSSRepeatsAfterReload(hud) {
+ hud.jsterm.clearOutput(true);
+ content.wrappedJSObject.testConsole();
+
+ info("wait for repeats with the console API");
+
+ 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,
+ },
+ ],
+ }).then(testConsoleRepeats.bind(null, hud));
+}
+
+function testConsoleRepeats(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("undefined");
+ content.console.log("undefined");
+
+ info("make sure console API messages are not coalesced with jsterm output");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "'undefined' jsterm input message",
+ text: "undefined",
+ category: CATEGORY_INPUT,
+ repeats: 1,
+ },
+ {
+ name: "'undefined' jsterm output message",
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ repeats: 1,
+ },
+ {
+ name: "'undefined' console.log message",
+ text: "undefined",
+ category: CATEGORY_WEBDEV,
+ repeats: 1,
+ },
+ ],
+ }).then(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_result_format_as_string.js b/browser/devtools/webconsole/test/browser_result_format_as_string.js
new file mode 100644
index 000000000..c887fb309
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_result_format_as_string.js
@@ -0,0 +1,49 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-result-format-as-string.html";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTab(TEST_URI);
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openConsole(null, performTest);
+ }, true);
+}
+
+function performTest(hud)
+{
+ hud.jsterm.clearOutput(true);
+
+ hud.jsterm.execute("document.querySelector('p')");
+ waitForSuccess({
+ name: "eval result shown",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ is(hud.outputNode.textContent.indexOf("bug772506_content"), -1,
+ "no content element found");
+ ok(!hud.outputNode.querySelector("div"), "no div element found");
+
+ let msg = hud.outputNode.querySelector(".webconsole-msg-output");
+ ok(msg, "eval output node found");
+ is(msg.textContent.indexOf("HTMLDivElement"), -1,
+ "HTMLDivElement string not displayed");
+ EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"});
+ ok(!gBrowser._bug772506, "no content variable");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js b/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js
new file mode 100644
index 000000000..42e011339
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+const TEST_REPLACED_API_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-replaced-api.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/testscript.js";
+
+function test() {
+ waitForExplicitFinish();
+
+ const PREF = "devtools.webconsole.persistlog";
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ // First test that the warning does not appear on a page that doesn't override
+ // the window.console object.
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testWarningNotPresent);
+ }, true);
+
+ function testWarningNotPresent(hud)
+ {
+ 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(() => 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(null, loadTestPage);
+ }));
+ }
+
+ function loadTestPage()
+ {
+ info("load test " + TEST_REPLACED_API_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testWarningPresent);
+ }, true);
+ content.location = TEST_REPLACED_API_URI;
+ }
+
+ function testWarningPresent(hud)
+ {
+ info("wait for the warning to show");
+ 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(finishTest);
+ content.location.reload();
+ });
+ });
+ }
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js b/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js
new file mode 100644
index 000000000..1bc94b5d3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that source URLs are abbreviated properly for display on the right-
+// hand side of the Web Console.
+
+function test() {
+ testAbbreviation("http://example.com/x.js", "x.js");
+ testAbbreviation("http://example.com/foo/bar/baz/boo.js", "boo.js");
+ testAbbreviation("http://example.com/foo/bar/", "bar");
+ testAbbreviation("http://example.com/foo.js?bar=1&baz=2", "foo.js");
+ testAbbreviation("http://example.com/foo/?bar=1&baz=2", "foo");
+
+ finishTest();
+}
+
+function testAbbreviation(aFullURL, aAbbreviatedURL) {
+ is(WebConsoleUtils.abbreviateSourceURL(aFullURL), aAbbreviatedURL, aFullURL +
+ " is abbreviated to " + aAbbreviatedURL);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js
new file mode 100644
index 000000000..c35cbd7d5
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js
@@ -0,0 +1,45 @@
+/* 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 page's resources are displayed in the console as they're
+// loaded
+
+const TEST_NETWORK_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network.html" + "?_date=" + Date.now();
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console basic network logging test");
+ browser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function() {
+ browser.addEventListener("load", testBasicNetLogging, true);
+ content.location = TEST_NETWORK_URI;
+ });
+}
+
+function testBasicNetLogging(aEvent) {
+ browser.removeEventListener(aEvent.type, testBasicNetLogging, true);
+
+ outputNode = HUDService.getHudByWindow(content).outputNode;
+
+ waitForSuccess({
+ name: "network console message",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("running network console") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("test-network.html");
+ findLogEntry("testscript.js");
+ findLogEntry("test-image.png");
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js b/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js
new file mode 100644
index 000000000..2c6e294fd
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js
@@ -0,0 +1,38 @@
+/* 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 object still exists after a page reload.
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false);
+
+ openConsole();
+
+ browser.addEventListener("DOMContentLoaded", testPageReload, false);
+ content.location.reload();
+}
+
+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");
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js b/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js
new file mode 100644
index 000000000..d13a97fe0
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js
@@ -0,0 +1,25 @@
+/* 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 input field is focused when the console is opened.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("DOMContentLoaded", testInputFocus, false);
+}
+
+function testInputFocus() {
+ browser.removeEventListener("DOMContentLoaded", testInputFocus, false);
+
+ openConsole(null, function(hud) {
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused");
+
+ finishTest();
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
new file mode 100644
index 000000000..86a9d8ff8
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
@@ -0,0 +1,44 @@
+/* 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 to ensure that errors don't appear when the console is closed while a
+// completion is being performed.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testClosingAfterCompletion);
+ }, true);
+}
+
+function testClosingAfterCompletion(hud) {
+ let inputNode = hud.jsterm.inputNode;
+
+ let errorWhileClosing = false;
+ function errorListener(evt) {
+ errorWhileClosing = true;
+ }
+
+ browser.addEventListener("error", errorListener, false);
+
+ // Focus the inputNode and perform the keycombo to close the WebConsole.
+ inputNode.focus();
+
+ gDevTools.once("toolbox-destroyed", function() {
+ browser.removeEventListener("error", errorListener, false);
+ is(errorWhileClosing, false, "no error while closing the WebConsole");
+ finishTest();
+ });
+
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("k", { accelKey: true, altKey: true });
+ } else {
+ EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true });
+ }
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
new file mode 100644
index 000000000..bc325d9ce
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
@@ -0,0 +1,67 @@
+/* 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 errors still show up in the Web Console after a page reload.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
+
+function test() {
+ expectUncaughtException();
+ addTab(TEST_URI);
+ browser.addEventListener("load", onLoad, true);
+}
+
+// see bug 580030: the error handler fails silently after page reload.
+// https://bugzilla.mozilla.org/show_bug.cgi?id=580030
+function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+
+ openConsole(null, function(hud) {
+ hud.jsterm.clearOutput();
+ browser.addEventListener("load", testErrorsAfterPageReload, true);
+ content.location.reload();
+ });
+}
+
+function testErrorsAfterPageReload(aEvent) {
+ browser.removeEventListener(aEvent.type, testErrorsAfterPageReload, true);
+
+ // dispatch a click event to the button in the test page and listen for
+ // errors.
+
+ Services.console.registerListener(consoleObserver);
+
+ let button = content.document.querySelector("button").wrappedJSObject;
+ ok(button, "button found");
+ EventUtils.sendMouseEvent({type: "click"}, button, content.wrappedJSObject);
+}
+
+var consoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function test_observe(aMessage)
+ {
+ // Ignore errors we don't care about.
+ if (!(aMessage instanceof Ci.nsIScriptError) ||
+ aMessage.category != "content javascript") {
+ return;
+ }
+
+ Services.console.unregisterListener(this);
+
+ let outputNode = HUDService.getHudByWindow(content).outputNode;
+
+ waitForSuccess({
+ name: "error message after page reload",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("fooBazBaz") > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+ }
+};
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
new file mode 100644
index 000000000..33fdc6e9e
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
@@ -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/. */
+
+// Tests that console groups behave properly.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testGroups);
+ }, true);
+}
+
+function testGroups(HUD) {
+ let jsterm = HUD.jsterm;
+ let outputNode = HUD.outputNode;
+ jsterm.clearOutput();
+
+ // We test for one group by testing for zero "new" groups. The
+ // "webconsole-new-group" class creates a divider. Thus one group is
+ // indicated by zero new groups, two groups are indicated by one new group,
+ // and so on.
+
+ let waitForSecondMessage = {
+ name: "second console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-msg-output").length == 2;
+ },
+ successFn: function()
+ {
+ let timestamp1 = Date.now();
+ if (timestamp1 - timestamp0 < 5000) {
+ is(outputNode.querySelectorAll(".webconsole-new-group").length, 0,
+ "no group dividers exist after the second console message");
+ }
+
+ for (let i = 0; i < outputNode.itemCount; i++) {
+ outputNode.getItemAtIndex(i).timestamp = 0; // a "far past" value
+ }
+
+ jsterm.execute("2");
+ waitForSuccess(waitForThirdMessage);
+ },
+ failureFn: finishTest,
+ };
+
+ let waitForThirdMessage = {
+ name: "one group divider exists after the third console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-new-group").length == 1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ };
+
+ let timestamp0 = Date.now();
+ jsterm.execute("0");
+
+ waitForSuccess({
+ name: "no group dividers exist after the first console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-new-group").length == 0;
+ },
+ successFn: function()
+ {
+ jsterm.execute("1");
+ waitForSuccess(waitForSecondMessage);
+ },
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
new file mode 100644
index 000000000..d1c9487c3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
@@ -0,0 +1,36 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 ***** */
+
+// Tests that appropriately-localized timestamps are printed.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("DOMContentLoaded", testTimestamp, false);
+
+ function testTimestamp()
+ {
+ browser.removeEventListener("DOMContentLoaded", testTimestamp, false);
+ const TEST_TIMESTAMP = 12345678;
+ let date = new Date(TEST_TIMESTAMP);
+ let localizedString = WCU_l10n.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");
+ finishTest();
+ }
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
new file mode 100644
index 000000000..154799370
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
@@ -0,0 +1,68 @@
+/* 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 exceptions thrown by content don't show up twice in the Web
+// Console.
+
+const TEST_DUPLICATE_ERROR_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-duplicate-error.html";
+
+function test() {
+ expectUncaughtException();
+ addTab(TEST_DUPLICATE_ERROR_URI);
+ browser.addEventListener("DOMContentLoaded", testDuplicateErrors, false);
+}
+
+function testDuplicateErrors() {
+ browser.removeEventListener("DOMContentLoaded", testDuplicateErrors,
+ false);
+ openConsole(null, function(hud) {
+ hud.jsterm.clearOutput();
+
+ Services.console.registerListener(consoleObserver);
+
+ expectUncaughtException();
+ content.location.reload();
+ });
+}
+
+var consoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function (aMessage)
+ {
+ // we ignore errors we don't care about
+ if (!(aMessage instanceof Ci.nsIScriptError) ||
+ aMessage.category != "content javascript") {
+ return;
+ }
+
+ Services.console.unregisterListener(this);
+
+ outputNode = HUDService.getHudByWindow(content).outputNode;
+
+ waitForSuccess({
+ name: "fooDuplicateError1 error displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("fooDuplicateError1") > -1;
+ },
+ successFn: function()
+ {
+ let text = 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");
+ }
+
+ findLogEntry("test-duplicate-error.html");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+ }
+};
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js b/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
new file mode 100644
index 000000000..ceeac816d
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/browser/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testCompletion);
+ }, true);
+}
+
+function testCompletion(hud) {
+ var jsterm = hud.jsterm;
+ var 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");
+
+ jsterm = input = null;
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
new file mode 100644
index 000000000..1f142ce74
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
@@ -0,0 +1,109 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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>
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 585237";
+let hud, testDriver;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ testDriver = testGen();
+ testNext();
+ });
+ }, true);
+}
+
+function testNext() {
+ testDriver.next();
+}
+
+function testGen() {
+ let console = content.console;
+ 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++) {
+ console.log("foo #" + i); // must change message to prevent repeats
+ }
+
+ waitForSuccess({
+ name: "20 console.log messages displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("foo #29") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ is(countMessageNodes(), 20, "there are 20 message nodes in the output " +
+ "when the log limit is set to 20");
+
+ console.log("bar bug585237");
+
+ waitForSuccess({
+ name: "another console.log message displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bar bug585237") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ 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++) {
+ console.log("boo #" + i); // must change message to prevent repeats
+ }
+
+ waitForSuccess({
+ name: "another 20 console.log message displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("boo #19") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ is(countMessageNodes(), 30, "there are 30 message nodes in the output " +
+ "when the log limit is set to 30");
+
+ prefBranch.clearUserPref("console");
+ hud = testDriver = prefBranch = console = outputNode = null;
+ finishTest();
+
+ yield;
+}
+
+function countMessageNodes() {
+ return outputNode.querySelectorAll(".hud-msg-node").length;
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
new file mode 100644
index 000000000..a062ebd99
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -0,0 +1,50 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-585956-console-trace.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", tabLoaded, true);
+}
+
+function tabLoaded() {
+ browser.removeEventListener("load", tabLoaded, true);
+
+ openConsole(null, function(hud) {
+ content.location.reload();
+
+ waitForSuccess({
+ name: "stacktrace message",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".hud-log");
+ },
+ successFn: performChecks,
+ failureFn: finishTest,
+ });
+ });
+}
+
+function performChecks() {
+ // The expected stack trace object.
+ let stacktrace = [
+ { filename: TEST_URI, lineNumber: 9, functionName: "window.foobar585956c", language: 2 },
+ { filename: TEST_URI, lineNumber: 14, functionName: "foobar585956b", language: 2 },
+ { filename: TEST_URI, lineNumber: 18, functionName: "foobar585956a", language: 2 },
+ { filename: TEST_URI, lineNumber: 21, functionName: null, language: 2 }
+ ];
+
+ let hudId = HUDService.getHudIdByWindow(content);
+ let HUD = HUDService.hudReferences[hudId];
+
+ let node = HUD.outputNode.querySelector(".hud-log");
+ ok(node, "found trace log node");
+ ok(node._stacktrace, "found stacktrace object");
+ is(node._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct");
+ isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
+
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
new file mode 100644
index 000000000..21dcc603b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -0,0 +1,283 @@
+/* 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/. */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete popup keyboard usage test";
+let HUD, popup, jsterm, inputNode, completeNode;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ HUD = aHud;
+ info("web console opened");
+
+ content.wrappedJSObject.foobarBug585991 = {
+ "item0": "value0",
+ "item1": "value1",
+ "item2": "value2",
+ "item3": "value3",
+ };
+
+ jsterm = HUD.jsterm;
+ popup = jsterm.autocompletePopup;
+ completeNode = jsterm.completeNode;
+ inputNode = jsterm.inputNode;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", onShown, false);
+
+ ok(popup.isOpen, "popup is open");
+
+ // 4 values, and the following properties:
+ // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__
+ // hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString
+ // toSource unwatch valueOf watch constructor.
+ is(popup.itemCount, 18, "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__",
+ "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, 17,
+ "Index of the first item from bottom is selected.");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.inputNode.value.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");
+
+ info("press Tab and wait for popup to hide");
+ popup._panel.addEventListener("popuphidden", popupHideAfterTab, false);
+ EventUtils.synthesizeKey("VK_TAB", {});
+ }, false);
+
+ info("wait for completion: window.foobarBug585991.");
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+}
+
+function popupHideAfterTab()
+{
+ // At this point the completion suggestion should be accepted.
+ popup._panel.removeEventListener("popuphidden", popupHideAfterTab, false);
+
+ ok(!popup.isOpen, "popup is not open");
+
+ is(inputNode.value, "window.foobarBug585991.watch",
+ "completion was successful after VK_TAB");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", onShown, false);
+
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 18, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 17, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.inputNode.value.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._panel.addEventListener("popuphidden", function onHidden() {
+ popup._panel.removeEventListener("popuphidden", onHidden, false);
+
+ ok(!popup.isOpen, "popup is not open after VK_ESCAPE");
+
+ is(inputNode.value, "window.foobarBug585991.",
+ "completion was cancelled");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ executeSoon(testReturnKey);
+ }, 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(".", {});
+ });
+}
+
+function testReturnKey()
+{
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", onShown, false);
+
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 18, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 17, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.inputNode.value.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._panel.addEventListener("popuphidden", function onHidden() {
+ popup._panel.removeEventListener("popuphidden", onHidden, false);
+
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(inputNode.value, "window.foobarBug585991.valueOf",
+ "completion was successful after VK_RETURN");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ dontShowArrayNumbers();
+ }, 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(".", {});
+ });
+}
+
+function dontShowArrayNumbers()
+{
+ info("dontShowArrayNumbers");
+ content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"];
+
+ let jsterm = HUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let completeNode = jsterm.completeNode;
+
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", onShown, false);
+
+ let sameItems = popup.getItems().map(function(e) {return e.label;});
+ ok(!sameItems.some(function(prop, index) { prop === "0"; }),
+ "Completing on an array doesn't show numbers.");
+
+ popup._panel.addEventListener("popuphidden", testReturnWithNoSelection, 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(".", {});
+ });
+}
+
+function testReturnWithNoSelection()
+{
+ popup._panel.removeEventListener("popuphidden", testReturnWithNoSelection, false);
+
+ info("test pressing return with open popup, but no selection, see bug 873250");
+ content.wrappedJSObject.testBug873250a = "hello world";
+ content.wrappedJSObject.testBug873250b = "hello world 2";
+
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", 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._panel.addEventListener("popuphidden", popupHideAfterReturnWithNoSelection);
+ executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+ });
+
+ executeSoon(() => {
+ info("wait for popup to show");
+ jsterm.setInputValue("window.testBu");
+ EventUtils.synthesizeKey("g", {});
+ });
+}
+
+function popupHideAfterReturnWithNoSelection()
+{
+ popup._panel.removeEventListener("popuphidden", popupHideAfterReturnWithNoSelection);
+
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(inputNode.value, "", "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");
+
+ executeSoon(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
new file mode 100644
index 000000000..eb9134b4a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -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/. */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete popup test";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(HUD) {
+ let items = [
+ {label: "item0", value: "value0"},
+ {label: "item1", value: "value1"},
+ {label: "item2", value: "value2"},
+ ];
+
+ let popup = HUD.jsterm.autocompletePopup;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup._panel.addEventListener("popupshown", function() {
+ popup._panel.removeEventListener("popupshown", arguments.callee, false);
+
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 0, "no items");
+
+ popup.setItems(items);
+
+ is(popup.itemCount, items.length, "items added");
+
+ let sameItems = popup.getItems();
+ is(sameItems.every(function(aItem, aIndex) {
+ return aItem === items[aIndex];
+ }), 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");
+
+ popup.selectedIndex = 1;
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+
+ popup.selectedItem = items[2];
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+
+ is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works");
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+
+ is(popup.selectNextItem(), items[2], "selectPreviousItem() works");
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+
+ ok(!popup.selectNextItem(), "selectPreviousItem() works");
+
+ is(popup.selectedIndex, -1, "no index is selected");
+ ok(!popup.selectedItem, "no item is selected");
+
+ 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");
+
+ popup.removeItem(items[2]);
+
+ is(popup.selectedIndex, 2, "index2 is selected");
+ is(popup.selectedItem, items[3], "item3 is still selected");
+ is(popup.itemCount, items.length - 1, "item2 removed");
+
+ popup.clearItems();
+ is(popup.itemCount, 0, "items cleared");
+
+ popup.hidePopup();
+ finishTest();
+ }, false);
+
+ popup.openPopup();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js b/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js
new file mode 100644
index 000000000..b5bba5003
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js
@@ -0,0 +1,81 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 ***** */
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testSelectionWhenMovingBetweenBoxes);
+ }, true);
+}
+
+function testSelectionWhenMovingBetweenBoxes(hud) {
+ let jsterm = hud.jsterm;
+
+ // Fill the console with some output.
+ jsterm.clearOutput();
+ jsterm.execute("1 + 2");
+ jsterm.execute("3 + 4");
+ jsterm.execute("5 + 6");
+
+ waitForSuccess({
+ name: "execution results displayed",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("5 + 6") > -1 &&
+ hud.outputNode.textContent.indexOf("11") > -1;
+ },
+ successFn: performTestsAfterOutput.bind(null, hud),
+ failureFn: finishTest,
+ });
+}
+
+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(outputNode);
+ is(outputNode.selectedCount, outputNode.childNodes.length, "all console " +
+ "messages are selected after performing a regular browser select-all " +
+ "operation");
+
+ outputNode.selectedIndex = -1;
+
+ // Test the context menu "Select All" (which has a different code path) works
+ // properly as well.
+ let contextMenuId = outputNode.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();
+
+ is(outputNode.selectedCount, outputNode.childNodes.length, "all console " +
+ "messages are selected after performing a select-all operation from " +
+ "the context menu");
+
+ outputNode.selectedIndex = -1;
+
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js
new file mode 100644
index 000000000..8ad4a4b70
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js
@@ -0,0 +1,91 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ * Patrick Walton <pcwalton@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let HUD, outputNode;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ HUD = aHud;
+
+ // See bugs 574036, 586386 and 587617.
+ outputNode = HUD.outputNode;
+ let selection = getSelection();
+ let jstermInput = HUD.jsterm.inputNode;
+ let console = content.wrappedJSObject.console;
+ let contentSelection = content.wrappedJSObject.getSelection();
+
+ HUD.jsterm.clearOutput();
+
+ let controller = top.document.commandDispatcher.
+ getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ console.log("Hello world! bug587617");
+
+ waitForSuccess({
+ name: "console log 'Hello world!' message",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug587617") > -1;
+ },
+ successFn: function()
+ {
+ outputNode.selectedIndex = 0;
+ outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+ controller = top.document.commandDispatcher.
+ getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+ let selectedNode = outputNode.getItemAtIndex(0);
+ waitForClipboard(getExpectedClipboardText(selectedNode), clipboardSetup,
+ testContextMenuCopy, testContextMenuCopy);
+ },
+ failureFn: finishTest,
+ });
+}
+
+// Test that the context menu "Copy" (which has a different code path) works
+// properly as well.
+function testContextMenuCopy() {
+ let contextMenuId = outputNode.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");
+
+ copyItem.doCommand();
+
+ let selectedNode = outputNode.getItemAtIndex(0);
+
+ HUD = outputNode = null;
+ waitForClipboard(getExpectedClipboardText(selectedNode), clipboardSetup,
+ finishTest, finishTest);
+}
+
+function getExpectedClipboardText(aItem) {
+ return "[" + WCU_l10n.timestampString(aItem.timestamp) + "] " +
+ aItem.clipboardText;
+}
+
+function clipboardSetup() {
+ goDoCommand("cmd_copy");
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js
new file mode 100644
index 000000000..a27c64b02
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js
@@ -0,0 +1,39 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 588342";
+let fm;
+
+function test() {
+ fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud) {
+ waitForFocus(function() {
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+ "jsterm input is focused on web console open");
+ isnot(fm.focusedWindow, content, "content document has no focus");
+ closeConsole(null, consoleClosed);
+ }, hud.iframeWindow);
+}
+
+function consoleClosed() {
+ is(fm.focusedWindow, browser.contentWindow,
+ "content document has focus");
+
+ fm = null;
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
new file mode 100644
index 000000000..74f56aa77
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
@@ -0,0 +1,49 @@
+/* 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 adding text to one of the output labels doesn't cause errors.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testTextNodeInsertion);
+ }, true);
+}
+
+// Test for bug 588730: Adding a text node to an existing label element causes
+// warnings
+function testTextNodeInsertion(hud) {
+ 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");
+
+ finishTest();
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
new file mode 100644
index 000000000..c22894924
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testInputExpansion);
+ }, true);
+}
+
+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;
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js b/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js
new file mode 100644
index 000000000..95e032f99
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js
@@ -0,0 +1,62 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ * Patrick Walton <pcwalton@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" +
+ "foobarCssParser:baz'>test CSS parser filter</div>";
+
+function onContentLoaded()
+{
+ browser.removeEventListener("load", onContentLoaded, true);
+
+ let HUD = HUDService.getHudByWindow(content);
+ let hudId = HUD.hudId;
+ let outputNode = HUD.outputNode;
+
+ HUD.jsterm.clearOutput();
+
+ waitForSuccess({
+ name: "css error displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("foobarCssParser") > -1;
+ },
+ successFn: function()
+ {
+ HUD.setFilterState("cssparser", false);
+
+ let msg = "the unknown CSS property warning is not displayed, " +
+ "after filtering";
+ testLogEntry(outputNode, "foobarCssParser", msg, true, true);
+
+ HUD.setFilterState("cssparser", true);
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
+
+/**
+ * Unit test for bug 589162:
+ * CSS filtering on the console does not work
+ */
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function() {
+ browser.addEventListener("load", onContentLoaded, true);
+ content.location.reload();
+ });
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js b/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
new file mode 100644
index 000000000..fbea54e9d
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
@@ -0,0 +1,41 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Julian Viereck <jviereck@mozilla.com>
+ * Patrick Walton <pcwalton@mozilla.com>
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that, when the user types an extraneous closing bracket, no error
+// appears.
+
+function test() {
+ addTab("data:text/html;charset=utf-8,test for bug 592442");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testExtraneousClosingBrackets);
+ }, true);
+}
+
+function testExtraneousClosingBrackets(hud) {
+ 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");
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js b/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
new file mode 100644
index 000000000..9fe88e128
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
@@ -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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html";
+
+const TEST_IFRAME_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html";
+
+const TEST_DUMMY_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let tab1, tab2;
+
+function test() {
+ addTab(TEST_URI);
+ tab1 = tab;
+ browser.addEventListener("load", tab1Loaded, true);
+}
+
+function tab1Loaded(aEvent) {
+ browser.removeEventListener(aEvent.type, tab1Loaded, true);
+ content.console.log("FOO");
+ openConsole(null, function() {
+ tab2 = gBrowser.addTab(TEST_DUMMY_URI);
+ gBrowser.selectedTab = tab2;
+ gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true);
+ });
+}
+
+function tab2Loaded(aEvent) {
+ tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
+
+ openConsole(gBrowser.selectedTab, function() {
+ tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true);
+ tab1.linkedBrowser.contentWindow.location.reload();
+ });
+}
+
+function tab1Reloaded(aEvent) {
+ tab1.linkedBrowser.removeEventListener(aEvent.type, tab1Reloaded, true);
+
+ let hud1 = HUDService.getHudByWindow(tab1.linkedBrowser.contentWindow);
+ let outputNode1 = hud1.outputNode;
+
+ waitForSuccess({
+ name: "iframe network request displayed in tab1",
+ validatorFn: function()
+ {
+ let selector = ".webconsole-msg-url[value='" + TEST_IFRAME_URI +"']";
+ return outputNode1.querySelector(selector);
+ },
+ successFn: function()
+ {
+ let hud2 = HUDService.getHudByWindow(tab2.linkedBrowser.contentWindow);
+ 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);
+
+ testEnd();
+ },
+ failureFn: testEnd,
+ });
+}
+
+function testEnd() {
+ closeConsole(tab2, function() {
+ gBrowser.removeTab(tab2);
+ tab1 = tab2 = null;
+ executeSoon(finishTest);
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
new file mode 100644
index 000000000..c2dc7f753
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
@@ -0,0 +1,132 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+let HUD;
+
+let outputItem;
+
+function consoleOpened(aHud) {
+ HUD = aHud;
+
+ outputNode = HUD.outputNode;
+
+ browser.addEventListener("load", tabLoad2, true);
+
+ // Reload so we get some output in the console.
+ browser.contentWindow.location.reload();
+}
+
+function tabLoad2(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad2, true);
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(([result]) => {
+ let msg = [...result.matched][0];
+ outputItem = msg.querySelector(".hud-clickable");
+ ok(outputItem, "found a network message");
+ document.addEventListener("popupshown", networkPanelShown, false);
+
+ // Send the mousedown and click events such that the network panel opens.
+ EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
+ EventUtils.sendMouseEvent({type: "click"}, outputItem);
+ });
+}
+
+function networkPanelShown(aEvent) {
+ document.removeEventListener(aEvent.type, networkPanelShown, false);
+
+ info("networkPanelShown");
+
+ document.addEventListener("popupshown", networkPanelShowFailure, false);
+
+ // The network panel should not open for the second time.
+ EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
+ EventUtils.sendMouseEvent({type: "click"}, outputItem);
+
+ executeSoon(function() {
+ aEvent.target.addEventListener("popuphidden", networkPanelHidden, false);
+ aEvent.target.hidePopup();
+ });
+}
+
+function networkPanelShowFailure(aEvent) {
+ document.removeEventListener(aEvent.type, networkPanelShowFailure, false);
+
+ ok(false, "the network panel should not show");
+}
+
+function networkPanelHidden(aEvent) {
+ this.removeEventListener(aEvent.type, networkPanelHidden, false);
+
+ info("networkPanelHidden");
+
+ // The network panel should not show because this is a mouse event that starts
+ // in a position and ends in another.
+ EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
+ outputItem);
+ EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6},
+ outputItem);
+
+ // The network panel should not show because this is a middle-click.
+ EventUtils.sendMouseEvent({type: "mousedown", button: 1},
+ outputItem);
+ EventUtils.sendMouseEvent({type: "click", button: 1},
+ outputItem);
+
+ // The network panel should not show because this is a right-click.
+ EventUtils.sendMouseEvent({type: "mousedown", button: 2},
+ outputItem);
+ EventUtils.sendMouseEvent({type: "click", button: 2},
+ outputItem);
+
+ executeSoon(function() {
+ document.removeEventListener("popupshown", networkPanelShowFailure, false);
+
+ // Done with the network output. Now test the jsterm output and the property
+ // panel.
+ HUD.jsterm.execute("document", () => {
+ info("jsterm execute 'document' callback");
+
+ HUD.jsterm.once("variablesview-open", onVariablesViewOpen);
+ let outputItem = outputNode
+ .querySelector(".webconsole-msg-output .hud-clickable");
+ ok(outputItem, "jsterm output message found");
+
+ // Send the mousedown and click events such that the property panel opens.
+ EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
+ EventUtils.sendMouseEvent({type: "click"}, outputItem);
+ });
+ });
+}
+
+function onVariablesViewOpen() {
+ info("onVariablesViewOpen");
+
+ executeSoon(function() {
+ HUD = outputItem = null;
+ executeSoon(finishTest);
+ });
+}
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js b/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
new file mode 100644
index 000000000..2cdf52bc9
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
@@ -0,0 +1,157 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let inputNode, values;
+
+function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+
+ openConsole(null, function(HUD) {
+ inputNode = HUD.jsterm.inputNode;
+
+ inputNode.focus();
+
+ ok(!inputNode.value, "inputNode.value 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++) {
+ HUD.jsterm.setInputValue(values[i]);
+ HUD.jsterm.execute();
+ }
+
+ performTests();
+ });
+}
+
+function performTests() {
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[4],
+ "VK_UP: inputNode.value #4 is correct");
+
+ ok(inputNode.selectionStart == values[4].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[3],
+ "VK_UP: inputNode.value #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(inputNode.value, values[3],
+ "VK_UP two times: inputNode.value #3 is correct");
+
+ ok(inputNode.selectionStart == inputNode.value.indexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[3],
+ "VK_UP again: inputNode.value #3 is correct");
+
+ ok(inputNode.selectionStart == 0 &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[2],
+ "VK_UP: inputNode.value #2 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[1],
+ "VK_UP: inputNode.value #1 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(inputNode.value, values[0],
+ "VK_UP: inputNode.value #0 is correct");
+
+ ok(inputNode.selectionStart == values[0].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(inputNode.value, values[1],
+ "VK_DOWN: inputNode.value #1 is correct");
+
+ ok(inputNode.selectionStart == values[1].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(inputNode.value, values[2],
+ "VK_DOWN: inputNode.value #2 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(inputNode.value, values[3],
+ "VK_DOWN: inputNode.value #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(inputNode.value, values[3],
+ "VK_DOWN two times: inputNode.value #3 is correct");
+
+ ok(inputNode.selectionStart > inputNode.value.lastIndexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(inputNode.value, values[3],
+ "VK_DOWN again: inputNode.value #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(inputNode.value, values[4],
+ "VK_DOWN: inputNode.value #4 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ ok(!inputNode.value,
+ "VK_DOWN: inputNode.value is empty");
+
+ inputNode = values = null;
+ executeSoon(finishTest);
+}
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 594497 and bug 619598");
+ browser.addEventListener("load", tabLoad, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js
new file mode 100644
index 000000000..7fe96922e
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+const TEST_FILE = "test-network.html";
+
+function tabReload(aEvent) {
+ browser.removeEventListener(aEvent.type, tabReload, true);
+
+ outputNode = hud.outputNode;
+
+ waitForSuccess({
+ name: "console.log() message displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent
+ .indexOf("running network console logging tests") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("test-network.html");
+ findLogEntry("test-image.png");
+ findLogEntry("testscript.js");
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
+
+function test() {
+ let jar = getJar(getRootDirectory(gTestPath));
+ let dir = jar ?
+ extractJarToTmp(jar) :
+ getChromeDir(getResolvedURI(gTestPath));
+ dir.append(TEST_FILE);
+
+ let uri = Services.io.newFileURI(dir);
+
+ const PREF = "devtools.webconsole.persistlog";
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ addTab("data:text/html;charset=utf8,<p>test file URI");
+ browser.addEventListener("load", function tabLoad() {
+ browser.removeEventListener("load", tabLoad, true);
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ hud.jsterm.clearOutput();
+ browser.addEventListener("load", tabReload, true);
+ content.location = uri.spec;
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
new file mode 100644
index 000000000..a1c96b9c1
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
@@ -0,0 +1,101 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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>
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console doesn't leak when multiple tabs and windows are
+// opened and then closed.
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350";
+
+let win1 = window, win2;
+let openTabs = [];
+let 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() {
+ // open the Web Console for each of the four tabs and log a message.
+ let consolesOpen = 0;
+ for (let i = 0; i < openTabs.length; i++) {
+ let tab = openTabs[i];
+ openConsole(tab, function(index, hud) {
+ ok(hud, "HUD is open for tab " + index);
+ let window = hud.target.tab.linkedBrowser.contentWindow;
+ window.console.log("message for tab " + index);
+ consolesOpen++;
+ if (consolesOpen == 4) {
+ // Use executeSoon() to allow the promise to resolve.
+ executeSoon(closeConsoles);
+ }
+ }.bind(null, i));
+ }
+}
+
+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/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js
new file mode 100644
index 000000000..8e00c27ed
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js
@@ -0,0 +1,209 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TESTS_PATH = "http://example.com/browser/browser/devtools/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: "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",
+ expectError: true,
+ },
+ { // #4
+ file: "test-bug-595934-malformedxml.xhtml",
+ category: "malformed-xml",
+ matchString: "no element found",
+ },
+ { // #5
+ file: "test-bug-595934-svg.xhtml",
+ category: "SVG",
+ matchString: "fooBarSVG",
+ },
+ { // #6
+ file: "test-bug-595934-canvas.html",
+ category: "Canvas",
+ matchString: "strokeStyle",
+ },
+ { // #7
+ file: "test-bug-595934-css-parser.html",
+ category: "CSS Parser",
+ matchString: "foobarCssParser",
+ },
+ { // #8
+ file: "test-bug-595934-malformedxml-external.html",
+ category: "malformed-xml",
+ matchString: "</html>",
+ },
+ { // #9
+ file: "test-bug-595934-empty-getelementbyid.html",
+ category: "DOM",
+ matchString: "getElementById",
+ },
+ { // #10
+ file: "test-bug-595934-canvas-css.html",
+ category: "CSS Parser",
+ matchString: "foobarCanvasCssParser",
+ },
+ { // #11
+ file: "test-bug-595934-image.html",
+ category: "Image",
+ matchString: "corrupt",
+ },
+];
+
+let pos = -1;
+
+let foundCategory = false;
+let foundText = false;
+let pageLoaded = false;
+let pageError = false;
+let output = null;
+let jsterm = null;
+let hud = null;
+let testEnded = false;
+
+let TestObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function test_observe(aSubject)
+ {
+ if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) {
+ return;
+ }
+
+ var expectedCategory = TESTS[pos].category;
+
+ info("test #" + pos + " console observer got " + aSubject.category +
+ ", is expecting " + expectedCategory);
+
+ if (aSubject.category == expectedCategory) {
+ foundCategory = true;
+ startNextTest();
+ }
+ else {
+ info("unexpected message was: " + aSubject.sourceName + ":" +
+ aSubject.lineNumber + "; " + aSubject.errorMessage);
+ }
+ }
+};
+
+function consoleOpened(aHud) {
+ hud = aHud;
+ 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) {
+ let 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(aEvent) {
+ if (content.location.href != testLocation) {
+ return;
+ }
+ gBrowser.selectedBrowser.removeEventListener(aEvent.type, onLoad, true);
+
+ pageLoaded = true;
+ test.onload && test.onload(aEvent);
+
+ if (test.expectError) {
+ content.addEventListener("error", function _onError() {
+ content.removeEventListener("error", _onError);
+ pageError = true;
+ startNextTest();
+ });
+ expectUncaughtException();
+ }
+ else {
+ pageError = true;
+ }
+
+ startNextTest();
+ }, true);
+
+ content.location = 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);
+
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 595934 - message categories coverage.");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
new file mode 100644
index 000000000..aab5058c9
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
@@ -0,0 +1,106 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let tab1, tab2, win1, win2;
+let noErrors = true;
+
+function tab1Loaded(aEvent) {
+ browser.removeEventListener(aEvent.type, tab1Loaded, true);
+
+ 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");
+
+ Array.forEach(win1.gBrowser.tabs, function(aTab) {
+ win1.gBrowser.removeTab(aTab);
+ });
+ Array.forEach(win2.gBrowser.tabs, function(aTab) {
+ win2.gBrowser.removeTab(aTab);
+ });
+
+ executeSoon(function() {
+ win2.close();
+ tab1 = tab2 = win1 = win2 = null;
+ finishTest();
+ });
+ }
+
+ waitForFocus(openConsoles, tab2.linkedBrowser.contentWindow);
+}
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", tab1Loaded, true);
+ tab1 = gBrowser.selectedTab;
+ win1 = window;
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
new file mode 100644
index 000000000..c634b11bd
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
@@ -0,0 +1,43 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/" +
+ "webconsole/test/test-bug-597136-external-script-" +
+ "errors.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(hud) {
+ executeSoon(function() {
+ consoleOpened(hud);
+ });
+ });
+ }, true);
+}
+
+function consoleOpened(hud) {
+ let button = content.document.querySelector("button");
+ let outputNode = hud.outputNode;
+
+ expectUncaughtException();
+ EventUtils.sendMouseEvent({ type: "click" }, button, content);
+
+ waitForSuccess({
+ name: "external script error message",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bogus is not defined") > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
new file mode 100644
index 000000000..3aa0f5399
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
@@ -0,0 +1,47 @@
+/* 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.
+
+const TEST_URI = "http://example.com/";
+
+let good = true;
+let listener = {
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]),
+ observe: function(aSubject, aTopic, aData) {
+ if (aSubject instanceof Ci.nsIScriptError &&
+ aSubject.category === "XPConnect JavaScript") {
+ good = false;
+ }
+ }
+};
+
+let xhr;
+
+function test() {
+ Services.console.registerListener(listener);
+
+ HUDService; // trigger a lazy-load of the HUD Service
+
+ 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 = null;
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
new file mode 100644
index 000000000..7f55cb0fc
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
@@ -0,0 +1,81 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network.html";
+
+function consoleOpened(aHud) {
+ hud = aHud;
+
+ 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,
+ successFn: testScroll,
+ failureFn: finishTest,
+ }],
+ }).then(testScroll);
+ content.location.reload();
+ });
+}
+
+function testScroll() {
+ let msgNode = hud.outputNode.querySelector(".webconsole-msg-network");
+ ok(msgNode.classList.contains("hud-filtered-by-type"),
+ "network message is filtered by type");
+ ok(msgNode.classList.contains("hud-filtered-by-string"),
+ "network message is filtered by string");
+
+ let scrollBox = hud.outputNode.scrollBoxObject.element;
+ ok(scrollBox.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 = hud.outputNode.querySelector(".hud-log").clientHeight;
+ ok(scrollBox.scrollTop >= scrollBox.scrollHeight - scrollBox.clientHeight -
+ nodeHeight * 2, "scroll location is correct");
+
+ hud.setFilterState("network", true);
+ hud.setFilterState("networkinfo", true);
+
+ executeSoon(finishTest);
+}
+
+function test() {
+ const PREF = "devtools.webconsole.persistlog";
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
new file mode 100644
index 000000000..b6636a079
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
@@ -0,0 +1,63 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html";
+
+let newTabIsOpen = false;
+
+function tabLoaded(aEvent) {
+ gBrowser.selectedBrowser.removeEventListener(aEvent.type, tabLoaded, true);
+
+ openConsole(gBrowser.selectedTab, function() {
+ gBrowser.selectedBrowser.addEventListener("load", tabReloaded, true);
+ expectUncaughtException();
+ content.location.reload();
+ });
+}
+
+function tabReloaded(aEvent) {
+ gBrowser.selectedBrowser.removeEventListener(aEvent.type, tabReloaded, true);
+
+ let hudId = HUDService.getHudIdByWindow(content);
+ let HUD = HUDService.hudReferences[hudId];
+ ok(HUD, "Web Console is open");
+
+ waitForSuccess({
+ name: "error message displayed",
+ validatorFn: function() {
+ return HUD.outputNode.textContent.indexOf("fooBug597756_error") > -1;
+ },
+ successFn: function() {
+ if (newTabIsOpen) {
+ finishTest();
+ return;
+ }
+ closeConsole(gBrowser.selectedTab, function() {
+ gBrowser.removeCurrentTab();
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+
+ newTabIsOpen = true;
+ gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
+ expectUncaughtException();
+ content.location = TEST_URI;
+ });
+ },
+ failureFn: finishTest,
+ });
+}
+
+function test() {
+ expectUncaughtException();
+ addTab(TEST_URI);
+ browser.addEventListener("load", tabLoaded, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
new file mode 100644
index 000000000..1737b2275
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
@@ -0,0 +1,275 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let testEnded = false;
+let pos = -1;
+
+let dateNow = Date.now();
+
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+
+let longString = (new Array(tempScope.DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+let initialString = longString.substring(0,
+ tempScope.DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+let inputValues = [
+ // [showsVariablesView?, input value, expected output format,
+ // print() output, console API output, optional console API test]
+
+ // 0
+ [false, "'hello \\nfrom \\rthe \\\"string world!'",
+ '"hello \nfrom \rthe "string world!"',
+ "hello \nfrom \rthe \"string world!"],
+
+ // 1
+ [false, "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
+ "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"",
+ "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165"],
+
+ // 2
+ [false, "window.location.href", '"' + TEST_URI + '"', TEST_URI],
+
+ // 3
+ [false, "0", "0"],
+
+ // 4
+ [false, "'0'", '"0"', "0"],
+
+ // 5
+ [false, "42", "42"],
+
+ // 6
+ [false, "'42'", '"42"', "42"],
+
+ // 7
+ [true, "/foobar/", "[object RegExp]", '"/foobar/"', "[object RegExp]"],
+
+ // 8
+ [false, "null", "null"],
+
+ // 9
+ [false, "undefined", "undefined"],
+
+ // 10
+ [false, "true", "true"],
+
+ // 11
+ [true, "document.getElementById", "[object Function]",
+ "function getElementById() {\n [native code]\n}",
+ "[object Function]"],
+
+ // 12
+ [true, "(function() { return 42; })", "[object Function]",
+ "function () { return 42; }", "[object Function]"],
+
+ // 13
+ [true, "new Date(" + dateNow + ")", "[object Date]", (new Date(dateNow)).toString(), "[object Date]"],
+
+ // 14
+ [true, "document.body", "[object HTMLBodyElement]"],
+
+ // 15
+ [true, "window.location", "[object Location]", TEST_URI, "[object Location]"],
+
+ // 16
+ [true, "[1,2,3,'a','b','c','4','5']", '[object Array]',
+ '1,2,3,a,b,c,4,5',
+ "[object Array]"],
+
+ // 17
+ [true, "({a:'b', c:'d', e:1, f:'2'})", "[object Object]"],
+
+ // 18
+ [false, "'" + longString + "'",
+ '"' + initialString + "\"[\u2026]", initialString],
+];
+
+longString = null;
+initialString = null;
+tempScope = null;
+
+let eventHandlers = [];
+let popupShown = [];
+let HUD;
+let testDriver;
+
+function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+
+ waitForFocus(function () {
+ openConsole(null, function(aHud) {
+ HUD = aHud;
+ testNext();
+ });
+ }, content);
+}
+
+function subtestNext() {
+ testDriver.next();
+}
+
+function testNext() {
+ pos++;
+ if (pos == inputValues.length) {
+ testEnd();
+ return;
+ }
+
+ testDriver = testGen();
+ testDriver.next();
+}
+
+function testGen() {
+ let cpos = pos;
+
+ let showsVariablesView = inputValues[cpos][0];
+ let inputValue = inputValues[cpos][1];
+ let expectedOutput = inputValues[cpos][2];
+
+ let printOutput = inputValues[cpos].length >= 4 ?
+ inputValues[cpos][3] : expectedOutput;
+
+ let consoleOutput = inputValues[cpos].length >= 5 ?
+ inputValues[cpos][4] : printOutput;
+
+ let consoleTest = inputValues[cpos][5] || inputValue;
+
+ HUD.jsterm.clearOutput();
+
+ // Test the console.log() output.
+
+ HUD.jsterm.execute("console.log(" + consoleTest + ")");
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ name: "console API output is correct for inputValues[" + cpos + "]",
+ text: consoleOutput,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(subtestNext);
+
+ yield undefined;
+
+ HUD.jsterm.clearOutput();
+
+ // Test jsterm print() output.
+
+ HUD.jsterm.setInputValue("print(" + inputValue + ")");
+ HUD.jsterm.execute();
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ name: "jsterm print() output is correct for inputValues[" + cpos + "]",
+ text: printOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ }).then(subtestNext);
+
+ yield undefined;
+
+ // Test jsterm execution output.
+
+ HUD.jsterm.clearOutput();
+ HUD.jsterm.setInputValue(inputValue);
+ HUD.jsterm.execute();
+
+ let outputItem;
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ name: "jsterm output is correct for inputValues[" + cpos + "]",
+ text: expectedOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ }).then(([result]) => {
+ outputItem = [...result.matched][0];
+ ok(outputItem, "found message element");
+ subtestNext();
+ });
+
+ yield undefined;
+
+ let messageBody = outputItem.querySelector(".webconsole-msg-body");
+ ok(messageBody, "we have the message body for inputValues[" + cpos + "]");
+
+ // Test click on output.
+ let eventHandlerID = eventHandlers.length + 1;
+
+ let variablesViewShown = function(aEvent, aView, aOptions) {
+ if (aOptions.label.indexOf(expectedOutput) == -1) {
+ return;
+ }
+
+ HUD.jsterm.off("variablesview-open", variablesViewShown);
+
+ eventHandlers[eventHandlerID] = null;
+
+ ok(showsVariablesView,
+ "the variables view shown for inputValues[" + cpos + "]");
+
+ popupShown[cpos] = true;
+
+ if (showsVariablesView) {
+ executeSoon(subtestNext);
+ }
+ };
+
+ HUD.jsterm.on("variablesview-open", variablesViewShown);
+
+ eventHandlers.push(variablesViewShown);
+
+ // Send the mousedown, mouseup and click events to check if the variables
+ // view opens.
+ EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window);
+ EventUtils.sendMouseEvent({ type: "click" }, messageBody, window);
+
+ if (showsVariablesView) {
+ yield undefined; // wait for the panel to open if we need to.
+ }
+
+ testNext();
+
+ yield undefined;
+}
+
+function testEnd() {
+ if (testEnded) {
+ return;
+ }
+
+ testEnded = true;
+
+ for (let i = 0; i < eventHandlers.length; i++) {
+ if (eventHandlers[i]) {
+ HUD.jsterm.off("variablesview-open", eventHandlers[i]);
+ }
+ }
+
+ for (let i = 0; i < inputValues.length; i++) {
+ if (inputValues[i][0] && !popupShown[i]) {
+ ok(false, "the variables view failed to show for inputValues[" + i + "]");
+ }
+ }
+
+ HUD = inputValues = testDriver = null;
+ executeSoon(finishTest);
+}
+
+function test() {
+ requestLongerTimeout(2);
+ addTab(TEST_URI);
+ browser.addEventListener("load", tabLoad, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
new file mode 100644
index 000000000..a913f5e0c
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
@@ -0,0 +1,76 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs";
+
+let loads = 0;
+function performTest(aRequest, aConsole)
+{
+ loads++;
+ ok(aRequest, "page load was logged");
+ if (loads != 2) {
+ return;
+ }
+
+ let headers = null;
+
+ function readHeader(aName)
+ {
+ for (let header of headers) {
+ if (header.name == aName) {
+ return header.value;
+ }
+ }
+ return null;
+ }
+
+ aConsole.webConsoleClient.getResponseHeaders(aRequest.actor,
+ function (aResponse) {
+ headers = aResponse.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");
+
+ if (contentType || contentLength == 60) {
+ console.debug("lastFinishedRequest", lastFinishedRequest,
+ "request", lastFinishedRequest.request,
+ "response", lastFinishedRequest.response,
+ "updates", lastFinishedRequest.updates,
+ "response headers", headers);
+ }
+
+ executeSoon(finishTest);
+ });
+
+ HUDService.lastFinishedRequestCallback = null;
+}
+
+function test()
+{
+ addTab("data:text/plain;charset=utf8,hello world");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, () => {
+ HUDService.lastFinishedRequestCallback = performTest;
+
+ browser.addEventListener("load", function onReload() {
+ browser.removeEventListener("load", onReload, true);
+ executeSoon(() => content.location.reload());
+ }, true);
+
+ executeSoon(() => content.location = TEST_URI);
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
new file mode 100644
index 000000000..3dd67379d
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html";
+
+function performTest(lastFinishedRequest, aConsole)
+{
+ ok(lastFinishedRequest, "charset test page was loaded and logged");
+ HUDService.lastFinishedRequestCallback = null;
+
+ executeSoon(() => {
+ aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
+ (aResponse) => {
+ ok(!aResponse.contentDiscarded, "response body was not discarded");
+
+ let body = aResponse.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.lastFinishedRequestCallback = null;
+ executeSoon(finishTest);
+ });
+ });
+}
+
+function test()
+{
+ addTab("data:text/html;charset=utf-8,Web Console - bug 600183 test");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(hud) {
+ hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
+ ok(hud.ui._saveRequestAndResponseBodies,
+ "The saveRequestAndResponseBodies property was successfully set.");
+
+ HUDService.lastFinishedRequestCallback = performTest;
+ content.location = TEST_URI;
+ });
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
new file mode 100644
index 000000000..5a3566a3e
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
@@ -0,0 +1,82 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-601177-log-levels.html";
+
+function performTest()
+{
+ let hudId = HUDService.getHudIdByWindow(content);
+ let HUD = HUDService.hudReferences[hudId];
+
+ findEntry(HUD, "hud-networkinfo", "test-bug-601177-log-levels.html",
+ "found test-bug-601177-log-levels.html");
+
+ findEntry(HUD, "hud-networkinfo", "test-bug-601177-log-levels.js",
+ "found test-bug-601177-log-levels.js");
+
+ findEntry(HUD, "hud-networkinfo", "test-image.png", "found test-image.png");
+
+ findEntry(HUD, "hud-network", "foobar-known-to-fail.png",
+ "found foobar-known-to-fail.png");
+
+ findEntry(HUD, "hud-exception", "foobarBug601177exception",
+ "found exception");
+
+ findEntry(HUD, "hud-jswarn", "undefinedPropertyBug601177",
+ "found strict warning");
+
+ findEntry(HUD, "hud-jswarn", "foobarBug601177strictError",
+ "found strict error");
+
+ executeSoon(finishTest);
+}
+
+function findEntry(aHUD, aClass, aString, aMessage)
+{
+ return testLogEntry(aHUD.outputNode, aString, aMessage, false, false,
+ aClass);
+}
+
+function test()
+{
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 601177: log levels");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(hud) {
+ browser.addEventListener("load", function onLoad2() {
+ browser.removeEventListener("load", onLoad2, true);
+ waitForSuccess({
+ name: "all messages displayed",
+ validatorFn: function()
+ {
+ return hud.outputNode.itemCount >= 7;
+ },
+ successFn: performTest,
+ failureFn: function() {
+ info("itemCount: " + hud.outputNode.itemCount);
+ finishTest();
+ },
+ });
+ }, true);
+
+ expectUncaughtException();
+ content.location = TEST_URI;
+ });
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
new file mode 100644
index 000000000..2ff469db2
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
@@ -0,0 +1,75 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+function consoleOpened(HUD) {
+ HUD.jsterm.clearOutput();
+
+ let longMessage = "";
+ for (let i = 0; i < 50; i++) {
+ longMessage += "LongNonwrappingMessage";
+ }
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test message " + i);
+ }
+
+ content.console.log(longMessage);
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test message " + i);
+ }
+
+ HUD.jsterm.execute("1+1");
+
+ function performTest() {
+ let scrollBox = HUD.outputNode.scrollBoxObject.element;
+ isnot(scrollBox.scrollTop, 0, "scroll location is not at the top");
+
+ let node = HUD.outputNode.getItemAtIndex(HUD.outputNode.itemCount - 1);
+ let rectNode = node.getBoundingClientRect();
+ let rectOutput = HUD.outputNode.getBoundingClientRect();
+
+ // Visible scroll viewport.
+ let height = scrollBox.scrollHeight - scrollBox.scrollTop;
+
+ // Top position of the last message node, relative to the outputNode.
+ let top = rectNode.top - rectOutput.top;
+
+ // Bottom position of the last message node, relative to the outputNode.
+ let bottom = rectNode.bottom - rectOutput.top;
+
+ ok(top >= 0 && Math.floor(bottom) <= height + 1,
+ "last message is visible");
+
+ finishTest();
+ };
+
+ waitForSuccess({
+ name: "console output displayed",
+ validatorFn: function()
+ {
+ return HUD.outputNode.itemCount >= 103;
+ },
+ successFn: performTest,
+ failureFn: function() {
+ info("itemCount: " + HUD.outputNode.itemCount);
+ finishTest();
+ },
+ });
+}
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
+ browser.addEventListener("load", function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
new file mode 100644
index 000000000..6872ffc0d
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the filter button UI logic works correctly.
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testFilterButtons);
+ }, true);
+}
+
+function testFilterButtons(aHud) {
+ hud = aHud;
+ hudId = hud.hudId;
+ hudBox = hud.ui.rootElement;
+
+ testMenuFilterButton("net");
+ testMenuFilterButton("css");
+ testMenuFilterButton("js");
+ testMenuFilterButton("logging");
+
+ finishTest();
+}
+
+function testMenuFilterButton(aCategory) {
+ let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]";
+ let button = hudBox.querySelector(selector);
+ ok(button, "we have the \"" + aCategory + "\" button");
+
+ let firstMenuItem = button.querySelector("menuitem");
+ ok(firstMenuItem, "we have the first menu item for the \"" + aCategory +
+ "\" button");
+
+ // Turn all the filters off, if they were on.
+ let menuItem = firstMenuItem;
+ while (menuItem != null) {
+ if (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 " +
+ aCategory + " 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 " + aCategory + " 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 " +
+ aCategory + " 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 " + aCategory + " is still " +
+ "checked after turning off its first menu item");
+
+ // Turn all the filters off by clicking the main part of the button.
+ let anonymousNodes = hud.ui.document.getAnonymousNodes(button);
+ let subbutton;
+ for (let i = 0; i < anonymousNodes.length; i++) {
+ let node = anonymousNodes[i];
+ if (node.classList.contains("toolbarbutton-menubutton-button")) {
+ subbutton = node;
+ break;
+ }
+ }
+ ok(subbutton, "we have the subbutton for category " + aCategory);
+
+ clickButton(subbutton);
+ ok(!isChecked(button), "the button for category " + aCategory + " is " +
+ "no longer checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ let prefKey = menuItem.getAttribute("prefKey");
+ ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+ aCategory + " 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 " + aCategory + " is " +
+ "checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey")) {
+ let prefKey = menuItem.getAttribute("prefKey");
+ ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+ aCategory + " 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) {
+ chooseMenuItem(menuItem);
+ menuItem = menuItem.nextSibling;
+ }
+
+ ok(!isChecked(button), "the button for category " + aCategory + " is " +
+ "unchecked after unchecking all its filters");
+
+ // Turn all the filters on again by clicking the button.
+ clickButton(subbutton);
+}
+
+function clickButton(aNode) {
+ EventUtils.sendMouseEvent({ type: "click" }, aNode);
+}
+
+function chooseMenuItem(aNode) {
+ let event = document.createEvent("XULCommandEvent");
+ event.initCommandEvent("command", true, true, window, 0, false, false, false,
+ false, null);
+ aNode.dispatchEvent(event);
+}
+
+function isChecked(aNode) {
+ return aNode.getAttribute("checked") === "true";
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
new file mode 100644
index 000000000..d4ed63916
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
@@ -0,0 +1,182 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let menuitems = [], menupopups = [], huds = [], tabs = [], runCount = 0;
+
+function test()
+{
+ if (runCount == 0) {
+ requestLongerTimeout(2);
+ }
+
+ // open tab 1
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 1");
+ tabs.push(tab);
+
+ browser.addEventListener("load", function onLoad1(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad1, true);
+
+ openConsole(null, (hud) => hud.iframeWindow.mozRequestAnimationFrame(() => {
+ info("iframe1 root height " + hud.ui.rootElement.clientHeight);
+
+ // open tab 2
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 2");
+ tabs.push(tab);
+
+ browser.addEventListener("load", function onLoad2(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad2, true);
+
+ openConsole(null, (hud) => hud.iframeWindow.mozRequestAnimationFrame(startTest));
+ }, true);
+ }));
+ }, true);
+}
+
+function startTest()
+{
+ // Find the relevant elements in the Web Console of tab 2.
+ let win2 = tabs[runCount*2 + 1].linkedBrowser.contentWindow;
+ let hudId2 = HUDService.getHudIdByWindow(win2);
+ huds[1] = HUDService.hudReferences[hudId2];
+ info("startTest: iframe2 root height " + huds[1].ui.rootElement.clientHeight);
+
+ if (runCount == 0) {
+ menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodies");
+ }
+ else {
+ menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodiesContextMenu");
+ }
+ menupopups[1] = menuitems[1].parentNode;
+
+ // Open the context menu from tab 2.
+ menupopups[1].addEventListener("popupshown", onpopupshown2, false);
+ executeSoon(function() {
+ menupopups[1].openPopup();
+ });
+}
+
+function onpopupshown2(aEvent)
+{
+ menupopups[1].removeEventListener(aEvent.type, onpopupshown2, false);
+
+ // By default bodies are not logged.
+ isnot(menuitems[1].getAttribute("checked"), "true",
+ "menuitems[1] is not checked");
+
+ ok(!huds[1].ui._saveRequestAndResponseBodies, "bodies are not logged");
+
+ // Enable body logging.
+ huds[1].ui.setSaveRequestAndResponseBodies(true).then(() => {
+ menupopups[1].hidePopup();
+ });
+
+ menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+ menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
+
+ info("menupopups[1] hidden");
+
+ // Reopen the context menu.
+ huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2b(aEvent));
+ menupopups[1].openPopup();
+ }, false);
+}
+
+function testpopup2b(aEvent) {
+ is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked");
+
+ menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+ menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
+
+ info("menupopups[1] hidden");
+
+ // Switch to tab 1 and open the Web Console context menu from there.
+ gBrowser.selectedTab = tabs[runCount*2];
+ waitForFocus(function() {
+ // Find the relevant elements in the Web Console of tab 1.
+ let win1 = tabs[runCount*2].linkedBrowser.contentWindow;
+ let hudId1 = HUDService.getHudIdByWindow(win1);
+ huds[0] = HUDService.hudReferences[hudId1];
+
+ info("iframe1 root height " + huds[0].ui.rootElement.clientHeight);
+
+ menuitems[0] = huds[0].ui.rootElement.querySelector("#saveBodies");
+ menupopups[0] = huds[0].ui.rootElement.querySelector("menupopup");
+
+ menupopups[0].addEventListener("popupshown", onpopupshown1, false);
+ executeSoon(() => menupopups[0].openPopup());
+ }, tabs[runCount*2].linkedBrowser.contentWindow);
+ }, false);
+
+ executeSoon(function() {
+ menupopups[1].hidePopup();
+ });
+}
+
+function onpopupshown1(aEvent)
+{
+ menupopups[0].removeEventListener(aEvent.type, onpopupshown1, false);
+
+ // The menuitem checkbox must not be in sync with the other tabs.
+ isnot(menuitems[0].getAttribute("checked"), "true",
+ "menuitems[0] is not checked");
+
+ // Enable body logging for tab 1 as well.
+ huds[0].ui.setSaveRequestAndResponseBodies(true).then(() => {
+ menupopups[0].hidePopup();
+ });
+
+ // Close the menu, and switch back to tab 2.
+ menupopups[0].addEventListener("popuphidden", function _onhidden(aEvent) {
+ menupopups[0].removeEventListener(aEvent.type, _onhidden, false);
+
+ info("menupopups[0] hidden");
+
+ gBrowser.selectedTab = tabs[runCount*2 + 1];
+ waitForFocus(function() {
+ // Reopen the context menu from tab 2.
+ huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2c(aEvent));
+ menupopups[1].openPopup();
+ }, tabs[runCount*2 + 1].linkedBrowser.contentWindow);
+ }, false);
+}
+
+function testpopup2c(aEvent) {
+ is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked");
+
+ menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+ menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
+
+ info("menupopups[1] hidden");
+
+ // Done if on second run
+ closeConsole(gBrowser.selectedTab, function() {
+ if (runCount == 0) {
+ runCount++;
+ info("start second run");
+ executeSoon(test);
+ }
+ else {
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = tabs[2];
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = tabs[1];
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = tabs[0];
+ gBrowser.removeCurrentTab();
+ huds = menuitems = menupopups = tabs = null;
+ executeSoon(finishTest);
+ }
+ });
+ }, false);
+
+ executeSoon(function() {
+ menupopups[1].hidePopup();
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js
new file mode 100644
index 000000000..323340f7a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js
@@ -0,0 +1,39 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-603750-websocket.html";
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 603750: Web Socket errors");
+ browser.addEventListener("load", function tabLoad() {
+ browser.removeEventListener("load", tabLoad, true);
+ openConsole(null, (hud) => {
+ content.location = TEST_URI;
+ info("waiting for websocket errors");
+ 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,
+ },
+ ]}).then(finishTest);
+ });
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
new file mode 100644
index 000000000..6eb5177e5
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.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/ */
+
+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>';
+
+function onContentLoaded()
+{
+ browser.removeEventListener("load", onContentLoaded, true);
+
+ let HUD = HUDService.getHudByWindow(content);
+ let jsterm = HUD.jsterm;
+ let outputNode = HUD.outputNode;
+
+ let cssWarning = "Unknown property '-moz-opacity'. Declaration dropped.";
+ let textFound = false;
+ let repeats = 0;
+
+ function displayResults()
+ {
+ ok(textFound, "css warning was found");
+ is(repeats, 2, "The unknown CSS property warning is displayed only once");
+ }
+
+ waitForSuccess({
+ name: "2 repeated CSS warnings",
+ validatorFn: () => {
+ let node = outputNode.querySelector(".webconsole-msg-cssparser");
+ if (!node) {
+ return false;
+ }
+
+ textFound = node.textContent.indexOf(cssWarning) > -1;
+ repeats = node.querySelector(".webconsole-msg-repeat")
+ .getAttribute("value");
+ return textFound && repeats == 2;
+ },
+ successFn: () => {
+ displayResults();
+ testConsoleLogRepeats();
+ },
+ failureFn: () => {
+ displayResults();
+ finishTest();
+ },
+ });
+}
+
+function testConsoleLogRepeats()
+{
+ let HUD = HUDService.getHudByWindow(content);
+ let jsterm = HUD.jsterm;
+ let outputNode = HUD.outputNode;
+
+ 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();
+
+ waitForSuccess({
+ timeout: 10000,
+ name: "10 repeated console.log messages",
+ validatorFn: function()
+ {
+ let node = outputNode.querySelector(".webconsole-msg-console");
+ return node && node.childNodes[3].firstChild.getAttribute("value") == 10;
+ },
+ successFn: finishTest,
+ failureFn: function() {
+ info("output content: " + outputNode.textContent);
+ finishTest();
+ },
+ });
+}
+
+/**
+ * Unit test for bug 611795:
+ * Repeated CSS messages get collapsed into one.
+ */
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(aHud) {
+ // Clear cached messages that are shown once the Web Console opens.
+ aHud.jsterm.clearOutput(true);
+ browser.addEventListener("load", onContentLoaded, true);
+ content.location.reload();
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
new file mode 100644
index 000000000..7e0fe28e1
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
@@ -0,0 +1,52 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html";
+
+let TestObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function test_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic == "console-api-log-event") {
+ executeSoon(performTest);
+ }
+ }
+};
+
+function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ Services.obs.addObserver(TestObserver, "console-api-log-event", false);
+ content.location.reload();
+ });
+}
+
+function performTest() {
+ Services.obs.removeObserver(TestObserver, "console-api-log-event");
+ TestObserver = null;
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarBug613013",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(finishTest);
+}
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", tabLoad, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
new file mode 100644
index 000000000..8748b0aa7
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
@@ -0,0 +1,85 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ */
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613280";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(HUD) {
+ content.console.log("foobarBazBug613280");
+ waitForSuccess({
+ name: "a message is displayed",
+ validatorFn: function()
+ {
+ return HUD.outputNode.itemCount > 0;
+ },
+ successFn: performTest.bind(null, HUD),
+ failureFn: finishTest,
+ });
+ });
+ }, true);
+}
+
+function performTest(HUD) {
+ let input = HUD.jsterm.inputNode;
+ let selection = getSelection();
+ let contentSelection = browser.contentWindow.wrappedJSObject.getSelection();
+
+ let clipboard_setup = function() {
+ goDoCommand("cmd_copy");
+ };
+
+ let clipboard_copy_done = function() {
+ finishTest();
+ };
+
+ // Check if we first need to clear any existing selections.
+ if (selection.rangeCount > 0 || contentSelection.rangeCount > 0 ||
+ input.selectionStart != input.selectionEnd) {
+ if (input.selectionStart != input.selectionEnd) {
+ input.selectionStart = input.selectionEnd = 0;
+ }
+
+ if (selection.rangeCount > 0) {
+ selection.removeAllRanges();
+ }
+
+ if (contentSelection.rangeCount > 0) {
+ contentSelection.removeAllRanges();
+ }
+
+ goUpdateCommand("cmd_copy");
+ }
+
+ let controller = top.document.commandDispatcher.
+ getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ HUD.jsterm.execute("'bug613280: hello world!'");
+
+ HUD.outputNode.selectedIndex = HUD.outputNode.itemCount - 1;
+ HUD.outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+
+ controller = top.document.commandDispatcher.
+ getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ ok(HUD.outputNode.selectedItem, "we have a selected message");
+
+ waitForClipboard(getExpectedClipboardText(HUD.outputNode.selectedItem),
+ clipboard_setup, clipboard_copy_done, clipboard_copy_done);
+}
+
+function getExpectedClipboardText(aItem) {
+ return "[" + WCU_l10n.timestampString(aItem.timestamp) + "] " +
+ aItem.clipboardText;
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
new file mode 100644
index 000000000..b3317a669
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
@@ -0,0 +1,104 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ */
+
+let hud, testDriver;
+
+function testNext() {
+ testDriver.next();
+}
+
+function testGen() {
+ hud.jsterm.clearOutput();
+ let outputNode = hud.outputNode;
+ let scrollBox = outputNode.scrollBoxObject.element;
+
+ for (let i = 0; i < 150; i++) {
+ content.console.log("test message " + i);
+ }
+
+ waitForSuccess({
+ name: "150 console.log messages displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-log").length == 150;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ let oldScrollTop = scrollBox.scrollTop;
+ ok(oldScrollTop > 0, "scroll location is not at the top");
+
+ // scroll to the first node
+ outputNode.focus();
+
+ EventUtils.synthesizeKey("VK_HOME", {});
+
+ let topPosition = scrollBox.scrollTop;
+ isnot(topPosition, oldScrollTop, "scroll location updated (moved to top)");
+
+ // add a message and make sure scroll doesn't change
+ content.console.log("test message 150");
+
+ waitForSuccess({
+ name: "console.log message no. 151 displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-log").length == 151;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ is(scrollBox.scrollTop, topPosition, "scroll location is still at the top");
+
+ // scroll back to the bottom
+ outputNode.lastChild.focus();
+ EventUtils.synthesizeKey("VK_END", {});
+
+ oldScrollTop = outputNode.scrollTop;
+
+ content.console.log("test message 151");
+
+ waitForSuccess({
+ name: "console.log message no. 152 displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-log").length == 152;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ isnot(scrollBox.scrollTop, oldScrollTop,
+ "scroll location updated (moved to bottom)");
+
+ hud = testDriver = null;
+ finishTest();
+
+ yield;
+}
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 613642: remember scroll location");
+ browser.addEventListener("load", function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ testDriver = testGen();
+ testDriver.next();
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
new file mode 100644
index 000000000..5bde49602
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
@@ -0,0 +1,101 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ */
+
+let hud, testDriver;
+
+function testNext() {
+ testDriver.next();
+}
+
+function testGen() {
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+ let oldPref = Services.prefs.getIntPref("devtools.hud.loglimit.console");
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 140);
+ let scrollBoxElement = outputNode.scrollBoxObject.element;
+ let boxObject = outputNode.scrollBoxObject;
+
+ for (let i = 0; i < 150; i++) {
+ content.console.log("test message " + i);
+ }
+
+ waitForSuccess({
+ name: "150 console.log messages displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-log").length == 140;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ let oldScrollTop = scrollBoxElement.scrollTop;
+ ok(oldScrollTop > 0, "scroll location is not at the top");
+
+ let firstNode = outputNode.firstChild;
+ ok(firstNode, "found the first message");
+
+ let msgNode = outputNode.querySelectorAll("richlistitem")[80];
+ ok(msgNode, "found the 80th message");
+
+ // scroll to the middle message node
+ boxObject.ensureElementIsVisible(msgNode);
+
+ isnot(scrollBoxElement.scrollTop, oldScrollTop,
+ "scroll location updated (scrolled to message)");
+
+ oldScrollTop = scrollBoxElement.scrollTop;
+
+ // add a message
+ content.console.log("hello world");
+
+ waitForSuccess({
+ name: "console.log message #151 displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("hello world") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ // 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.setIntPref("devtools.hud.loglimit.console", oldPref);
+
+ hud = testDriver = null;
+ finishTest();
+
+ yield;
+}
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 613642: maintain scroll with pruning of old messages");
+ browser.addEventListener("load", function tabLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoad, true);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ testDriver = testGen();
+ testDriver.next();
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
new file mode 100644
index 000000000..279fc2967
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
@@ -0,0 +1,68 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ */
+
+function consoleOpened(hud) {
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+ let boxObject = outputNode.scrollBoxObject.element;
+
+ for (let i = 0; i < 150; i++) {
+ content.console.log("test message " + i);
+ }
+
+ let oldScrollTop = -1;
+
+ waitForSuccess({
+ name: "console.log messages displayed",
+ validatorFn: function()
+ {
+ return outputNode.itemCount == 150;
+ },
+ successFn: function()
+ {
+ oldScrollTop = boxObject.scrollTop;
+ ok(oldScrollTop > 0, "scroll location is not at the top");
+
+ hud.jsterm.execute("'hello world'");
+
+ waitForSuccess(waitForExecute);
+ },
+ failureFn: finishTest,
+ });
+
+ let waitForExecute = {
+ name: "jsterm output displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ isnot(boxObject.scrollTop, oldScrollTop, "scroll location updated");
+
+ oldScrollTop = boxObject.scrollTop;
+ outputNode.scrollBoxObject.ensureElementIsVisible(outputNode.lastChild);
+
+ is(boxObject.scrollTop, oldScrollTop, "scroll location is the same");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ };
+}
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 614793: jsterm result scroll");
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
new file mode 100644
index 000000000..62085dd50
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.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 network log messages bring up the network panel.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html";
+
+let testEnded = false;
+
+let TestObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function test_observe(aSubject)
+ {
+ if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) {
+ return;
+ }
+
+ is(aSubject.category, "content javascript", "error category");
+
+ testEnded = true;
+ if (aSubject.category == "content javascript") {
+ executeSoon(checkOutput);
+ }
+ else {
+ executeSoon(finishTest);
+ }
+ }
+};
+
+function checkOutput()
+{
+ waitForSuccess({
+ name: "exception message",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("bug618078exception") > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+}
+
+function testEnd()
+{
+ Services.console.unregisterListener(TestObserver);
+}
+
+function test()
+{
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 618078");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ Services.console.registerListener(TestObserver);
+ registerCleanupFunction(testEnd);
+
+ executeSoon(function() {
+ expectUncaughtException();
+ content.location = TEST_URI;
+ });
+ });
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
new file mode 100644
index 000000000..b1bd90ffe
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(hud) {
+ content.location.reload();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(performTest);
+ });
+ }, true);
+}
+
+function performTest() {
+ let hudId = HUDService.getHudIdByWindow(content);
+ let HUD = HUDService.hudReferences[hudId];
+
+ let networkMessage = HUD.outputNode.querySelector(".webconsole-msg-network");
+ ok(networkMessage, "found network message");
+
+ let networkLink = networkMessage.querySelector(".webconsole-msg-link");
+ ok(networkLink, "found network message link");
+
+ let popupset = document.getElementById("mainPopupSet");
+ ok(popupset, "found #mainPopupSet");
+
+ let popupsShown = 0;
+ let hiddenPopups = 0;
+
+ let onpopupshown = function() {
+ document.removeEventListener("popupshown", onpopupshown, false);
+ popupsShown++;
+
+ executeSoon(function() {
+ let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+ is(popups.length, 1, "found one popup");
+
+ document.addEventListener("popuphidden", onpopuphidden, false);
+
+ registerCleanupFunction(function() {
+ is(hiddenPopups, 1, "correct number of popups hidden");
+ if (hiddenPopups != 1) {
+ document.removeEventListener("popuphidden", onpopuphidden, false);
+ }
+ });
+
+ executeSoon(closeConsole);
+ });
+ };
+
+ let onpopuphidden = function() {
+ document.removeEventListener("popuphidden", onpopuphidden, false);
+ hiddenPopups++;
+
+ executeSoon(function() {
+ let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+ is(popups.length, 0, "no popups found");
+
+ executeSoon(finishTest);
+ });
+ };
+
+ document.addEventListener("popupshown", onpopupshown, false);
+
+ registerCleanupFunction(function() {
+ is(popupsShown, 1, "correct number of popups shown");
+ if (popupsShown != 1) {
+ document.removeEventListener("popupshown", onpopupshown, false);
+ }
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow);
+ EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow);
+ EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
new file mode 100644
index 000000000..03b362057
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
@@ -0,0 +1,68 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Sucan <mihai.sucan@gmail.com>
+ */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html";
+
+function test$(HUD) {
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.setInputValue("$(document.body)");
+ HUD.jsterm.execute();
+
+ waitForSuccess({
+ name: "jsterm output for $()",
+ validatorFn: function()
+ {
+ return HUD.outputNode.querySelector(".webconsole-msg-output:last-child");
+ },
+ successFn: function()
+ {
+ let outputItem = HUD.outputNode.
+ querySelector(".webconsole-msg-output:last-child");
+ ok(outputItem.textContent.indexOf("<p>") > -1,
+ "jsterm output is correct for $()");
+
+ test$$(HUD);
+ },
+ failureFn: test$$.bind(null, HUD),
+ });
+}
+
+function test$$(HUD) {
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.setInputValue("$$(document)");
+ HUD.jsterm.execute();
+
+ waitForSuccess({
+ name: "jsterm output for $$()",
+ validatorFn: function()
+ {
+ return HUD.outputNode.querySelector(".webconsole-msg-output:last-child");
+ },
+ successFn: function()
+ {
+ let outputItem = HUD.outputNode.
+ querySelector(".webconsole-msg-output:last-child");
+ ok(outputItem.textContent.indexOf("621644") > -1,
+ "jsterm output is correct for $$()");
+
+ executeSoon(finishTest);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, test$);
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js b/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
new file mode 100644
index 000000000..0aa95af84
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let prefs = {
+ "net": [
+ "network",
+ "networkinfo"
+ ],
+ "css": [
+ "csserror",
+ "cssparser"
+ ],
+ "js": [
+ "exception",
+ "jswarn",
+ "jslog",
+ ],
+ "logging": [
+ "error",
+ "warn",
+ "info",
+ "log"
+ ]
+};
+
+function test() {
+ // Set all prefs to true
+ for (let category in prefs) {
+ prefs[category].forEach(function(pref) {
+ Services.prefs.setBoolPref("devtools.webconsole.filter." + pref, true);
+ });
+ }
+
+ addTab("about:blank");
+ openConsole(null, onConsoleOpen);
+}
+
+function onConsoleOpen(hud) {
+ 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(null, function() {
+ openConsole(null, onConsoleReopen1);
+ });
+}
+
+function onConsoleReopen1(hud) {
+ 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(null, function() {
+ openConsole(null, onConsoleReopen2);
+ });
+}
+
+function onConsoleReopen2(hud) {
+ 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");
+ }
+
+ // Clear prefs
+ for (let category in prefs) {
+ prefs[category].forEach(function(pref) {
+ Services.prefs.clearUserPref("devtools.webconsole.filter." + pref);
+ });
+ }
+
+ prefs = null;
+ finishTest();
+}
+
+function isChecked(aNode) {
+ return aNode.getAttribute("checked") === "true";
+}
+
+function isUnchecked(aNode) {
+ return aNode.getAttribute("checked") === "false";
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js b/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
new file mode 100644
index 000000000..6e93ce523
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
@@ -0,0 +1,32 @@
+/* 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
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Test console for bug 623749");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, runTest);
+ }, true);
+}
+
+function runTest(HUD) {
+ 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, inputNode.value.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");
+
+ executeSoon(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js b/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js
new file mode 100644
index 000000000..5b5193922
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+let itemsSet, HUD, outputNode;
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 626484");
+ browser.addEventListener("load", function tabLoaded(aEvent) {
+ browser.removeEventListener(aEvent.type, tabLoaded, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+
+function consoleOpened(aHud) {
+ HUD = aHud;
+ outputNode = HUD.outputNode;
+ HUD.jsterm.clearOutput();
+
+ let console = content.wrappedJSObject.console;
+ console.log("The first line.");
+ console.log("The second line.");
+ console.log("The last line.");
+ itemsSet = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1],
+ [2, 1, 0]];
+
+ waitForSuccess({
+ name: "console.log messages displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-log").length == 3;
+ },
+ successFn: nextTest,
+ failureFn: finishTest,
+ });
+}
+
+function nextTest() {
+ if (itemsSet.length === 0) {
+ outputNode.clearSelection();
+ HUD.jsterm.clearOutput();
+ HUD = outputNode = null;
+ executeSoon(finishTest);
+ }
+ else {
+ outputNode.clearSelection();
+ let items = itemsSet.shift();
+ items.forEach(function (index) {
+ outputNode.addItemToSelection(outputNode.getItemAtIndex(index));
+ });
+ outputNode.focus();
+ waitForClipboard(getExpectedClipboardText(items.length),
+ clipboardSetup, nextTest, nextTest);
+ }
+}
+
+function getExpectedClipboardText(aItemCount) {
+ let expectedClipboardText = [];
+ for (let i = 0; i < aItemCount; i++) {
+ let item = outputNode.getItemAtIndex(i);
+ expectedClipboardText.push("[" +
+ WCU_l10n.timestampString(item.timestamp) + "] " +
+ item.clipboardText);
+ }
+ return expectedClipboardText.join("\n");
+}
+
+function clipboardSetup() {
+ goDoCommand("cmd_copy");
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
new file mode 100644
index 000000000..73870efab
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
@@ -0,0 +1,129 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Mihai Sucan <mihai.sucan@gmail.com>
+ */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs";
+
+let lastFinishedRequests = {};
+let webConsoleClient;
+
+function requestDoneCallback(aHttpRequest )
+{
+ let status = aHttpRequest.response.status;
+ lastFinishedRequests[status] = aHttpRequest;
+}
+
+function consoleOpened(hud)
+{
+ webConsoleClient = hud.ui.webConsoleClient;
+ hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
+ ok(hud.ui._saveRequestAndResponseBodies,
+ "The saveRequestAndResponseBodies property was successfully set.");
+
+ HUDService.lastFinishedRequestCallback = requestDoneCallback;
+ waitForSuccess(waitForResponses);
+ content.location = TEST_URI;
+ });
+
+ let waitForResponses = {
+ name: "301 and 404 responses",
+ validatorFn: function()
+ {
+ return "301" in lastFinishedRequests &&
+ "404" in lastFinishedRequests;
+ },
+ successFn: getHeaders,
+ failureFn: finishTest,
+ };
+}
+
+function getHeaders()
+{
+ HUDService.lastFinishedRequestCallback = 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 (aResponse) {
+ lastFinishedRequests["301"].response.headers = aResponse.headers;
+
+ webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor,
+ function (aResponse) {
+ lastFinishedRequests["404"].response.headers = aResponse.headers;
+ executeSoon(getContent);
+ });
+ });
+}
+
+function getContent()
+{
+ webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor,
+ function (aResponse) {
+ lastFinishedRequests["301"].response.content = aResponse.content;
+ lastFinishedRequests["301"].discardResponseBody = aResponse.contentDiscarded;
+
+ webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor,
+ function (aResponse) {
+ lastFinishedRequests["404"].response.content = aResponse.content;
+ lastFinishedRequests["404"].discardResponseBody =
+ aResponse.contentDiscarded;
+
+ webConsoleClient = null;
+ executeSoon(performTest);
+ });
+ });
+}
+
+function performTest()
+{
+ function readHeader(aName)
+ {
+ for (let header of headers) {
+ if (header.name == aName) {
+ 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 = null;
+ executeSoon(finishTest);
+}
+
+function test()
+{
+ addTab("data:text/html;charset=utf-8,<p>Web Console test for bug 630733");
+
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
new file mode 100644
index 000000000..322eabd18
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html";
+
+let getterValue = null;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+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");
+
+ finishTest();
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
new file mode 100644
index 000000000..b14470aa7
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(HUD) {
+ let tmp = {};
+ Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp);
+ let WCU = tmp.WebConsoleUtils;
+ let JSPropertyProvider = tmp.JSPropertyProvider;
+ tmp = null;
+
+ let jsterm = HUD.jsterm;
+ let win = content.wrappedJSObject;
+
+ // Make sure autocomplete does not walk through iterators and generators.
+ let result = win.gen1.next();
+ let completion = JSPropertyProvider(win, "gen1.");
+ is(completion, null, "no matches for gen1");
+
+ is(result+1, win.gen1.next(), "gen1.next() did not execute");
+
+ result = win.gen2.next();
+
+ completion = JSPropertyProvider(win, "gen2.");
+ is(completion, null, "no matches for gen2");
+
+ is((result/2+1)*2, win.gen2.next(),
+ "gen2.next() did not execute");
+
+ result = win.iter1.next();
+ is(result[0], "foo", "iter1.next() [0] is correct");
+ is(result[1], "bar", "iter1.next() [1] is correct");
+
+ completion = JSPropertyProvider(win, "iter1.");
+ is(completion, null, "no matches for iter1");
+
+ result = win.iter1.next();
+ is(result[0], "baz", "iter1.next() [0] is correct");
+ is(result[1], "baaz", "iter1.next() [1] is correct");
+
+ completion = JSPropertyProvider(content, "iter2.");
+ is(completion, null, "no matches for iter2");
+
+ completion = JSPropertyProvider(win, "window.");
+ ok(completion, "matches available for window");
+ ok(completion.matches.length, "matches available for window (length)");
+
+ jsterm.clearOutput();
+
+ jsterm.execute("window");
+
+ waitForSuccess({
+ name: "jsterm window object output",
+ validatorFn: function()
+ {
+ return HUD.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD));
+ let node = HUD.outputNode.querySelector(".webconsole-msg-output");
+ EventUtils.synthesizeMouse(node, 2, 2, {}, HUD.iframeWindow);
+ },
+ failureFn: finishTest,
+ });
+}
+
+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/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js
new file mode 100644
index 000000000..e8b20cde7
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js
@@ -0,0 +1,196 @@
+/* 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.
+
+const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html";
+
+const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+let lastRequest = null;
+let requestCallback = null;
+
+function test()
+{
+ const PREF = "devtools.webconsole.persistlog";
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ addTab("data:text/html;charset=utf-8,Web Console network logging tests");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+
+ HUDService.lastFinishedRequestCallback = function(aRequest) {
+ lastRequest = aRequest;
+ if (requestCallback) {
+ requestCallback();
+ }
+ };
+
+ executeSoon(testPageLoad);
+ });
+ }, true);
+}
+
+function testPageLoad()
+{
+ requestCallback = function() {
+ // 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");
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testPageLoadBody);
+ };
+
+ content.location = TEST_NETWORK_REQUEST_URI;
+}
+
+function testPageLoadBody()
+{
+ let loaded = false;
+ let requestCallbackInvoked = false;
+
+ // Turn off logging of request bodies and check again.
+ requestCallback = function() {
+ ok(lastRequest, "Page load was logged again");
+ lastRequest = null;
+ requestCallback = null;
+ requestCallbackInvoked = true;
+
+ if (loaded) {
+ executeSoon(testXhrGet);
+ }
+ };
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ loaded = true;
+
+ if (requestCallbackInvoked) {
+ executeSoon(testXhrGet);
+ }
+ }, true);
+
+ content.location.reload();
+}
+
+function testXhrGet()
+{
+ requestCallback = function() {
+ ok(lastRequest, "testXhrGet() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testXhrPost);
+ };
+
+ // Start the XMLHttpRequest() GET test.
+ content.wrappedJSObject.testXhrGet();
+}
+
+function testXhrPost()
+{
+ requestCallback = function() {
+ ok(lastRequest, "testXhrPost() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testFormSubmission);
+ };
+
+ // Start the XMLHttpRequest() POST test.
+ content.wrappedJSObject.testXhrPost();
+}
+
+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.
+ requestCallback = function() {
+ 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,
+ }],
+ }).then(testLiveFilteringOnSearchStrings);
+ };
+
+ let form = content.document.querySelector("form");
+ ok(form, "we have the HTML form");
+ form.submit();
+}
+
+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'\"");
+
+ HUDService.lastFinishedRequestCallback = null;
+ lastRequest = null;
+ requestCallback = null;
+ finishTest();
+}
+
+function countMessageNodes() {
+ let messageNodes = hud.outputNode.querySelectorAll(".hud-msg-node");
+ 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(aValue)
+{
+ hud.ui.filterBox.value = aValue;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js b/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
new file mode 100644
index 000000000..af54ee7bf
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
@@ -0,0 +1,89 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642108.";
+const LOG_LIMIT = 20;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad(){
+ browser.removeEventListener("load", onLoad, false);
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", LOG_LIMIT);
+
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ });
+
+ openConsole(null, testCSSPruning);
+ }, true);
+}
+
+function populateConsoleRepeats(aHudRef) {
+ for (let i = 0; i < 5; i++) {
+ let node = aHudRef.ui.createMessageNode(CATEGORY_CSS, SEVERITY_WARNING,
+ "css log x");
+ aHudRef.ui.outputMessage(CATEGORY_CSS, node);
+ }
+}
+
+function populateConsole(aHudRef) {
+ for (let i = 0; i < LOG_LIMIT + 5; i++) {
+ let node = aHudRef.ui.createMessageNode(CATEGORY_CSS, SEVERITY_WARNING,
+ "css log " + i);
+ aHudRef.ui.outputMessage(CATEGORY_CSS, node);
+ }
+}
+
+function testCSSPruning(hudRef) {
+ populateConsoleRepeats(hudRef);
+
+ let waitForNoRepeatedNodes = {
+ name: "number of nodes is LOG_LIMIT",
+ validatorFn: function()
+ {
+ return countMessageNodes() == LOG_LIMIT;
+ },
+ successFn: function()
+ {
+ is(Object.keys(hudRef.ui._repeatNodes).length, LOG_LIMIT,
+ "repeated nodes pruned from repeatNodes");
+
+ let msg = hudRef.outputNode.querySelector(".webconsole-msg-cssparser " +
+ ".webconsole-msg-repeat");
+ is(msg.getAttribute("value"), 1,
+ "repeated nodes pruned from repeatNodes (confirmed)");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ };
+
+ waitForSuccess({
+ name: "repeated nodes in cssNodes",
+ validatorFn: function()
+ {
+ let msg = hudRef.outputNode.querySelector(".webconsole-msg-cssparser " +
+ ".webconsole-msg-repeat");
+ return msg && msg.getAttribute("value") == 5;
+ },
+ successFn: function()
+ {
+ populateConsole(hudRef);
+ waitForSuccess(waitForNoRepeatedNodes);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function countMessageNodes() {
+ let outputNode = HUDService.getHudByWindow(content).outputNode;
+ return outputNode.querySelectorAll(".hud-msg-node").length;
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js
new file mode 100644
index 000000000..1db6ab68f
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js
@@ -0,0 +1,102 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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>test for bug 642615";
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+function consoleOpened(HUD) {
+ 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, document);
+ },
+ onClipboardCopy,
+ finishTest);
+ }
+
+ function onClipboardCopy() {
+ updateEditUIVisibility();
+ goDoCommand("cmd_paste");
+
+ waitForSuccess(waitForPaste);
+ }
+
+ let waitForPaste = {
+ name: "no completion value after paste",
+ validatorFn: function()
+ {
+ return !jsterm.completeNode.value;
+ },
+ successFn: onClipboardPaste,
+ failureFn: finishTest,
+ };
+
+ function onClipboardPaste() {
+ goDoCommand("cmd_undo");
+ waitForSuccess({
+ name: "completion value for 'docu' after undo",
+ validatorFn: function()
+ {
+ return !!jsterm.completeNode.value;
+ },
+ successFn: onCompletionValueAfterUndo,
+ failureFn: finishTest,
+ });
+ }
+
+ function onCompletionValueAfterUndo() {
+ is(jsterm.completeNode.value, completionValue,
+ "same completeNode.value after undo");
+
+ EventUtils.synthesizeKey("v", {accelKey: true});
+ waitForSuccess({
+ name: "no completion after ctrl-v (paste)",
+ validatorFn: function()
+ {
+ return !jsterm.completeNode.value;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+ }
+
+ EventUtils.synthesizeKey("u", {});
+
+ waitForSuccess({
+ name: "completion value for 'docu'",
+ validatorFn: function()
+ {
+ return !!jsterm.completeNode.value;
+ },
+ successFn: onCompletionValue,
+ failureFn: finishTest,
+ });
+}
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
new file mode 100644
index 000000000..d72a61611
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -0,0 +1,217 @@
+/* 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 Web Console limits the number of lines displayed according to
+// the limit set for each category.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/" +
+ "webconsole/test/test-bug-644419-log-limits.html";
+
+let hud, outputNode;
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 644419: Console should " +
+ "have user-settable log limits for each message category");
+ browser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+
+ openConsole(null, function(aHud) {
+ aHud.jsterm.clearOutput();
+ hud = aHud;
+ outputNode = aHud.outputNode;
+
+ browser.addEventListener("load", testWebDevLimits, true);
+ expectUncaughtException();
+ content.location = TEST_URI;
+ });
+}
+
+function testWebDevLimits(aEvent) {
+ browser.removeEventListener(aEvent.type, testWebDevLimits, true);
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 10);
+
+ // Find the sentinel entry.
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ }).then(testWebDevLimits2);
+}
+
+function testWebDevLimits2() {
+ // Fill the log with Web Developer errors.
+ for (let i = 0; i < 11; i++) {
+ content.console.log("test message " + i);
+ }
+
+ 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");
+ testJsLimits();
+ });
+}
+
+function testJsLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.exception", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing JS limits");
+
+ // Find the sentinel entry.
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing JS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(testJsLimits2);
+}
+
+function testJsLimits2() {
+ // Fill the log with JS errors.
+ let head = content.document.getElementsByTagName("head")[0];
+ for (let i = 0; i < 11; i++) {
+ var script = content.document.createElement("script");
+ script.text = "fubar" + i + ".bogus(6);";
+ expectUncaughtException();
+ head.insertBefore(script, head.firstChild);
+ }
+
+ 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");
+ testNetLimits();
+ });
+}
+
+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.
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing Net limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ // Fill the log with network messages.
+ gCounter = 0;
+ loadImage();
+ });
+}
+
+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;
+ }
+
+ is(gCounter, 11, "loaded 11 files");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-image.png?_fubar=10",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "test-image.png?_fubar=0", "first message is pruned", false, true);
+ findLogEntry("test-image.png?_fubar=1");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing Net limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.network");
+ testCssLimits();
+ });
+}
+
+function testCssLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing CSS limits");
+
+ // Find the sentinel entry.
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing CSS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(testCssLimits2);
+}
+
+function testCssLimits2() {
+ // Fill the log with CSS errors.
+ let body = content.document.getElementsByTagName("body")[0];
+ for (let i = 0; i < 11; i++) {
+ var div = content.document.createElement("div");
+ div.setAttribute("style", "-moz-foobar" + i + ": 42;");
+ body.insertBefore(div, body.firstChild);
+ }
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "-moz-foobar10",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "Unknown property '-moz-foobar0'",
+ "first message is pruned", false, true);
+ findLogEntry("Unknown property '-moz-foobar1'");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing CSS limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ finishTest();
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
new file mode 100644
index 000000000..aeca2751a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
@@ -0,0 +1,56 @@
+/* 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 console logging methods display the method location along with
+// the output in the console.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/" +
+ "webconsole/test/" +
+ "test-bug-646025-console-file-location.html";
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console file location display test");
+ browser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function(hud) {
+ content.location = TEST_URI;
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "message for level log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 5 },
+ },
+ {
+ text: "message for level info",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_INFO,
+ source: { url: "test-file-location.js", line: 6 },
+ },
+ {
+ text: "message for level warn",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_WARNING,
+ source: { url: "test-file-location.js", line: 7 },
+ },
+ {
+ text: "message for level error",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ source: { url: "test-file-location.js", line: 8 },
+ },
+ {
+ text: "message for level debug",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 9 },
+ }],
+ }).then(finishTest);
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
new file mode 100644
index 000000000..6b2152637
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -0,0 +1,106 @@
+/* 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 document.body autocompletes in the web console.
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console autocompletion bug in document.body");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+let gHUD;
+
+function consoleOpened(aHud) {
+ gHUD = aHud;
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let completeNode = jsterm.completeNode;
+
+ let tmp = {};
+ Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp);
+ let WCU = tmp.WebConsoleUtils;
+ tmp = null;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup._panel.addEventListener("popupshown", function onShown() {
+ popup._panel.removeEventListener("popupshown", onShown, false);
+
+ ok(popup.isOpen, "popup is open");
+
+ // expected properties:
+ // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__
+ // constructor hasOwnProperty isPrototypeOf propertyIsEnumerable
+ // toLocaleString toSource toString unwatch valueOf watch.
+ ok(popup.itemCount >= 14, "popup.itemCount is correct");
+
+ popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false);
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ }, false);
+
+ jsterm.setInputValue("document.body");
+ EventUtils.synthesizeKey(".", {});
+}
+
+function autocompletePopupHidden()
+{
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let completeNode = jsterm.completeNode;
+ let inputNode = jsterm.inputNode;
+
+ popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false);
+
+ ok(!popup.isOpen, "popup is not open");
+ let inputStr = "document.b";
+ jsterm.setInputValue(inputStr);
+ EventUtils.synthesizeKey("o", {});
+ let testStr = inputStr.replace(/./g, " ") + " ";
+
+ waitForSuccess({
+ name: "autocomplete shows document.body",
+ validatorFn: function()
+ {
+ return completeNode.value == testStr + "dy";
+ },
+ successFn: testPropertyPanel,
+ failureFn: finishTest,
+ });
+}
+
+function testPropertyPanel()
+{
+ let jsterm = gHUD.jsterm;
+ jsterm.clearOutput();
+ jsterm.execute("document");
+
+ waitForSuccess({
+ name: "jsterm document object output",
+ validatorFn: function()
+ {
+ return gHUD.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ jsterm.once("variablesview-fetched", onVariablesViewReady);
+ let node = gHUD.outputNode.querySelector(".webconsole-msg-output");
+ EventUtils.synthesizeMouse(node, 2, 2, {}, gHUD.iframeWindow);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function onVariablesViewReady(aEvent, aView)
+{
+ findVariableViewProperties(aView, [
+ { name: "body", value: "[object HTMLBodyElement]" },
+ ], { webconsole: gHUD }).then(finishTest);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
new file mode 100644
index 000000000..c95163e6b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+// Tests that the $0 console helper works as intended.
+
+function createDocument()
+{
+ let doc = content.document;
+ let div = doc.createElement("div");
+ let 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()
+{
+ let h1 = content.document.querySelector("h1");
+ ok(h1, "we have the header node");
+
+ openInspector(runSelectionTests);
+}
+
+function runSelectionTests(aInspector)
+{
+ aInspector.highlighter.unlockAndFocus();
+ aInspector.highlighter.outline.setAttribute("disable-transitions", "true");
+
+ executeSoon(function() {
+ aInspector.selection.once("new-node", performTestComparisons);
+ let h1 = content.document.querySelector("h1");
+ EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content);
+ });
+}
+
+function performTestComparisons()
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let inspector = gDevTools.getToolbox(target).getPanel("inspector");
+ inspector.highlighter.lock();
+
+ let isHighlighting =
+ !(inspector.highlighter.outline.getAttribute("hidden") == "true");
+
+ ok(isHighlighting, "inspector is highlighting");
+
+ let h1 = content.document.querySelector("h1");
+ is(inspector.selection.node, h1, "selection matches node");
+
+ openConsole(gBrowser.selectedTab, performWebConsoleTests);
+}
+
+function performWebConsoleTests(hud)
+{
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let jsterm = hud.jsterm;
+ outputNode = hud.outputNode;
+
+ jsterm.clearOutput();
+ jsterm.execute("$0");
+
+ waitForSuccess({
+ name: "$0 output",
+ validatorFn: function()
+ {
+ return outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ let node = outputNode.querySelector(".webconsole-msg-output");
+ isnot(node.textContent.indexOf("[object HTMLHeadingElement"), -1,
+ "correct output for $0");
+
+ jsterm.clearOutput();
+ jsterm.execute("$0.textContent = 'bug653531'");
+ waitForSuccess(waitForNodeUpdate);
+ },
+ failureFn: finishUp,
+ });
+
+ let waitForNodeUpdate = {
+ name: "$0.textContent update",
+ validatorFn: function()
+ {
+ return outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ let node = outputNode.querySelector(".webconsole-msg-output");
+ isnot(node.textContent.indexOf("bug653531"), -1,
+ "correct output for $0.textContent");
+ let inspector = gDevTools.getToolbox(target).getPanel("inspector");
+ is(inspector.selection.node.textContent, "bug653531",
+ "node successfully updated");
+
+ executeSoon(finishUp);
+ },
+ failureFn: finishUp,
+ };
+}
+
+function finishUp() {
+ finishTest();
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ waitForFocus(createDocument, content);
+ }, true);
+
+ content.location = "data:text/html;charset=utf-8,test for highlighter helper in web console";
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
new file mode 100644
index 000000000..fac1f3896
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -0,0 +1,103 @@
+/* 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 Console API implements the time() and timeEnd() methods.
+
+function test() {
+ addTab("http://example.com/browser/browser/devtools/webconsole/" +
+ "test/test-bug-658368-time-methods.html");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud) {
+ outputNode = hud.outputNode;
+
+ waitForSuccess({
+ name: "aTimer started",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("aTimer: timer started") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("ms");
+ // The next test makes sure that timers with the same name but in separate
+ // tabs, do not contain the same value.
+ addTab("data:text/html;charset=utf-8,<script type='text/javascript'>" +
+ "console.timeEnd('bTimer');</script>");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testTimerIndependenceInTabs);
+ }, true);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function testTimerIndependenceInTabs(hud) {
+ outputNode = hud.outputNode;
+
+ executeSoon(function() {
+ testLogEntry(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.
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ executeSoon(testTimerIndependenceInSameTab);
+ }, true);
+ content.location = "data:text/html;charset=utf-8,<script type='text/javascript'>" +
+ "console.time('bTimer');</script>";
+ });
+}
+
+function testTimerIndependenceInSameTab() {
+ let hudId = HUDService.getHudIdByWindow(content);
+ let hud = HUDService.hudReferences[hudId];
+ outputNode = hud.outputNode;
+
+ waitForSuccess({
+ name: "bTimer started",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bTimer: timer started") > -1;
+ },
+ successFn: function() {
+ hud.jsterm.clearOutput();
+
+ // Now the following console.timeEnd() call shouldn't display anything,
+ // if the timers in different pages are not related.
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ executeSoon(testTimerIndependenceInSameTabAgain);
+ }, true);
+ content.location = "data:text/html;charset=utf-8," +
+ "<script type='text/javascript'>" +
+ "console.timeEnd('bTimer');</script>";
+ },
+ failureFn: finishTest,
+ });
+}
+
+function testTimerIndependenceInSameTabAgain() {
+ let hudId = HUDService.getHudIdByWindow(content);
+ let hud = HUDService.hudReferences[hudId];
+ outputNode = hud.outputNode;
+
+ executeSoon(function() {
+ testLogEntry(outputNode, "bTimer: timer started", "bTimer was not started",
+ false, true);
+
+ closeConsole(gBrowser.selectedTab, function() {
+ gBrowser.removeCurrentTab();
+ executeSoon(finishTest);
+ });
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
new file mode 100644
index 000000000..50fc7ce77
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -0,0 +1,29 @@
+/* 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 console.dir works as intended.
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 659907: Expand console " +
+ "object with a dir method");
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud) {
+ hud.jsterm.execute("console.dir(document)");
+ hud.jsterm.once("variablesview-fetched", testConsoleDir.bind(null, hud));
+}
+
+function testConsoleDir(hud, ev, view) {
+ findVariableViewProperties(view, [
+ { name: "__proto__.__proto__.querySelectorAll", value: "[object Function]" },
+ { name: "location", value: "[object Location]" },
+ { name: "__proto__.write", value: "[object Function]" },
+ ], { webconsole: hud }).then(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js b/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js
new file mode 100644
index 000000000..ade0c0936
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js
@@ -0,0 +1,48 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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>bug 660806 - history navigation must not show the autocomplete popup";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(HUD)
+{
+ content.wrappedJSObject.foobarBug660806 = {
+ "location": "value0",
+ "locationbar": "value1",
+ };
+
+ let jsterm = HUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let onShown = function() {
+ ok(false, "popup shown");
+ };
+
+ popup._panel.addEventListener("popupshown", onShown, false);
+
+ 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._panel.removeEventListener("popupshown", onShown, false);
+ executeSoon(finishTest);
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js b/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js
new file mode 100644
index 000000000..7f8d568fc
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js
@@ -0,0 +1,133 @@
+/* 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 console.group/groupEnd works as intended.
+const GROUP_INDENT = 12;
+
+let testDriver, hud;
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 664131: Expand console " +
+ "object with group methods");
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ testDriver = testGen();
+ testNext();
+ });
+ }, true);
+}
+
+function testNext() {
+ testDriver.next();
+}
+
+function testGen() {
+ outputNode = hud.outputNode;
+
+ hud.jsterm.clearOutput();
+
+ content.console.group("bug664131a");
+
+ waitForSuccess({
+ name: "console.group displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug664131a") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+ is(msg.length, 1, "one message node displayed");
+ is(msg[0].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
+
+ content.console.log("bug664131a-inside");
+
+ waitForSuccess({
+ name: "console.log message displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug664131a-inside") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+ is(msg.length, 2, "two message nodes displayed");
+ is(msg[1].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
+
+ content.console.groupEnd("bug664131a");
+ content.console.log("bug664131-outside");
+
+ waitForSuccess({
+ name: "console.log message displayed after groupEnd()",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug664131-outside") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+ is(msg.length, 3, "three message nodes displayed");
+ is(msg[2].style.marginLeft, "0px", "correct group indent found");
+
+ content.console.groupCollapsed("bug664131b");
+
+ waitForSuccess({
+ name: "console.groupCollapsed displayed",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug664131b") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+ is(msg.length, 4, "four message nodes displayed");
+ is(msg[3].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
+
+
+ // Test that clearing the console removes the indentation.
+ hud.jsterm.clearOutput();
+ content.console.log("bug664131-cleared");
+
+ waitForSuccess({
+ name: "console.log displayed after clearOutput",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bug664131-cleared") > -1;
+ },
+ successFn: testNext,
+ failureFn: finishTest,
+ });
+
+ yield;
+
+ msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+ is(msg.length, 1, "one message node displayed");
+ is(msg[0].style.marginLeft, "0px", "correct group indent found");
+
+ testDriver = hud = null;
+ finishTest();
+
+ yield;
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js b/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js
new file mode 100644
index 000000000..c85c52d13
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js
@@ -0,0 +1,42 @@
+/* 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 for bug 704295
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testCompletion);
+ }, true);
+}
+
+function testCompletion(hud) {
+ var jsterm = hud.jsterm;
+ var 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_ENTER", {});
+ 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_ENTER", {});
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ jsterm = input = null;
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js b/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
new file mode 100644
index 000000000..6433e0f71
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/browser/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testInputChange);
+ }, true);
+}
+
+function testInputChange(hud) {
+ var jsterm = hud.jsterm;
+ var 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");
+
+ jsterm = input = null;
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
new file mode 100644
index 000000000..14c9714f3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
@@ -0,0 +1,96 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Tanvi Vyas <tanvi@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console Mixed Content messages are displayed
+
+const TEST_HTTPS_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html";
+
+var origBlockDisplay;
+var origBlockActive;
+
+function test() {
+ addTab("data:text/html;charset=utf8,Web Console mixed content test");
+ browser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener("load", onLoad, true);
+ origBlockDisplay = Services.prefs.getBoolPref("security.mixed_content.block_display_content");
+ origBlockActive = Services.prefs.getBoolPref("security.mixed_content.block_active_content")
+ Services.prefs.setBoolPref("security.mixed_content.block_display_content", false);
+ Services.prefs.setBoolPref("security.mixed_content.block_active_content", false);
+ openConsole(null, testMixedContent);
+}
+
+function testMixedContent(hud) {
+ content.location = TEST_HTTPS_URI;
+ var aOutputNode = hud.outputNode;
+
+ waitForSuccess(
+ {
+ name: "mixed content warning displayed successfully",
+ timeout: 20000,
+ validatorFn: function() {
+ return ( aOutputNode.querySelector(".webconsole-mixed-content") );
+ },
+
+ successFn: function() {
+
+ //tests on the urlnode
+ let node = aOutputNode.querySelector(".webconsole-mixed-content");
+ ok(testSeverity(node), "Severity type is SEVERITY_WARNING.");
+
+ //tests on the warningNode
+ let warningNode = aOutputNode.querySelector(".webconsole-mixed-content-link");
+ is(warningNode.value, "[Mixed Content]", "Message text is accurate." );
+ testClickOpenNewTab(warningNode);
+
+ endTest();
+ },
+
+ failureFn: endTest,
+ }
+ );
+
+}
+
+function testSeverity(node) {
+ let linkNode = node.parentNode;
+ let msgNode = linkNode.parentNode;
+ let bodyNode = msgNode.parentNode;
+ let finalNode = bodyNode.parentNode;
+
+ return finalNode.classList.contains("webconsole-msg-warn");
+}
+
+function testClickOpenNewTab(warningNode) {
+ /* Invoke the click event and check if a new tab would open to the correct page */
+ let linkOpened = false;
+ let oldOpenUILinkIn = window.openUILinkIn;
+
+ window.openUILinkIn = function(aLink) {
+ if (aLink == "https://developer.mozilla.org/en/Security/MixedContent") {
+ linkOpened = true;
+ }
+ }
+
+ EventUtils.synthesizeMouse(warningNode, 2, 2, {},
+ warningNode.ownerDocument.defaultView);
+
+ ok(linkOpened, "Clicking the Mixed Content Warning node opens the desired page");
+
+ window.openUILinkIn = oldOpenUILinkIn;
+}
+
+function endTest() {
+ Services.prefs.setBoolPref("security.mixed_content.block_display_content", origBlockDisplay);
+ Services.prefs.setBoolPref("security.mixed_content.block_active_content", origBlockActive);
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js b/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
new file mode 100644
index 000000000..a87066dd6
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.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/. */
+
+// This is a test for the Open URL context menu item
+// that is shown for network requests
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"
+const COMMAND_NAME = "consoleCmd_openURL";
+const CONTEXT_MENU_ID = "#menu_openURL";
+
+let HUD = null, outputNode = null, contextMenu = null;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ HUD = aHud;
+ outputNode = aHud.outputNode;
+ contextMenu = HUD.iframeWindow.document.getElementById("output-contextmenu");
+
+ registerCleanupFunction(() => {
+ HUD = outputNode = contextMenu = null;
+ });
+
+ HUD.jsterm.clearOutput();
+
+ content.console.log("bug 764572");
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug 764572",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onConsoleMessage);
+}
+
+function onConsoleMessage(aResults) {
+ outputNode.focus();
+ outputNode.selectedItem = [...aResults[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.");
+
+ waitForContextMenu(contextMenu, outputNode.selectedItem, () => {
+ let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " should be hidden.");
+ }, testOnNetActivity);
+}
+
+function testOnNetActivity() {
+ HUD.jsterm.clearOutput();
+
+ // Reload the url to show net activity in console.
+ content.location.reload();
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(onNetworkMessage);
+}
+
+function onNetworkMessage(aResults) {
+ outputNode.focus();
+ outputNode.selectedItem = [...aResults[0].matched][0];
+
+ let currentTab = gBrowser.selectedTab;
+ let newTab = null;
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true);
+ newTab = aEvent.target;
+ newTab.linkedBrowser.addEventListener("load", onTabLoaded, true);
+ }, true);
+
+ function onTabLoaded() {
+ newTab.linkedBrowser.removeEventListener("load", onTabLoaded, true);
+ gBrowser.removeTab(newTab);
+ gBrowser.selectedTab = currentTab;
+ executeSoon(testOnNetActivity_contextmenu);
+ }
+
+ // 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);
+}
+
+function testOnNetActivity_contextmenu() {
+ let target = outputNode.querySelector(".webconsole-msg-network");
+
+ outputNode.focus();
+ outputNode.selectedItem = target;
+
+ waitForContextMenu(contextMenu, target, () => {
+ let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isShown, CONTEXT_MENU_ID + " should be shown.");
+ }, finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js b/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
new file mode 100644
index 000000000..b60fb70bf
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
@@ -0,0 +1,113 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" +
+ "/test-bug-766001-js-console-links.html";
+
+let nodes, dbg, toolbox, target, index = 0, src, line;
+
+function test()
+{
+ expectUncaughtException();
+ requestLongerTimeout(2);
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testViewSource);
+ }, true);
+}
+
+function testViewSource(aHud)
+{
+ registerCleanupFunction(function() {
+ nodes = dbg = toolbox = target = index = src = line = null;
+ });
+
+ let JSSelector = ".webconsole-msg-exception .webconsole-location";
+ let consoleSelector = ".webconsole-msg-console .webconsole-location";
+
+ waitForSuccess({
+ name: "find the location node",
+ validatorFn: function()
+ {
+ return aHud.outputNode.querySelector(JSSelector) &&
+ aHud.outputNode.querySelector(consoleSelector);
+ },
+ successFn: function()
+ {
+ nodes = [aHud.outputNode.querySelector(JSSelector),
+ aHud.outputNode.querySelector(consoleSelector)];
+
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ toolbox = gDevTools.getToolbox(target);
+ toolbox.once("jsdebugger-selected", checkLineAndClickNext);
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[index%2]);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function checkLineAndClickNext(aEvent, aPanel)
+{
+ if (index == 3) {
+ finishTest();
+ return;
+ }
+ info(aEvent + " event fired for index " + index);
+
+ dbg = aPanel;
+
+ src = nodes[index%2].getAttribute("title");
+ ok(src, "source url found for index " + index);
+ line = nodes[index%2].sourceLine;
+ ok(line, "found source line for index " + index);
+
+ info("Waiting for the correct script to be selected for index " + index);
+ dbg.panelWin.addEventListener("Debugger:SourceShown", onSource, false);
+}
+
+function onSource(aEvent) {
+ if (aEvent.detail.url != src) {
+ return;
+ }
+ dbg.panelWin.removeEventListener("Debugger:SourceShown", onSource, false);
+
+ ok(true, "Correct script is selected for index " + index);
+
+ checkCorrectLine(function() {
+ gDevTools.showToolbox(target, "webconsole").then(function() {
+ index++;
+ info("webconsole selected for index " + index);
+
+ toolbox.once("jsdebugger-selected", checkLineAndClickNext);
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[index%2]);
+ });
+ });
+}
+
+function checkCorrectLine(aCallback)
+{
+ waitForSuccess({
+ name: "correct source and line test for debugger for index " + index,
+ validatorFn: function()
+ {
+ let debuggerView = dbg.panelWin.DebuggerView;
+ if (debuggerView.editor &&
+ debuggerView.editor.getCaretPosition().line == line - 1) {
+ return true;
+ }
+ return false;
+ },
+ successFn: function()
+ {
+ aCallback && executeSoon(aCallback);
+ },
+ failureFn: finishTest,
+ timeout: 10000,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_770099_bad_policyuri.js b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_bad_policyuri.js
new file mode 100644
index 000000000..cd9c0fcc8
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_bad_policyuri.js
@@ -0,0 +1,55 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console CSP messages are displayed
+
+const TEST_BAD_POLICY_URI = "https://example.com/browser/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html";
+
+let hud = undefined;
+
+function test() {
+ addTab("data:text/html;charset=utf8,Web Console CSP bad policy URI test");
+ browser.addEventListener("load", function _onLoad() {
+ browser.removeEventListener("load", _onLoad, true);
+ openConsole(null, loadDocument);
+ }, true);
+}
+
+function loadDocument(theHud) {
+ hud = theHud;
+ hud.jsterm.clearOutput();
+ browser.addEventListener("load", onLoad, true);
+ content.location = TEST_BAD_POLICY_URI;
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener("load", onLoad, true);
+ testPolicyURIMessage();
+}
+
+function testPolicyURIMessage() {
+ let aOutputNode = hud.outputNode;
+
+ waitForSuccess(
+ {
+ name: "CSP policy URI warning displayed successfully",
+ validatorFn: function() {
+ return aOutputNode.querySelector(".webconsole-msg-error");
+ },
+
+ successFn: function() {
+ //tests on the urlnode
+ let node = aOutputNode.querySelector(".webconsole-msg-error");
+ isnot(node.textContent.indexOf("can't fetch policy"), -1,
+ "CSP Policy URI message found");
+ finishTest();
+ },
+
+ failureFn: finishTest,
+ }
+ );
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js
new file mode 100644
index 000000000..893401be6
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js
@@ -0,0 +1,46 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that the Web Console CSP messages are displayed
+
+const TEST_VIOLATION = "https://example.com/browser/browser/devtools/webconsole/test/test_bug_770099_violation.html";
+const CSP_VIOLATION_MSG = "Content Security Policy: Directive default-src https://example.com:443 violated by http://some.example.com/test.png"
+
+let hud = undefined;
+
+function test() {
+ addTab("data:text/html;charset=utf8,Web Console CSP violation test");
+ browser.addEventListener("load", function _onLoad() {
+ browser.removeEventListener("load", _onLoad, true);
+ openConsole(null, loadDocument);
+ }, true);
+}
+
+function loadDocument(theHud){
+ hud = theHud;
+ hud.jsterm.clearOutput()
+ browser.addEventListener("load", onLoad, true);
+ content.location = TEST_VIOLATION;
+}
+
+function onLoad(aEvent) {
+ browser.removeEventListener("load", onLoad, true);
+ testViolationMessage();
+}
+
+function testViolationMessage(){
+ let aOutputNode = hud.outputNode;
+
+ waitForSuccess({
+ name: "CSP policy URI warning displayed successfully",
+ validatorFn: function() {
+ return hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
new file mode 100644
index 000000000..a8e7ed87a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -0,0 +1,150 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" +
+ "/test-bug-782653-css-errors.html";
+
+let nodes, hud, StyleEditorUI;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testViewSource);
+ }, true);
+}
+
+function testViewSource(aHud)
+{
+ hud = aHud;
+
+ registerCleanupFunction(function() {
+ nodes = hud = StyleEditorUI = null;
+ });
+
+ let selector = ".webconsole-msg-cssparser .webconsole-location";
+
+ waitForSuccess({
+ name: "find the location node",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(selector);
+ },
+ successFn: function()
+ {
+ nodes = hud.outputNode.querySelectorAll(selector);
+ is(nodes.length, 2, "correct number of css messages");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.once("styleeditor-selected", (event, panel) => {
+ StyleEditorUI = panel.UI;
+
+ let count = 0;
+ StyleEditorUI.on("editor-added", function() {
+ if (++count == 2) {
+ onStyleEditorReady(panel);
+ }
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[0]);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function onStyleEditorReady(aPanel)
+{
+ let win = aPanel.panelWindow;
+ ok(win, "Style Editor Window is defined");
+ ok(StyleEditorUI, "Style Editor UI is defined");
+
+ waitForFocus(function() {
+ info("style editor window focused");
+
+ let href = nodes[0].getAttribute("title");
+ let line = nodes[0].sourceLine;
+ ok(line, "found source line");
+
+ checkStyleEditorForSheetAndLine(href, line - 1, function() {
+ info("first check done");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ let href = nodes[1].getAttribute("title");
+ let line = nodes[1].sourceLine;
+ ok(line, "found source line");
+
+ toolbox.selectTool("webconsole").then(function() {
+ info("webconsole selected");
+
+ toolbox.once("styleeditor-selected", function(aEvent) {
+ info(aEvent + " event fired");
+
+ checkStyleEditorForSheetAndLine(href, line - 1, function() {
+ info("second check done");
+ finishTest();
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[1]);
+ });
+ });
+ }, win);
+}
+
+function checkStyleEditorForSheetAndLine(aHref, aLine, aCallback)
+{
+ let foundEditor = null;
+ waitForSuccess({
+ name: "style editor for stylesheet",
+ validatorFn: function()
+ {
+ for (let editor of StyleEditorUI.editors) {
+ if (editor.styleSheet.href == aHref) {
+ foundEditor = editor;
+ return true;
+ }
+ }
+ return false;
+ },
+ successFn: function()
+ {
+ performLineCheck(foundEditor, aLine, aCallback);
+ },
+ failureFn: finishTest,
+ });
+}
+
+function performLineCheck(aEditor, aLine, aCallback)
+{
+ function checkForCorrectState()
+ {
+ is(aEditor.sourceEditor.getCaretPosition().line, aLine,
+ "correct line is selected");
+ is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex,
+ "correct stylesheet is selected in the editor");
+
+ aCallback && executeSoon(aCallback);
+ }
+
+ waitForSuccess({
+ name: "source editor load",
+ validatorFn: function()
+ {
+ return aEditor.sourceEditor;
+ },
+ successFn: checkForCorrectState,
+ failureFn: function() {
+ info("selectedStyleSheetIndex " + StyleEditorUI.selectedStyleSheetIndex
+ + " expected " + aEditor.styleSheet.styleSheetIndex);
+ finishTest();
+ },
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js b/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
new file mode 100644
index 000000000..1efd9c141
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
@@ -0,0 +1,214 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * zmgmoz <zmgmoz@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845
+
+let jsterm, inputNode;
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 804845 and bug 619598");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, doTests);
+ }, true);
+}
+
+function doTests(HUD) {
+ jsterm = HUD.jsterm;
+ inputNode = jsterm.inputNode;
+ ok(!jsterm.inputNode.value, "inputNode.value is empty");
+ is(jsterm.inputNode.selectionStart, 0);
+ is(jsterm.inputNode.selectionEnd, 0);
+
+ testSingleLineInputNavNoHistory();
+ testMultiLineInputNavNoHistory();
+ testNavWithHistory();
+
+ jsterm = inputNode = null;
+ executeSoon(finishTest);
+}
+
+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(inputNode.value + lineValues[i]);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+ }
+ let inputValue = inputNode.value;
+ 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(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat,
+ "up arrow from end of multiline");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.value.slice(inputNode.selectionStart), "",
+ "down arrow from within multiline");
+
+ // navigate up through input lines
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.value.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(inputNode.value.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(inputNode.value, 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(inputNode.value.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(inputNode.value.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(inputNode.value.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 i = 0; i < match.length; i++) {
+ 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(inputNode.value, values[i-1],
+ "ctrl-p updates inputNode from backwards history values[" + i-1 + "]");
+ }
+ let inputValue = inputNode.value;
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "ctrl-p at beginning of history moves caret location to beginning of line");
+ is(inputNode.value, 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(inputNode.value, 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(!inputNode.value, "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(inputNode.value, inputValue,
+ "ctrl-p from end of multi-line does not trigger history");
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.value, values[values.length-1],
+ "ctrl-p from start of multi-line triggers history");
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js b/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
new file mode 100644
index 000000000..2f075a3a4
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
@@ -0,0 +1,61 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * zmgmoz <zmgmoz@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// 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
+
+function test() {
+ addTab("data:text/html;charset=utf-8,Web Console test for bug 817834");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testEditedInputHistory);
+ }, true);
+}
+
+function testEditedInputHistory(HUD) {
+ let jsterm = HUD.jsterm;
+ let inputNode = jsterm.inputNode;
+ ok(!inputNode.value, "inputNode.value is empty");
+ is(inputNode.selectionStart, 0);
+ is(inputNode.selectionEnd, 0);
+
+ jsterm.setInputValue('"first item"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(inputNode.value, '"first item"', "null test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.value, '"first item"', "null test history down");
+
+ jsterm.execute();
+ is(inputNode.value, "", "cleared input line after submit");
+
+ jsterm.setInputValue('"editing input 1"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(inputNode.value, '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.value, '"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(inputNode.value, '"second item"', "test history up");
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(inputNode.value, '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.value, '"second item"', "test history down");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.value, '"editing input 2"',
+ "test history down restores new in-progress input again");
+
+ executeSoon(finishTest);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_821877_csp_errors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_821877_csp_errors.js
new file mode 100644
index 000000000..dae8f4d8b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_821877_csp_errors.js
@@ -0,0 +1,28 @@
+// Tests that CSP errors from nsDocument::InitCSP are logged to the Web Console
+
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-821877-csperrors.html";
+const CSP_DEPRECATED_HEADER_MSG = "The X-Content-Security-Policy and X-Content-Security-Report-Only headers will be deprecated in the future. Please use the Content-Security-Policy and Content-Security-Report-Only headers with CSP spec compliant syntax instead.";
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function testCSPErrorLogged (hud) {
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Deprecated CSP header error displayed successfully",
+ text: CSP_DEPRECATED_HEADER_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ ],
+ }).then(finishTest);
+ });
+ }, true);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
new file mode 100644
index 000000000..19705326d
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-837351-security-errors.html";
+
+function run_test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad(aEvent) {
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function testSecurityErrorLogged (hud) {
+ let button = hud.ui.rootElement.querySelector(".webconsole-filter-button[category=\"security\"]");
+ ok(button, "Found security button in the web console");
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Blocked loading mixed active content \"http://example.com/\"",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR
+ },
+ ],
+ }).then(finishTest);
+ });
+ }, true);
+}
+
+function test()
+{
+ SpecialPowers.pushPrefEnv({'set': [["security.mixed_content.block_active_content", true]]}, run_test);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js b/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js
new file mode 100644
index 000000000..babfddabb
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js
@@ -0,0 +1,44 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Jennifer Fong <jfong@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ Services.prefs.setIntPref("devtools.webconsole.fontSize", 10);
+ openConsole(null, testFontSizeChange);
+ }, true);
+}
+
+function testFontSizeChange(hud) {
+ let inputNode = hud.jsterm.inputNode;
+ let outputNode = hud.jsterm.outputNode;
+ outputNode.focus();
+
+ EventUtils.synthesizeKey("-", { accelKey: true });
+ is(inputNode.style.fontSize, "10px", "input font stays at same size with ctrl+-");
+ is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+-");
+
+ EventUtils.synthesizeKey("=", { accelKey: true });
+ is(inputNode.style.fontSize, "11px", "input font increased with ctrl+=");
+ is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+=");
+
+ EventUtils.synthesizeKey("-", { accelKey: true });
+ is(inputNode.style.fontSize, "10px", "font decreased with ctrl+-");
+ is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+-");
+
+ EventUtils.synthesizeKey("0", { accelKey: true });
+ is(inputNode.style.fontSize, "", "font reset with ctrl+0");
+ is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+0");
+
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_chrome.js
new file mode 100644
index 000000000..a04a011a7
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js
@@ -0,0 +1,35 @@
+/* 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 code completion works properly.
+
+function test() {
+ addTab("about:config");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testChrome);
+ }, true);
+}
+
+function testChrome(hud) {
+ 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);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, function() {
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ executeSoon(finishTest);
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_completion.js b/browser/devtools/webconsole/test/browser_webconsole_completion.js
new file mode 100644
index 000000000..fc27c7397
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js
@@ -0,0 +1,120 @@
+/* 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 code completion works properly.
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
+
+let testDriver;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(hud) {
+ testDriver = testCompletion(hud);
+ testDriver.next();
+ });
+ }, true);
+}
+
+function testNext() {
+ executeSoon(function() {
+ testDriver.next();
+ });
+}
+
+function testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ // Test typing 'docu'.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+ yield;
+
+ 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);
+ jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
+ yield;
+
+ 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);
+ jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
+ yield;
+
+ is(input.value, "window.Object", "'window.Ob' tab completion");
+
+ // Test typing 'document.getElem'.
+ input.value = "document.getElem";
+ input.setSelectionRange(16, 16);
+ jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
+ yield;
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, "", "'document.getElem' completion");
+
+ // Test pressing tab another time.
+ jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
+ yield;
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' another tab completion");
+
+ // Test pressing shift_tab.
+ jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext);
+ yield;
+
+ is(input.value, "document.getElem", "'document.getElem' untab completion");
+ is(jsterm.completeNode.value, "", "'document.getElem' completion");
+
+ jsterm.clearOutput();
+
+ input.value = "docu";
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+ yield;
+
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ jsterm.execute();
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ // Test multi-line completion works
+ input.value = "console.log('one');\nconsol";
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+ yield;
+
+ is(jsterm.completeNode.value, " \n e", "multi-line completion");
+
+ // Test non-object autocompletion.
+ input.value = "Object.name.sl";
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+ yield;
+
+ is(jsterm.completeNode.value, " ice", "non-object completion");
+
+ // Test string literal autocompletion.
+ input.value = "'Asimov'.sl";
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+ yield;
+
+ is(jsterm.completeNode.value, " ice", "string literal completion");
+
+ testDriver = jsterm = input = null;
+ executeSoon(finishTest);
+ yield;
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_extras.js b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js
new file mode 100644
index 000000000..4e0fa63d8
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js
@@ -0,0 +1,39 @@
+/* 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 basic console.log()-style APIs and filtering work.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-extras.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(hud) {
+ waitForSuccess({
+ name: "two nodes displayed",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelectorAll(".hud-msg-node").length == 2;
+ },
+ successFn: function()
+ {
+ let nodes = hud.outputNode.querySelectorAll(".hud-msg-node");
+ ok(/start/.test(nodes[0].textContent), "start found");
+ ok(/end/.test(nodes[1].textContent), "end found - complete!");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+
+ let button = content.document.querySelector("button");
+ ok(button, "we have the button");
+ EventUtils.sendMouseEvent({ type: "click" }, button, content);
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
new file mode 100644
index 000000000..4d8a1d492
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
@@ -0,0 +1,146 @@
+/* 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 basic console.log()-style APIs and filtering work.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let testDriver = null;
+let subtestDriver = null;
+
+function test() {
+ addTab(TEST_URI);
+
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ hudId = hud.hudId;
+ outputNode = hud.outputNode;
+ testDriver = testGen();
+ testDriver.next();
+ });
+}
+
+function testGen() {
+ subtestGen("log");
+ yield;
+
+ subtestGen("info");
+ yield;
+
+ subtestGen("warn");
+ yield;
+
+ subtestGen("error");
+ yield;
+
+ subtestGen("debug"); // bug 616742
+ yield;
+
+ testDriver = subtestDriver = null;
+ finishTest();
+
+ yield;
+}
+
+function subtestGen(aMethod) {
+ subtestDriver = testConsoleLoggingAPI(aMethod);
+ subtestDriver.next();
+}
+
+function testConsoleLoggingAPI(aMethod) {
+ let console = content.wrappedJSObject.console;
+
+ hud.jsterm.clearOutput();
+
+ setStringFilter("foo");
+ console[aMethod]("foo-bar-baz");
+ console[aMethod]("bar-baz");
+
+ function nextTest() {
+ subtestDriver.next();
+ }
+
+ waitForSuccess({
+ name: "1 hidden " + aMethod + " node via string filtering",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-filtered-by-string").length == 1;
+ },
+ successFn: nextTest,
+ failureFn: nextTest,
+ });
+
+ yield;
+
+ 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
+ setStringFilter("");
+ hud.setFilterState(aMethod, false);
+ console[aMethod]("foo-bar-baz");
+
+ waitForSuccess({
+ name: "1 message hidden for " + aMethod + " (logging turned off)",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll("description").length == 1;
+ },
+ successFn: nextTest,
+ failureFn: nextTest,
+ });
+
+ yield;
+
+ hud.jsterm.clearOutput();
+ hud.setFilterState(aMethod, true);
+ console[aMethod]("foo-bar-baz");
+
+ waitForSuccess({
+ name: "1 message shown for " + aMethod + " (logging turned on)",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll("description").length == 1;
+ },
+ successFn: nextTest,
+ failureFn: nextTest,
+ });
+
+ yield;
+
+ hud.jsterm.clearOutput();
+ setStringFilter("");
+
+ // test for multiple arguments.
+ console[aMethod]("foo", "bar");
+
+ waitForSuccess({
+ name: "show both console arguments for " + aMethod,
+ validatorFn: function()
+ {
+ let node = outputNode.querySelector(".hud-msg-node");
+ return node && /"foo" "bar"/.test(node.textContent);
+ },
+ successFn: nextTest,
+ failureFn: nextTest,
+ });
+
+ yield;
+ testDriver.next();
+ yield;
+}
+
+function setStringFilter(aValue) {
+ hud.ui.filterBox.value = aValue;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js b/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js
new file mode 100644
index 000000000..85f2ce847
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js
@@ -0,0 +1,67 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 ***** */
+
+// Tests that copying multiple messages inserts newlines in between.
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 586142";
+
+let hud;
+
+function test()
+{
+ addTab(TEST_URI);
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false);
+ openConsole(null, testNewlines);
+}
+
+function testNewlines(aHud) {
+ hud = aHud;
+ hud.jsterm.clearOutput();
+
+ for (let i = 0; i < 20; i++) {
+ content.console.log("Hello world #" + i);
+ }
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Hello world #19",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(testClipboard);
+}
+
+function testClipboard() {
+ let outputNode = hud.outputNode;
+
+ info("messages in output: " + outputNode.itemCount);
+ ok(outputNode.itemCount >= 20, "expected number of messages");
+
+ outputNode.selectAll();
+ outputNode.focus();
+
+ let clipboardTexts = [];
+ for (let i = 0; i < outputNode.itemCount; i++) {
+ let item = outputNode.getItemAtIndex(i);
+ clipboardTexts.push("[" +
+ WCU_l10n.timestampString(item.timestamp) +
+ "] " + item.clipboardText);
+ }
+
+ waitForClipboard(clipboardTexts.join("\n"),
+ function() { goDoCommand("cmd_copy"); },
+ finishTest, finishTest);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
new file mode 100644
index 000000000..23c4cd8f3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
@@ -0,0 +1,45 @@
+/* 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 commands run by the user are executed in content space.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testExecutionScope);
+ }, true);
+}
+
+function testExecutionScope(hud) {
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+ jsterm.execute("window.location.href;");
+
+ waitForSuccess({
+ name: "jsterm execution output (two nodes)",
+ validatorFn: function()
+ {
+ return jsterm.outputNode.querySelectorAll(".hud-msg-node").length == 2;
+ },
+ successFn: function()
+ {
+ let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node");
+
+ is(/window.location.href;/.test(nodes[0].textContent), true,
+ "'window.location.href;' written to output");
+
+ isnot(nodes[1].textContent.indexOf(TEST_URI), -1,
+ "command was executed in the window scope");
+
+ executeSoon(finishTest);
+ },
+ failureFn: finishTest,
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_for_of.js b/browser/devtools/webconsole/test/browser_webconsole_for_of.js
new file mode 100644
index 000000000..fc925535b
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_for_of.js
@@ -0,0 +1,35 @@
+/* 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-for-of.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testForOf);
+ }, true);
+}
+
+function testForOf(hud) {
+ var jsterm = hud.jsterm;
+ jsterm.execute("{ [x.tagName for (x of document.body.childNodes) if (x.nodeType === 1)].join(' '); }");
+
+ waitForSuccess({
+ name: "jsterm output displayed",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ let node = hud.outputNode.querySelector(".webconsole-msg-output");
+ ok(/H1 DIV H2 P/.test(node.textContent),
+ "for-of loop should find all top-level nodes");
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_history.js b/browser/devtools/webconsole/test/browser_webconsole_history.js
new file mode 100644
index 000000000..bb39cd60f
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_history.js
@@ -0,0 +1,66 @@
+/* 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 the console history feature accessed via the up and down arrow keys.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testHistory);
+ }, true);
+}
+
+function testHistory(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ let executeList = ["document", "window", "window.location"];
+
+ for each (var item in executeList) {
+ input.value = item;
+ jsterm.execute();
+ }
+
+ for (var i = executeList.length - 1; i != -1; i--) {
+ jsterm.historyPeruse(HISTORY_BACK);
+ is (input.value, executeList[i], "check history previous idx:" + i);
+ }
+
+ 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 (var 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);
+
+ executeSoon(finishTest);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js b/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js
new file mode 100644
index 000000000..b888b0f5c
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js
@@ -0,0 +1,48 @@
+/* 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 correct CSS styles are applied to the lines of console
+// output.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testJSInputAndOutputStyling);
+ }, true);
+}
+
+function testJSInputAndOutputStyling(hud) {
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+ jsterm.execute("2 + 2");
+
+ waitForSuccess({
+ name: "jsterm output is displayed",
+ validatorFn: function()
+ {
+ return jsterm.outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
+ let jsInputNode = jsterm.outputNode.querySelector(".hud-msg-node");
+ isnot(jsInputNode.textContent.indexOf("2 + 2"), -1,
+ "JS input node contains '2 + 2'");
+ ok(jsInputNode.classList.contains("webconsole-msg-input"),
+ "JS input node is of the CSS class 'webconsole-msg-input'");
+
+ let output = jsterm.outputNode.querySelector(".webconsole-msg-output");
+ isnot(output.textContent.indexOf("4"), -1,
+ "JS output node contains '4'");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js b/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js
new file mode 100644
index 000000000..2dce91293
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js
@@ -0,0 +1,60 @@
+/* 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 input box expands as the user types long lines.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testJSInputExpansion);
+ }, true);
+}
+
+function testJSInputExpansion(hud) {
+ let jsterm = hud.jsterm;
+ let input = 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");
+
+ finishTest();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
new file mode 100644
index 000000000..bf0ea3ba5
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let jsterm, testDriver;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(hud) {
+ testDriver = testJSTerm(hud);
+ testDriver.next();
+ });
+ }, true);
+}
+
+function nextTest() {
+ testDriver.next();
+}
+
+function checkResult(msg, desc, lines) {
+ waitForSuccess({
+ name: "correct number of results shown for " + desc,
+ validatorFn: function()
+ {
+ let nodes = jsterm.outputNode.querySelectorAll(".webconsole-msg-output");
+ return nodes.length == lines;
+ },
+ successFn: function()
+ {
+ let labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output");
+ if (typeof msg == "string") {
+ is(labels[lines-1].textContent.trim(), msg,
+ "correct message shown for " + desc);
+ }
+ else if (typeof msg == "function") {
+ ok(msg(labels), "correct message shown for " + desc);
+ }
+
+ nextTest();
+ },
+ failureFn: nextTest,
+ });
+}
+
+function testJSTerm(hud)
+{
+ jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+ jsterm.execute("'id=' + $('#header').getAttribute('id')");
+ checkResult('"id=header"', "$() worked", 1);
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("headerQuery = $$('h1')");
+ jsterm.execute("'length=' + headerQuery.length");
+ checkResult('"length=1"', "$$() worked", 2);
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("xpathQuery = $x('.//*', document.body);");
+ jsterm.execute("'headerFound=' + (xpathQuery[0] == headerQuery[0])");
+ checkResult('"headerFound=true"', "$x() worked", 2);
+ yield;
+
+ // no jsterm.clearOutput() here as we clear the output using the clear() fn.
+ jsterm.execute("clear()");
+
+ waitForSuccess({
+ name: "clear() worked",
+ validatorFn: function()
+ {
+ return jsterm.outputNode.childNodes.length == 0;
+ },
+ successFn: nextTest,
+ failureFn: nextTest,
+ });
+
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("'keysResult=' + (keys({b:1})[0] == 'b')");
+ checkResult('"keysResult=true"', "keys() worked", 1);
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("'valuesResult=' + (values({b:1})[0] == 1)");
+ checkResult('"valuesResult=true"', "values() worked", 1);
+ yield;
+
+ jsterm.clearOutput();
+
+ let tabs = gBrowser.tabs.length;
+
+ jsterm.execute("help()");
+ let output = jsterm.outputNode.querySelector(".webconsole-msg-output");
+ ok(!output, "help() worked");
+
+ jsterm.execute("help");
+ output = jsterm.outputNode.querySelector(".webconsole-msg-output");
+ ok(!output, "help worked");
+
+ jsterm.execute("?");
+ output = jsterm.outputNode.querySelector(".webconsole-msg-output");
+ ok(!output, "? worked");
+
+ let foundTab = null;
+ waitForSuccess({
+ name: "help tabs opened",
+ validatorFn: function()
+ {
+ let newTabOpen = gBrowser.tabs.length == tabs + 3;
+ if (!newTabOpen) {
+ return false;
+ }
+
+ foundTab = gBrowser.tabs[tabs];
+ return true;
+ },
+ successFn: function()
+ {
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ nextTest();
+ },
+ failureFn: nextTest,
+ });
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("pprint({b:2, a:1})");
+ checkResult('" b: 2\n a: 1"', "pprint()", 1);
+ yield;
+
+ // check instanceof correctness, bug 599940
+ jsterm.clearOutput();
+ jsterm.execute("[] instanceof Array");
+ checkResult("true", "[] instanceof Array == true", 1);
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("({}) instanceof Object");
+ checkResult("true", "({}) instanceof Object == true", 1);
+ yield;
+
+ // check for occurrences of Object XRayWrapper, bug 604430
+ jsterm.clearOutput();
+ jsterm.execute("document");
+ checkResult(function(nodes) {
+ return nodes[0].textContent.search(/\[object xraywrapper/i) == -1;
+ }, "document - no XrayWrapper", 1);
+ yield;
+
+ // check that pprint(window) and keys(window) don't throw, bug 608358
+ jsterm.clearOutput();
+ jsterm.execute("pprint(window)");
+ checkResult(null, "pprint(window)", 1);
+ yield;
+
+ jsterm.clearOutput();
+ jsterm.execute("keys(window)");
+ checkResult(null, "keys(window)", 1);
+ yield;
+
+ // bug 614561
+ jsterm.clearOutput();
+ jsterm.execute("pprint('hi')");
+ checkResult('" 0: "h"\n 1: "i""', "pprint('hi')", 1);
+ yield;
+
+ // check that pprint(function) shows function source, bug 618344
+ jsterm.clearOutput();
+ jsterm.execute("pprint(print)");
+ checkResult(function(nodes) {
+ return nodes[0].textContent.indexOf("aOwner.helperResult") > -1;
+ }, "pprint(function) shows source", 1);
+ yield;
+
+ // check that an evaluated null produces "null", bug 650780
+ jsterm.clearOutput();
+ jsterm.execute("null");
+ checkResult("null", "null is null", 1);
+ yield;
+
+ jsterm = testDriver = null;
+ executeSoon(finishTest);
+ yield;
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
new file mode 100644
index 000000000..eaba9f1e5
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
@@ -0,0 +1,66 @@
+/* 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 message type filter checkboxes work.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let hud;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ hud = aHud;
+ hud.jsterm.clearOutput();
+
+ let console = content.console;
+
+ for (let i = 0; i < 50; i++) {
+ console.log("foobarz #" + i);
+ }
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz #49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(testLiveFilteringOfMessageTypes);
+}
+
+function testLiveFilteringOfMessageTypes() {
+ is(hud.outputNode.itemCount, 50, "number of messages");
+
+ hud.setFilterState("log", false);
+ is(countMessageNodes(), 0, "the log nodes are hidden when the " +
+ "corresponding filter is switched off");
+
+ hud.setFilterState("log", true);
+ is(countMessageNodes(), 50, "the log nodes reappear when the " +
+ "corresponding filter is switched on");
+
+ finishTest();
+}
+
+function countMessageNodes() {
+ let messageNodes = hud.outputNode.querySelectorAll(".hud-log");
+ 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/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
new file mode 100644
index 000000000..e66e66fe3
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
@@ -0,0 +1,106 @@
+/* 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 text filter box works.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+let hud;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ hud = aHud;
+ hud.jsterm.clearOutput();
+ let console = content.console;
+
+ for (let i = 0; i < 50; i++) {
+ console.log("http://www.example.com/ " + i);
+ }
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "http://www.example.com/ 49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(testLiveFilteringOnSearchStrings);
+}
+
+function testLiveFilteringOnSearchStrings() {
+ is(hud.outputNode.itemCount, 50, "number of messages");
+
+ 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(" zzzz zzzz ");
+ is(countMessageNodes(), 0, "the log nodes are hidden when the search " +
+ "string is set to \" zzzz zzzz \"");
+
+ setStringFilter("");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is removed");
+
+ setStringFilter("\u9f2c");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching " +
+ "for weasels");
+
+ setStringFilter("\u0007");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ "the bell character");
+
+ setStringFilter('"foo"');
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ 'the string "foo"');
+
+ 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'\"");
+
+ finishTest();
+}
+
+function countMessageNodes() {
+ let outputNode = hud.outputNode;
+
+ let messageNodes = outputNode.querySelectorAll(".hud-log");
+ 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(aValue)
+{
+ hud.ui.filterBox.value = aValue;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js b/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js
new file mode 100644
index 000000000..cc030f8f5
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js
@@ -0,0 +1,71 @@
+/* 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 console logging via the console API produces nodes of the correct
+// CSS classes.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, consoleOpened);
+ }, true);
+}
+
+function consoleOpened(aHud) {
+ let console = content.console;
+ outputNode = aHud.outputNode;
+
+ ok(console, "console exists");
+ console.log("I am a log message");
+ console.error("I am an error");
+ console.info("I am an info message");
+ console.warn("I am a warning message");
+
+ waitForSuccess({
+ name: "console.warn displayed",
+ validatorFn: function()
+ {
+ return aHud.outputNode.textContent.indexOf("a warning") > -1;
+ },
+ successFn: testLogNodeClasses,
+ failureFn: finishTest,
+ });
+}
+
+function testLogNodeClasses() {
+ let domLogEntries = outputNode.childNodes;
+
+ let count = outputNode.childNodes.length;
+ ok(count > 0, "LogCount: " + count);
+
+ let klasses = ["hud-log",
+ "hud-warn",
+ "hud-info",
+ "hud-error",
+ "hud-exception",
+ "hud-network"];
+
+ function verifyClass(classList) {
+ let len = klasses.length;
+ for (var i = 0; i < len; i++) {
+ if (classList.contains(klasses[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ for (var i = 0; i < count; i++) {
+ let classList = domLogEntries[i].classList;
+ ok(verifyClass(classList),
+ "Log Node class verified: " + domLogEntries[i].getAttribute("class"));
+ }
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js
new file mode 100644
index 000000000..77c560ca8
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false);
+ openConsole(null, function(hud) {
+ content.console.log("a log message");
+
+ waitForSuccess({
+ name: "console.log message shown with an ID attribute",
+ validatorFn: function()
+ {
+ let node = hud.outputNode.querySelector(".hud-msg-node");
+ return node && node.getAttribute("id");
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
new file mode 100644
index 000000000..8bc835fa1
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
@@ -0,0 +1,211 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ * Julian Viereck <jviereck@mozilla.com>
+ * Patrick Walton <pcwalton@mozilla.com>
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Tests that network log messages bring up the network panel.
+
+const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html";
+
+const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+let lastRequest = null;
+let requestCallback = null;
+
+function test()
+{
+ addTab("data:text/html;charset=utf-8,Web Console network logging tests");
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+
+ openConsole(null, function(aHud) {
+ hud = aHud;
+
+ HUDService.lastFinishedRequestCallback = requestCallbackWrapper;
+
+ executeSoon(testPageLoad);
+ });
+ }, true);
+}
+
+function requestCallbackWrapper(aRequest)
+{
+ lastRequest = aRequest;
+
+ hud.ui.webConsoleClient.getResponseContent(lastRequest.actor,
+ function(aResponse) {
+ lastRequest.response.content = aResponse.content;
+ lastRequest.discardResponseBody = aResponse.contentDiscarded;
+
+ hud.ui.webConsoleClient.getRequestPostData(lastRequest.actor,
+ function(aResponse) {
+ lastRequest.request.postData = aResponse.postData;
+ lastRequest.discardRequestBody = aResponse.postDataDiscarded;
+
+ if (requestCallback) {
+ requestCallback();
+ }
+ });
+ });
+}
+
+function testPageLoad()
+{
+ requestCallback = function() {
+ // 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");
+ ok(!lastRequest.request.postData.text, "No request body was stored");
+ ok(lastRequest.discardRequestBody, "Request body was discarded");
+ ok(!lastRequest.response.content.text, "No response body was stored");
+ ok(lastRequest.discardResponseBody, "Response body was discarded");
+
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testPageLoadBody);
+ };
+
+ content.location = TEST_NETWORK_REQUEST_URI;
+}
+
+function testPageLoadBody()
+{
+ // Turn on logging of request bodies and check again.
+ hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
+ ok(hud.ui._saveRequestAndResponseBodies,
+ "The saveRequestAndResponseBodies property was successfully set.");
+
+ testPageLoadBodyAfterSettingUpdate();
+ });
+}
+
+function testPageLoadBodyAfterSettingUpdate()
+{
+ let loaded = false;
+ let requestCallbackInvoked = false;
+
+ requestCallback = function() {
+ ok(lastRequest, "Page load was logged again");
+ ok(!lastRequest.discardResponseBody, "Response body was not discarded");
+ is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ lastRequest = null;
+ requestCallback = null;
+ requestCallbackInvoked = true;
+
+ if (loaded) {
+ executeSoon(testXhrGet);
+ }
+ };
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ loaded = true;
+
+ if (requestCallbackInvoked) {
+ executeSoon(testXhrGet);
+ }
+ }, true);
+
+ content.location.reload();
+}
+
+function testXhrGet()
+{
+ requestCallback = function() {
+ ok(lastRequest, "testXhrGet() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ ok(!lastRequest.request.postData.text, "No request body was sent");
+ ok(!lastRequest.discardRequestBody, "Request body was not discarded");
+ is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testXhrPost);
+ };
+
+ // Start the XMLHttpRequest() GET test.
+ content.wrappedJSObject.testXhrGet();
+}
+
+function testXhrPost()
+{
+ requestCallback = function() {
+ ok(lastRequest, "testXhrPost() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+ is(lastRequest.request.postData.text, "Hello world!",
+ "Request body was logged");
+ is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ lastRequest = null;
+ requestCallback = null;
+ executeSoon(testFormSubmission);
+ };
+
+ // Start the XMLHttpRequest() POST test.
+ content.wrappedJSObject.testXhrPost();
+}
+
+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.
+ requestCallback = function() {
+ ok(lastRequest, "testFormSubmission() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+ isnot(lastRequest.request.postData.text.
+ indexOf("Content-Type: application/x-www-form-urlencoded"), -1,
+ "Content-Type is correct");
+ isnot(lastRequest.request.postData.text.
+ indexOf("Content-Length: 20"), -1, "Content-length is correct");
+ isnot(lastRequest.request.postData.text.
+ indexOf("name=foo+bar&age=144"), -1, "Form data is correct");
+ is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ executeSoon(testNetworkPanel);
+ };
+
+ let form = content.document.querySelector("form");
+ ok(form, "we have the HTML form");
+ form.submit();
+}
+
+function testNetworkPanel()
+{
+ // Open the NetworkPanel. The functionality of the NetworkPanel is tested
+ // within separate test files.
+ let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest);
+
+ networkPanel.panel.addEventListener("popupshown", function onPopupShown() {
+ networkPanel.panel.removeEventListener("popupshown", onPopupShown, true);
+
+ is(hud.ui.filterBox._netPanel, networkPanel,
+ "Network panel stored on anchor node");
+ ok(true, "NetworkPanel was opened");
+
+ // All tests are done. Shutdown.
+ networkPanel.panel.hidePopup();
+ lastRequest = null;
+ HUDService.lastFinishedRequestCallback = null;
+ executeSoon(finishTest);
+ }, true);
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js
new file mode 100644
index 000000000..678597224
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js
@@ -0,0 +1,543 @@
+/* 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 network panel works.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
+const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html";
+
+const TEST_IMG_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJREFU" +
+ "OI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuFDjq5dxD8" +
+ "FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNUyhsKAChRBQcP" +
+ "FVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQEod+Z5sP1FizDxGgy" +
+ "BqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTsDizDRMPuIOC+zEeTMZo9" +
+ "BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joREN2uqnlBmCwW1hIJagtev4f3z" +
+ "A16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOnidadmwtwsnMu18q83/kHSou+bFND" +
+ "Dr4AAAAASUVORK5CYII=";
+
+let testDriver;
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testNetworkPanel);
+ }, true);
+}
+
+function testNetworkPanel() {
+ testDriver = testGen();
+ testDriver.next();
+}
+
+function checkIsVisible(aPanel, aList) {
+ for (let id in aList) {
+ let node = aPanel.document.getElementById(id);
+ let isVisible = aList[id];
+ is(node.style.display, (isVisible ? "block" : "none"), id + " isVisible=" + isVisible);
+ }
+}
+
+function checkNodeContent(aPanel, aId, aContent) {
+ let node = aPanel.document.getElementById(aId);
+ if (node == null) {
+ ok(false, "Tried to access node " + aId + " that doesn't exist!");
+ }
+ else if (node.textContent.indexOf(aContent) != -1) {
+ ok(true, "checking content of " + aId);
+ }
+ else {
+ ok(false, "Got false value for " + aId + ": " + node.textContent + " doesn't have " + aContent);
+ }
+}
+
+function checkNodeKeyValue(aPanel, aId, aKey, aValue) {
+ let node = aPanel.document.getElementById(aId);
+
+ let headers = node.querySelectorAll("th");
+ for (let i = 0; i < headers.length; i++) {
+ if (headers[i].textContent == (aKey + ":")) {
+ is(headers[i].nextElementSibling.textContent, aValue,
+ "checking content of " + aId + " for key " + aKey);
+ return;
+ }
+ }
+
+ ok(false, "content check failed for " + aId + ", key " + aKey);
+}
+
+function testGen() {
+ let hud = HUDService.getHudByWindow(content);
+ let filterBox = hud.ui.filterBox;
+
+ let httpActivity = {
+ updates: [],
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: (new Date()).toISOString(),
+ request: {
+ url: "http://www.testpage.com",
+ method: "GET",
+ cookies: [],
+ headers: [
+ { name: "foo", value: "bar" },
+ ],
+ },
+ response: {
+ headers: [],
+ content: {},
+ cookies: [],
+ },
+ timings: {},
+ };
+
+ let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+
+ is(filterBox._netPanel, networkPanel,
+ "Network panel stored on the anchor object");
+
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ info("test 1");
+
+ checkIsVisible(networkPanel, {
+ requestCookie: false,
+ requestFormData: false,
+ requestBody: false,
+ responseContainer: false,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ checkNodeContent(networkPanel, "header", "http://www.testpage.com");
+ checkNodeContent(networkPanel, "header", "GET");
+ checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar");
+
+ // Test request body.
+ info("test 2: request body");
+ httpActivity.discardRequestBody = false;
+ httpActivity.request.postData = { text: "hello world" };
+ networkPanel.update();
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: false,
+ responseContainer: false,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+ checkNodeContent(networkPanel, "requestBodyContent", "hello world");
+
+ // Test response header.
+ info("test 3: response header");
+ httpActivity.timings.wait = 10;
+ httpActivity.response.httpVersion = "HTTP/3.14";
+ httpActivity.response.status = 999;
+ httpActivity.response.statusText = "earthquake win";
+ httpActivity.response.content.mimeType = "text/html";
+ httpActivity.response.headers.push(
+ { name: "Content-Type", value: "text/html" },
+ { name: "leaveHouses", value: "true" }
+ );
+
+ networkPanel.update();
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: false,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ checkNodeContent(networkPanel, "header", "HTTP/3.14 999 earthquake win");
+ checkNodeKeyValue(networkPanel, "responseHeadersContent", "leaveHouses", "true");
+ checkNodeContent(networkPanel, "responseHeadersInfo", "10ms");
+
+ info("test 4");
+
+ httpActivity.discardResponseBody = false;
+ httpActivity.timings.receive = 2;
+ networkPanel.update();
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestCookie: false,
+ requestFormData: false,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ info("test 5");
+
+ httpActivity.updates.push("responseContent", "eventTimings");
+ networkPanel.update();
+
+ checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms");
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestCookie: false,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: true,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ networkPanel.panel.hidePopup();
+
+ // Second run: Test for cookies and response body.
+ info("test 6: cookies and response body");
+ httpActivity.request.cookies.push(
+ { name: "foo", value: "bar" },
+ { name: "hello", value: "world" }
+ );
+ httpActivity.response.content.text = "get out here";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ is(filterBox._netPanel, networkPanel,
+ "Network panel stored on httpActivity object");
+
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: true,
+ responseContainer: true,
+ responseCookie: false,
+ responseBody: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ checkNodeKeyValue(networkPanel, "requestCookieContent", "foo", "bar");
+ checkNodeKeyValue(networkPanel, "requestCookieContent", "hello", "world");
+ checkNodeContent(networkPanel, "responseBodyContent", "get out here");
+ checkNodeContent(networkPanel, "responseBodyInfo", "2ms");
+
+ networkPanel.panel.hidePopup();
+
+ // Third run: Test for response cookies.
+ info("test 6b: response cookies");
+ httpActivity.response.cookies.push(
+ { name: "foobar", value: "boom" },
+ { name: "foobaz", value: "omg" }
+ );
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ is(filterBox._netPanel, networkPanel,
+ "Network panel stored on httpActivity object");
+
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: true,
+ responseContainer: true,
+ responseCookie: true,
+ responseBody: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false,
+ responseBodyFetchLink: false,
+ });
+
+ checkNodeKeyValue(networkPanel, "responseCookieContent", "foobar", "boom");
+ checkNodeKeyValue(networkPanel, "responseCookieContent", "foobaz", "omg");
+
+ networkPanel.panel.hidePopup();
+
+ // Check image request.
+ info("test 7: image request");
+ httpActivity.response.headers[1].value = "image/png";
+ httpActivity.response.content.mimeType = "image/png";
+ httpActivity.response.content.text = TEST_IMG_BASE64;
+ httpActivity.request.url = TEST_IMG;
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: true,
+ responseImageCached: false,
+ responseBodyFetchLink: false,
+ });
+
+ let imgNode = networkPanel.document.getElementById("responseImageNode");
+ is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
+ "Displayed image is correct");
+
+ function checkImageResponseInfo() {
+ checkNodeContent(networkPanel, "responseImageInfo", "2ms");
+ checkNodeContent(networkPanel, "responseImageInfo", "16x16px");
+ }
+
+ // Check if the image is loaded already.
+ imgNode.addEventListener("load", function onLoad() {
+ imgNode.removeEventListener("load", onLoad, false);
+ checkImageResponseInfo();
+ networkPanel.panel.hidePopup();
+ testDriver.next();
+ }, false);
+ yield;
+
+ // Check cached image request.
+ info("test 8: cached image request");
+ httpActivity.response.httpVersion = "HTTP/1.1";
+ httpActivity.response.status = 304;
+ httpActivity.response.statusText = "Not Modified";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: true,
+ requestFormData: false,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: true
+ });
+
+ let imgNode = networkPanel.document.getElementById("responseImageCachedNode");
+ is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
+ "Displayed image is correct");
+
+ networkPanel.panel.hidePopup();
+
+ // Test sent form data.
+ info("test 9: sent form data");
+ httpActivity.request.postData.text = [
+ "Content-Type: application/x-www-form-urlencoded",
+ "Content-Length: 59",
+ "name=rob&age=20"
+ ].join("\n");
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: false,
+ requestFormData: true,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: true
+ });
+
+ checkNodeKeyValue(networkPanel, "requestFormDataContent", "name", "rob");
+ checkNodeKeyValue(networkPanel, "requestFormDataContent", "age", "20");
+ networkPanel.panel.hidePopup();
+
+ // Test no space after Content-Type:
+ info("test 10: no space after Content-Type header in post data");
+ httpActivity.request.postData.text = "Content-Type:application/x-www-form-urlencoded\n";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: false,
+ requestFormData: true,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: true
+ });
+
+ networkPanel.panel.hidePopup();
+
+ // Test cached data.
+
+ info("test 11: cached data");
+
+ httpActivity.request.url = TEST_ENCODING_ISO_8859_1;
+ httpActivity.response.headers[1].value = "application/json";
+ httpActivity.response.content.mimeType = "application/json";
+ httpActivity.response.content.text = "my cached data is here!";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: false,
+ requestFormData: true,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseBodyCached: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ checkNodeContent(networkPanel, "responseBodyCachedContent",
+ "my cached data is here!");
+
+ networkPanel.panel.hidePopup();
+
+ // Test a response with a content type that can't be displayed in the
+ // NetworkPanel.
+ info("test 12: unknown content type");
+ httpActivity.response.headers[1].value = "application/x-shockwave-flash";
+ httpActivity.response.content.mimeType = "application/x-shockwave-flash";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel._onUpdate = function() {
+ networkPanel._onUpdate = null;
+ executeSoon(function() {
+ testDriver.next();
+ });
+ };
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: false,
+ requestFormData: true,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseBodyCached: false,
+ responseBodyUnknownType: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ let responseString =
+ WCU_l10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content",
+ ["application/x-shockwave-flash"]);
+ checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString);
+ networkPanel.panel.hidePopup();
+
+ /*
+
+ // This test disabled. See bug 603620.
+
+ // Test if the NetworkPanel figures out the content type based on an URL as
+ // well.
+ delete httpActivity.response.header["Content-Type"];
+ httpActivity.url = "http://www.test.com/someCrazyFile.swf?done=right&ending=txt";
+
+ networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
+ networkPanel.isDoneCallback = function NP_doneCallback() {
+ networkPanel.isDoneCallback = null;
+ testDriver.next();
+ }
+
+ yield;
+
+ checkIsVisible(networkPanel, {
+ requestBody: false,
+ requestFormData: true,
+ requestCookie: true,
+ responseContainer: true,
+ responseBody: false,
+ responseBodyCached: false,
+ responseBodyUnknownType: true,
+ responseNoBody: false,
+ responseImage: false,
+ responseImageCached: false
+ });
+
+ // Systems without Flash installed will return an empty string here. Ignore.
+ if (networkPanel.document.getElementById("responseBodyUnknownTypeContent").textContent !== "")
+ checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString);
+ else
+ ok(true, "Flash not installed");
+
+ networkPanel.panel.hidePopup(); */
+
+ // All done!
+ testDriver = null;
+ executeSoon(finishTest);
+
+ yield;
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_notifications.js b/browser/devtools/webconsole/test/browser_webconsole_notifications.js
new file mode 100644
index 000000000..c70605635
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_notifications.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/. */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for notifications";
+
+function test() {
+ observer.init();
+ addTab(TEST_URI);
+ browser.addEventListener("load", onLoad, true);
+}
+
+function webConsoleCreated(aID)
+{
+ Services.obs.removeObserver(observer, "web-console-created");
+ ok(HUDService.hudReferences[aID], "We have a hud reference");
+ content.wrappedJSObject.console.log("adding a log message");
+}
+
+function webConsoleDestroyed(aID)
+{
+ Services.obs.removeObserver(observer, "web-console-destroyed");
+ ok(!HUDService.hudReferences[aID], "We do not have a hud reference");
+ executeSoon(finishTest);
+}
+
+function webConsoleMessage(aID, aNodeID)
+{
+ Services.obs.removeObserver(observer, "web-console-message-created");
+ ok(aID, "we have a console ID");
+ is(typeof aNodeID, "string", "message node id is a string");
+ executeSoon(closeConsole);
+}
+
+let observer = {
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData)
+ {
+ aSubject = aSubject.QueryInterface(Ci.nsISupportsString);
+
+ switch(aTopic) {
+ case "web-console-created":
+ webConsoleCreated(aSubject);
+ break;
+ case "web-console-destroyed":
+ webConsoleDestroyed(aSubject);
+ break;
+ case "web-console-message-created":
+ webConsoleMessage(aSubject, aData);
+ 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);
+ }
+};
+
+function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole();
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js b/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js
new file mode 100644
index 000000000..fff74534a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+// Test that JavaScript expressions that evaluate to null or undefined produce
+// meaningful output.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testNullAndUndefinedOutput);
+ }, true);
+}
+
+function testNullAndUndefinedOutput(hud) {
+ let jsterm = hud.jsterm;
+ let outputNode = jsterm.outputNode;
+
+ jsterm.clearOutput();
+ jsterm.execute("null;");
+
+ waitForSuccess({
+ name: "null displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-msg-node").length == 2;
+ },
+ successFn: function()
+ {
+ let nodes = outputNode.querySelectorAll(".hud-msg-node");
+ isnot(nodes[1].textContent.indexOf("null"), -1,
+ "'null' printed to output");
+
+ jsterm.clearOutput();
+ jsterm.execute("undefined;");
+ waitForSuccess(waitForUndefined);
+ },
+ failureFn: finishTest,
+ });
+
+ let waitForUndefined = {
+ name: "undefined displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-msg-node").length == 2;
+ },
+ successFn: function()
+ {
+ let nodes = outputNode.querySelectorAll(".hud-msg-node");
+ isnot(nodes[1].textContent.indexOf("undefined"), -1,
+ "'undefined' printed to output");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ };
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_order.js b/browser/devtools/webconsole/test/browser_webconsole_output_order.js
new file mode 100644
index 000000000..63e9ecd6a
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js
@@ -0,0 +1,44 @@
+/* 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 any output created from calls to the console API comes after the
+// echoed JavaScript.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testOutputOrder);
+ }, true);
+}
+
+function testOutputOrder(hud) {
+ let jsterm = hud.jsterm;
+ let outputNode = jsterm.outputNode;
+
+ jsterm.clearOutput();
+ jsterm.execute("console.log('foo', 'bar');");
+
+ waitForSuccess({
+ name: "console.log message displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".hud-msg-node").length == 3;
+ },
+ successFn: function()
+ {
+ let nodes = outputNode.querySelectorAll(".hud-msg-node");
+ let executedStringFirst =
+ /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent);
+ let outputSecond = /"foo" "bar"/.test(nodes[2].textContent);
+ ok(executedStringFirst && outputSecond, "executed string comes first");
+
+ finishTest();
+ },
+ failureFn: finishTest,
+ });
+}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js
new file mode 100644
index 000000000..9d36a98ba
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js
@@ -0,0 +1,42 @@
+/* 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 the property provider, which is part of the code completion
+// infrastructure.
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test the JS property provider";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", testPropertyProvider, true);
+}
+
+function testPropertyProvider() {
+ browser.removeEventListener("load", testPropertyProvider, true);
+
+ let tmp = {};
+ Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp);
+ let JSPropertyProvider = tmp.JSPropertyProvider;
+ tmp = null;
+
+ let completion = JSPropertyProvider(content, "thisIsNotDefined");
+ is (completion.matches.length, 0, "no match for 'thisIsNotDefined");
+
+ // This is a case the PropertyProvider can't handle. Should return null.
+ completion = JSPropertyProvider(content, "window[1].acb");
+ is (completion, null, "no match for 'window[1].acb");
+
+ // A very advanced completion case.
+ var strComplete =
+ 'function a() { }document;document.getElementById(window.locatio';
+ completion = JSPropertyProvider(content, strComplete);
+ ok(completion.matches.length == 2, "two matches found");
+ ok(completion.matchProp == "locatio", "matching part is 'test'");
+ ok(completion.matches[0] == "location", "the first match is 'location'");
+ ok(completion.matches[1] == "locationbar", "the second match is 'locationbar'");
+
+ finishTest();
+}
+
diff --git a/browser/devtools/webconsole/test/browser_webconsole_view_source.js b/browser/devtools/webconsole/test/browser_webconsole_view_source.js
new file mode 100644
index 000000000..b4fe223be
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js
@@ -0,0 +1,66 @@
+/* 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.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
+
+function test() {
+ addTab(TEST_URI);
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, function(hud) {
+ executeSoon(function() {
+ testViewSource(hud);
+ });
+ });
+ }, true);
+}
+
+function testViewSource(hud) {
+ let button = content.document.querySelector("button");
+ button = XPCNativeWrapper.unwrap(button);
+ ok(button, "we have the button on the page");
+
+ expectUncaughtException();
+ EventUtils.sendMouseEvent({ type: "click" }, button, XPCNativeWrapper.unwrap(content));
+
+ waitForSuccess({
+ name: "find the location node",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-location");
+ },
+ successFn: function()
+ {
+ let locationNode = hud.outputNode.querySelector(".webconsole-location");
+
+ Services.ww.registerNotification(observer);
+
+ EventUtils.sendMouseEvent({ type: "click" }, locationNode);
+ },
+ failureFn: finishTest,
+ });
+}
+
+let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ ok(true, "the view source window was opened in response to clicking " +
+ "the location node");
+
+ // executeSoon() is necessary to avoid crashing Firefox. See bug 611543.
+ executeSoon(function() {
+ aSubject.close();
+ finishTest();
+ });
+ }
+};
+
+registerCleanupFunction(function() {
+ Services.ww.unregisterNotification(observer);
+});
diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js
new file mode 100644
index 000000000..0f90d37fb
--- /dev/null
+++ b/browser/devtools/webconsole/test/head.js
@@ -0,0 +1,1247 @@
+/* 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/. */
+
+let tempScope = {};
+Cu.import("resource:///modules/HUDService.jsm", tempScope);
+let HUDService = tempScope.HUDService;
+Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope);
+let WebConsoleUtils = tempScope.WebConsoleUtils;
+Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
+let gDevTools = tempScope.gDevTools;
+Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+let TargetFactory = tempScope.devtools.TargetFactory;
+Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+let console = tempScope.console;
+let Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
+
+let 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;
+
+// The possible message severities.
+const SEVERITY_ERROR = 0;
+const SEVERITY_WARNING = 1;
+const SEVERITY_INFO = 2;
+const SEVERITY_LOG = 3;
+
+const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI);
+
+function log(aMsg)
+{
+ dump("*** WebConsoleTest: " + aMsg + "\n");
+}
+
+function pprint(aObj)
+{
+ for (let prop in aObj) {
+ if (typeof aObj[prop] == "function") {
+ log("function " + prop);
+ }
+ else {
+ log(prop + ": " + aObj[prop]);
+ }
+ }
+}
+
+let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs;
+
+function addTab(aURL)
+{
+ gBrowser.selectedTab = gBrowser.addTab(aURL);
+ tab = gBrowser.selectedTab;
+ browser = gBrowser.getBrowserForTab(tab);
+}
+
+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.contentDocument.readyState != "complete") {
+ stillToLoad++;
+ browser.addEventListener("load", onLoad, true);
+ }
+ }
+
+ if (!stillToLoad)
+ callback();
+}
+
+/**
+ * Check if a log entry exists in the HUD output node.
+ *
+ * @param {Element} aOutputNode
+ * the HUD output node.
+ * @param {string} aMatchString
+ * the string you want to check if it exists in the output node.
+ * @param {string} aMsg
+ * the message describing the test
+ * @param {boolean} [aOnlyVisible=false]
+ * find only messages that are visible, not hidden by the filter.
+ * @param {boolean} [aFailIfFound=false]
+ * fail the test if the string is found in the output node.
+ * @param {string} aClass [optional]
+ * find only messages with the given CSS class.
+ */
+function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible,
+ aFailIfFound, aClass)
+{
+ let selector = ".hud-msg-node";
+ // Skip entries that are hidden by the filter.
+ if (aOnlyVisible) {
+ selector += ":not(.hud-filtered-by-type)";
+ }
+ if (aClass) {
+ selector += "." + aClass;
+ }
+
+ let msgs = aOutputNode.querySelectorAll(selector);
+ let found = false;
+ for (let i = 0, n = msgs.length; i < n; i++) {
+ let message = msgs[i].textContent.indexOf(aMatchString);
+ if (message > -1) {
+ found = true;
+ break;
+ }
+
+ // Search the labels too.
+ let labels = msgs[i].querySelectorAll("label");
+ for (let j = 0; j < labels.length; j++) {
+ if (labels[j].getAttribute("value").indexOf(aMatchString) > -1) {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ is(found, !aFailIfFound, aMsg);
+}
+
+/**
+ * A convenience method to call testLogEntry().
+ *
+ * @param string aString
+ * The string to find.
+ */
+function findLogEntry(aString)
+{
+ testLogEntry(outputNode, aString, "found " + aString);
+}
+
+/**
+ * Open the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [aTab]
+ * Optional tab element for which you want open the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [aCallback]
+ * Optional function to invoke after the Web Console completes
+ * initialization (web-console-created).
+ */
+function openConsole(aTab, aCallback = function() { })
+{
+ let target = TargetFactory.forTab(aTab || tab);
+ gDevTools.showToolbox(target, "webconsole").then(function(toolbox) {
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ aCallback(hud);
+ });
+}
+
+/**
+ * Close the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [aTab]
+ * Optional tab element for which you want close the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [aCallback]
+ * Optional function to invoke after the Web Console completes
+ * closing (web-console-destroyed).
+ */
+function closeConsole(aTab, aCallback = function() { })
+{
+ let target = TargetFactory.forTab(aTab || tab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ let panel = toolbox.getPanel("webconsole");
+ if (panel) {
+ let hudId = panel.hud.hudId;
+ toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug);
+ }
+ else {
+ toolbox.destroy().then(aCallback.bind(null));
+ }
+ }
+ else {
+ aCallback();
+ }
+}
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement aPopup
+ * The XUL popup you expect to open.
+ * @param nsIDOMElement aButton
+ * The button/element that receives the contextmenu event. This is
+ * expected to open the popup.
+ * @param function aOnShown
+ * Function to invoke on popupshown event.
+ * @param function aOnHidden
+ * Function to invoke on popuphidden event.
+ */
+function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden)
+{
+ function onPopupShown() {
+ info("onPopupShown");
+ aPopup.removeEventListener("popupshown", onPopupShown);
+
+ aOnShown();
+
+ // Use executeSoon() to get out of the popupshown event.
+ aPopup.addEventListener("popuphidden", onPopupHidden);
+ executeSoon(() => aPopup.hidePopup());
+ }
+ function onPopupHidden() {
+ info("onPopupHidden");
+ aPopup.removeEventListener("popuphidden", onPopupHidden);
+ aOnHidden();
+ }
+
+ aPopup.addEventListener("popupshown", onPopupShown);
+
+ info("wait for the context menu to open");
+ let eventDetails = { type: "contextmenu", button: 2};
+ EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails,
+ aButton.ownerDocument.defaultView);
+}
+
+/**
+ * Dump the output of all open Web Consoles - used only for debugging purposes.
+ */
+function dumpConsoles()
+{
+ if (gPendingOutputTest) {
+ console.log("dumpConsoles start");
+ for each (let hud in HUDService.hudReferences) {
+ 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 aMessage
+ * The message element you want to display.
+ */
+function dumpMessageElement(aMessage)
+{
+ let text = getMessageElementText(aMessage);
+ let repeats = aMessage.querySelector(".webconsole-msg-repeat");
+ if (repeats) {
+ repeats = repeats.getAttribute("value");
+ }
+ console.debug("id", aMessage.getAttribute("id"),
+ "date", aMessage.timestamp,
+ "class", aMessage.className,
+ "category", aMessage.category,
+ "severity", aMessage.severity,
+ "repeats", repeats,
+ "clipboardText", aMessage.clipboardText,
+ "text", text);
+}
+
+function finishTest()
+{
+ browser = hudId = hud = filterBox = outputNode = cs = null;
+
+ dumpConsoles();
+
+ if (HUDConsoleUI.browserConsole) {
+ let hud = HUDConsoleUI.browserConsole;
+
+ if (hud.jsterm) {
+ hud.jsterm.clearOutput(true);
+ }
+
+ HUDConsoleUI.toggleBrowserConsole().then(finishTest);
+ return;
+ }
+
+ let hud = HUDService.getHudByWindow(content);
+ if (!hud) {
+ finish();
+ return;
+ }
+
+ if (hud.jsterm) {
+ hud.jsterm.clearOutput(true);
+ }
+
+ closeConsole(hud.target.tab, finish);
+
+ hud = null;
+}
+
+function tearDown()
+{
+ dumpConsoles();
+
+ if (HUDConsoleUI.browserConsole) {
+ HUDConsoleUI.toggleBrowserConsole();
+ }
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.closeToolbox(target);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null;
+}
+
+registerCleanupFunction(tearDown);
+
+waitForExplicitFinish();
+
+/**
+ * Polls a given function waiting for it to become true.
+ *
+ * @param object aOptions
+ * Options object with the following properties:
+ * - 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, succesFn
+ * is called and polling stops. If validatorFn never returns true, then
+ * polling timeouts after several tries and a failure is recorded.
+ * - successFn
+ * A function called when the validator function returns true.
+ * - failureFn
+ * A function called if the validator function timeouts - fails to return
+ * true 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.
+ */
+function waitForSuccess(aOptions)
+{
+ let start = Date.now();
+ let timeout = aOptions.timeout || 5000;
+
+ function wait(validatorFn, successFn, failureFn)
+ {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + aOptions.name);
+ failureFn(aOptions);
+ return;
+ }
+
+ if (validatorFn(aOptions)) {
+ ok(true, aOptions.name);
+ successFn();
+ }
+ else {
+ setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+ }
+ }
+
+ wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn);
+}
+
+function openInspector(aCallback, aTab = gBrowser.selectedTab)
+{
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ aCallback(toolbox.getCurrentPanel());
+ });
+}
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param object aView
+ * The VariablesView instance.
+ * @param array aRules
+ * 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 aOptions
+ * 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(aView, aRules, aOptions)
+{
+ // Initialize the search.
+ function init()
+ {
+ // Separate out the rules that require expanding properties throughout the
+ // view.
+ let expandRules = [];
+ let rules = aRules.filter((aRule) => {
+ if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) {
+ expandRules.push(aRule);
+ 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, aView, 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 aRules array.
+ let returnResults = onAllRulesMatched.bind(null, aRules);
+
+ return Promise.all(outstanding).then(lastStep).then(returnResults);
+ }
+
+ function onMatch(aProp, aRule, aMatched)
+ {
+ if (aMatched && !aRule.matchedProp) {
+ aRule.matchedProp = aProp;
+ }
+ }
+
+ function finder(aRules, aVar, aPromises)
+ {
+ for (let [id, prop] in aVar) {
+ for (let rule of aRules) {
+ let matcher = matchVariablesViewProperty(prop, rule, aOptions);
+ aPromises.push(matcher.then(onMatch.bind(null, prop, rule)));
+ }
+ }
+ }
+
+ function processExpandRules(aRules)
+ {
+ let rule = aRules.shift();
+ if (!rule) {
+ return Promise.resolve(null);
+ }
+
+ let deferred = Promise.defer();
+ let expandOptions = {
+ rootVariable: aView,
+ expandTo: rule.name,
+ webconsole: aOptions.webconsole,
+ };
+
+ variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) {
+ let name = rule.name;
+ let lastName = name.split(".").pop();
+ rule.name = lastName;
+
+ let matched = matchVariablesViewProperty(aProp, rule, aOptions);
+ return matched.then(onMatch.bind(null, aProp, rule)).then(function() {
+ rule.name = name;
+ });
+ }, function onFailure() {
+ return Promise.resolve(null);
+ }).then(processExpandRules.bind(null, aRules)).then(function() {
+ deferred.resolve(null);
+ });
+
+ return deferred.promise;
+ }
+
+ function onAllRulesMatched(aRules)
+ {
+ for (let rule of aRules) {
+ 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 aRules;
+ }
+
+ return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object aProp
+ * The variable's view Property instance.
+ * @param object aRule
+ * Rules for matching the property. See findVariableViewProperties() for
+ * details.
+ * @param object aOptions
+ * 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(aProp, aRule, aOptions)
+{
+ function resolve(aResult) {
+ return Promise.resolve(aResult);
+ }
+
+ if (aRule.name) {
+ let match = aRule.name instanceof RegExp ?
+ aRule.name.test(aProp.name) :
+ aProp.name == aRule.name;
+ if (!match) {
+ return resolve(false);
+ }
+ }
+
+ if (aRule.value) {
+ let displayValue = aProp.displayValue;
+ if (aProp.displayValueClassName == "token-string") {
+ displayValue = displayValue.substring(1, displayValue.length - 1);
+ }
+
+ let match = aRule.value instanceof RegExp ?
+ aRule.value.test(displayValue) :
+ displayValue == aRule.value;
+ if (!match) {
+ info("rule " + aRule.name + " did not match value, expected '" +
+ aRule.value + "', found '" + displayValue + "'");
+ return resolve(false);
+ }
+ }
+
+ if ("isGetter" in aRule) {
+ let isGetter = !!(aProp.getter && aProp.get("get"));
+ if (aRule.isGetter != isGetter) {
+ info("rule " + aRule.name + " getter test failed");
+ return resolve(false);
+ }
+ }
+
+ if ("isGenerator" in aRule) {
+ let isGenerator = aProp.displayValue == "[object Generator]";
+ if (aRule.isGenerator != isGenerator) {
+ info("rule " + aRule.name + " generator test failed");
+ return resolve(false);
+ }
+ }
+
+ let outstanding = [];
+
+ if ("isIterator" in aRule) {
+ let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole);
+ outstanding.push(isIterator.then((aResult) => {
+ if (aResult != aRule.isIterator) {
+ info("rule " + aRule.name + " iterator test failed");
+ }
+ return aResult == aRule.isIterator;
+ }));
+ }
+
+ outstanding.push(Promise.resolve(true));
+
+ return Promise.all(outstanding).then(function _onMatchDone(aResults) {
+ let ruleMatched = aResults.indexOf(false) == -1;
+ return resolve(ruleMatched);
+ });
+}
+
+/**
+ * Check if the given variables view property is an iterator.
+ *
+ * @param object aProp
+ * The Property instance you want to check.
+ * @param object aWebConsole
+ * 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(aProp, aWebConsole)
+{
+ if (aProp.displayValue == "[object Iterator]") {
+ return Promise.resolve(true);
+ }
+
+ let deferred = Promise.defer();
+
+ variablesViewExpandTo({
+ rootVariable: aProp,
+ expandTo: "__proto__.__iterator__",
+ webconsole: aWebConsole,
+ }).then(function onSuccess(aProp) {
+ deferred.resolve(true);
+ }, function onFailure() {
+ deferred.resolve(false);
+ });
+
+ return deferred.promise;
+}
+
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param aOptions
+ * 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(aOptions)
+{
+ let root = aOptions.rootVariable;
+ let expandTo = aOptions.expandTo.split(".");
+ let jsterm = (aOptions.webconsole || {}).jsterm;
+ let lastDeferred = Promise.defer();
+
+ function fetch(aProp)
+ {
+ if (!aProp.onexpand) {
+ ok(false, "property " + aProp.name + " cannot be expanded: !onexpand");
+ return Promise.reject(aProp);
+ }
+
+ let deferred = Promise.defer();
+
+ if (aProp._fetched || !jsterm) {
+ executeSoon(function() {
+ deferred.resolve(aProp);
+ });
+ }
+ else {
+ jsterm.once("variablesview-fetched", function _onFetchProp() {
+ executeSoon(() => deferred.resolve(aProp));
+ });
+ }
+
+ aProp.expand();
+
+ return deferred.promise;
+ }
+
+ function getNext(aProp)
+ {
+ let name = expandTo.shift();
+ let newProp = aProp.get(name);
+
+ if (expandTo.length > 0) {
+ ok(newProp, "found property " + name);
+ if (newProp) {
+ fetch(newProp).then(getNext, fetchError);
+ }
+ else {
+ lastDeferred.reject(aProp);
+ }
+ }
+ else {
+ if (newProp) {
+ lastDeferred.resolve(newProp);
+ }
+ else {
+ lastDeferred.reject(aProp);
+ }
+ }
+ }
+
+ function fetchError(aProp)
+ {
+ lastDeferred.reject(aProp);
+ }
+
+ 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 aOptions
+ * 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.
+ * - callback: function to invoke after the property is updated.
+ */
+function updateVariablesViewProperty(aOptions)
+{
+ let view = aOptions.property._variablesView;
+ view.window.focus();
+ aOptions.property.focus();
+
+ switch (aOptions.field) {
+ case "name":
+ EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, view.window);
+ break;
+ case "value":
+ EventUtils.synthesizeKey("VK_ENTER", {}, view.window);
+ break;
+ default:
+ throw new Error("options.field is incorrect");
+ return;
+ }
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
+
+ for (let c of aOptions.string) {
+ EventUtils.synthesizeKey(c, {}, gVariablesView.window);
+ }
+
+ if (aOptions.webconsole) {
+ aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback);
+ }
+
+ EventUtils.synthesizeKey("VK_ENTER", {}, view.window);
+
+ if (!aOptions.webconsole) {
+ executeSoon(aOptions.callback);
+ }
+ });
+}
+
+/**
+ * Open the JavaScript debugger.
+ *
+ * @param object aOptions
+ * 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(aOptions = {})
+{
+ if (!aOptions.tab) {
+ aOptions.tab = gBrowser.selectedTab;
+ }
+
+ let deferred = Promise.defer();
+
+ let target = TargetFactory.forTab(aOptions.tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger");
+
+ gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) {
+ let panel = aToolbox.getCurrentPanel();
+ let panelWin = panel.panelWin;
+
+ panel._view.Variables.lazyEmpty = false;
+ panel._view.Variables.lazyAppend = false;
+
+ let resolveObject = {
+ target: target,
+ toolbox: aToolbox,
+ panel: panel,
+ panelWin: panelWin,
+ };
+
+ if (dbgPanelAlreadyOpen) {
+ deferred.resolve(resolveObject);
+ }
+ else {
+ panelWin.addEventListener("Debugger:AfterSourcesAdded",
+ function onAfterSourcesAdded() {
+ panelWin.removeEventListener("Debugger:AfterSourcesAdded",
+ onAfterSourcesAdded);
+ deferred.resolve(resolveObject);
+ });
+ }
+ }, function onFailure(aReason) {
+ console.debug("failed to open the toolbox for 'jsdebugger'", aReason);
+ deferred.reject(aReason);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Get the full text displayed by a Web Console message.
+ *
+ * @param nsIDOMElement aElement
+ * The message element from the Web Console output.
+ * @return string
+ * The full text displayed by the given message element.
+ */
+function getMessageElementText(aElement)
+{
+ let text = aElement.textContent;
+ let labels = aElement.querySelectorAll("label");
+ for (let label of labels) {
+ text += " " + label.getAttribute("value");
+ }
+ return text;
+}
+
+/**
+ * Wait for messages in the Web Console output.
+ *
+ * @param object aOptions
+ * Options for what you want to wait for:
+ * - webconsole: the webconsole instance you work with.
+ * - 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.
+ * - longString: boolean, set to |true} to match long strings in the
+ * 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.
+ * @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(aOptions)
+{
+ gPendingOutputTest++;
+ let webconsole = aOptions.webconsole;
+ let rules = WebConsoleUtils.cloneObject(aOptions.messages, true);
+ let rulesMatched = 0;
+ let listenerAdded = false;
+ let deferred = Promise.defer();
+
+ function checkText(aRule, aText)
+ {
+ let result;
+ if (typeof aRule == "string") {
+ result = aText.indexOf(aRule) > -1;
+ }
+ else if (aRule instanceof RegExp) {
+ result = aRule.test(aText);
+ }
+ return result;
+ }
+
+ function checkConsoleTrace(aRule, aElement)
+ {
+ let elemText = getMessageElementText(aElement);
+ let trace = aRule.consoleTrace;
+
+ if (!checkText("Stack trace from ", elemText)) {
+ return false;
+ }
+
+ let clickable = aElement.querySelector(".hud-clickable");
+ if (!clickable) {
+ ok(false, "console.trace() message is missing .hud-clickable");
+ displayErrorContext(aRule, aElement);
+ return false;
+ }
+ aRule.clickableElements = [clickable];
+
+ if (trace.file &&
+ !checkText("from " + trace.file + ", ", elemText)) {
+ ok(false, "console.trace() message is missing the file name: " +
+ trace.file);
+ displayErrorContext(aRule, aElement);
+ return false;
+ }
+
+ if (trace.fn &&
+ !checkText(", function " + trace.fn + ", ", elemText)) {
+ ok(false, "console.trace() message is missing the function name: " +
+ trace.fn);
+ displayErrorContext(aRule, aElement);
+ return false;
+ }
+
+ if (trace.line &&
+ !checkText(", line " + trace.line + ".", elemText)) {
+ ok(false, "console.trace() message is missing the line number: " +
+ trace.line);
+ displayErrorContext(aRule, aElement);
+ return false;
+ }
+
+ aRule.category = CATEGORY_WEBDEV;
+ aRule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleTime(aRule, aElement)
+ {
+ let elemText = getMessageElementText(aElement);
+ let time = aRule.consoleTime;
+
+ if (!checkText(time + ": timer started", elemText)) {
+ return false;
+ }
+
+ aRule.category = CATEGORY_WEBDEV;
+ aRule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleTimeEnd(aRule, aElement)
+ {
+ let elemText = getMessageElementText(aElement);
+ let time = aRule.consoleTimeEnd;
+ let regex = new RegExp(time + ": -?\\d+ms");
+
+ if (!checkText(regex, elemText)) {
+ return false;
+ }
+
+ aRule.category = CATEGORY_WEBDEV;
+ aRule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleDir(aRule, aElement)
+ {
+ if (!aElement.classList.contains("webconsole-msg-inspector")) {
+ return false;
+ }
+
+ let elemText = getMessageElementText(aElement);
+ if (!checkText(aRule.consoleDir, elemText)) {
+ return false;
+ }
+
+ let iframe = aElement.querySelector("iframe");
+ if (!iframe) {
+ ok(false, "console.dir message has no iframe");
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkSource(aRule, aElement)
+ {
+ let location = aElement.querySelector(".webconsole-location");
+ if (!location) {
+ return false;
+ }
+
+ if (!checkText(aRule.source.url, location.getAttribute("title"))) {
+ return false;
+ }
+
+ if ("line" in aRule.source && location.sourceLine != aRule.source.line) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkMessage(aRule, aElement)
+ {
+ let elemText = getMessageElementText(aElement);
+
+ if (aRule.text && !checkText(aRule.text, elemText)) {
+ return false;
+ }
+
+ if (aRule.noText && checkText(aRule.noText, elemText)) {
+ return false;
+ }
+
+ if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) {
+ return false;
+ }
+
+ if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) {
+ return false;
+ }
+
+ if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) {
+ return false;
+ }
+
+ if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) {
+ return false;
+ }
+
+ if (aRule.source && !checkSource(aRule, aElement)) {
+ return false;
+ }
+
+ let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime ||
+ aRule.consoleTimeEnd);
+
+ if (aRule.category && aElement.category != aRule.category) {
+ if (partialMatch) {
+ is(aElement.category, aRule.category,
+ "message category for rule: " + displayRule(aRule));
+ displayErrorContext(aRule, aElement);
+ }
+ return false;
+ }
+
+ if (aRule.severity && aElement.severity != aRule.severity) {
+ if (partialMatch) {
+ is(aElement.severity, aRule.severity,
+ "message severity for rule: " + displayRule(aRule));
+ displayErrorContext(aRule, aElement);
+ }
+ return false;
+ }
+
+ if (aRule.repeats) {
+ let repeats = aElement.querySelector(".webconsole-msg-repeat");
+ if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
+ return false;
+ }
+ }
+
+ if ("longString" in aRule) {
+ let longStrings = aElement.querySelectorAll(".longStringEllipsis");
+ if (aRule.longString != !!longStrings[0]) {
+ if (partialMatch) {
+ is(!!longStrings[0], aRule.longString,
+ "long string existence check failed for message rule: " +
+ displayRule(aRule));
+ displayErrorContext(aRule, aElement);
+ }
+ return false;
+ }
+ aRule.longStrings = longStrings;
+ }
+
+ if ("objects" in aRule) {
+ let clickables = aElement.querySelectorAll(".hud-clickable");
+ if (aRule.objects != !!clickables[0]) {
+ if (partialMatch) {
+ is(!!clickables[0], aRule.objects,
+ "objects existence check failed for message rule: " +
+ displayRule(aRule));
+ displayErrorContext(aRule, aElement);
+ }
+ return false;
+ }
+ aRule.clickableElements = clickables;
+ }
+
+ let count = aRule.count || 1;
+ if (!aRule.matched) {
+ aRule.matched = new Set();
+ }
+ aRule.matched.add(aElement);
+
+ return aRule.matched.size == count;
+ }
+
+ function onMessagesAdded(aEvent, aNewElements)
+ {
+ for (let elem of aNewElements) {
+ let location = elem.querySelector(".webconsole-location");
+ if (location) {
+ let url = location.getAttribute("title");
+ // Prevent recursion with the browser console and any potential
+ // messages coming from head.js.
+ if (url.indexOf("browser/devtools/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 maybeDone()
+ {
+ if (rulesMatched == rules.length) {
+ if (listenerAdded) {
+ webconsole.ui.off("messages-added", onMessagesAdded);
+ webconsole.ui.off("messages-updated", onMessagesAdded);
+ }
+ gPendingOutputTest--;
+ deferred.resolve(rules);
+ return true;
+ }
+ return false;
+ }
+
+ function testCleanup() {
+ if (rulesMatched == rules.length) {
+ return;
+ }
+
+ if (webconsole.ui) {
+ webconsole.ui.off("messages-added", onMessagesAdded);
+ }
+
+ for (let rule of rules) {
+ if (!rule._ruleMatched) {
+ ok(false, "failed to match rule: " + displayRule(rule));
+ }
+ }
+ }
+
+ function displayRule(aRule)
+ {
+ return aRule.name || aRule.text;
+ }
+
+ function displayErrorContext(aRule, aElement)
+ {
+ console.log("error occured during rule " + displayRule(aRule));
+ console.log("while checking the following message");
+ dumpMessageElement(aElement);
+ }
+
+ executeSoon(() => {
+ onMessagesAdded("messages-added", webconsole.outputNode.childNodes);
+ if (rulesMatched != rules.length) {
+ listenerAdded = true;
+ registerCleanupFunction(testCleanup);
+ webconsole.ui.on("messages-added", onMessagesAdded);
+ webconsole.ui.on("messages-updated", onMessagesAdded);
+ }
+ });
+
+ return deferred.promise;
+}
+
+
+/**
+ * Scroll the Web Console output to the given node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to scroll to.
+ */
+function scrollOutputToNode(aNode)
+{
+ let richListBoxNode = aNode.parentNode;
+ while (richListBoxNode.tagName != "richlistbox") {
+ richListBoxNode = richListBoxNode.parentNode;
+ }
+
+ let boxObject = richListBoxNode.scrollBoxObject;
+ let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ nsIScrollBoxObject.ensureElementIsVisible(aNode);
+}
+
+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);
+}
diff --git a/browser/devtools/webconsole/test/moz.build b/browser/devtools/webconsole/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/webconsole/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; 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/browser/devtools/webconsole/test/test-bug-585956-console-trace.html b/browser/devtools/webconsole/test/test-bug-585956-console-trace.html
new file mode 100644
index 000000000..e658ba633
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html
new file mode 100644
index 000000000..ebf9c515f
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html
new file mode 100644
index 000000000..5b12278d1
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"></iframe>
+ </body>
+ </html>
diff --git a/browser/devtools/webconsole/test/test-bug-595934-canvas-css.html b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.html
new file mode 100644
index 000000000..3c9cf03a5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js
new file mode 100644
index 000000000..cc364d6a3
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js
@@ -0,0 +1,10 @@
+/*
+ * 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/browser/devtools/webconsole/test/test-bug-595934-canvas.html b/browser/devtools/webconsole/test/test-bug-595934-canvas.html
new file mode 100644
index 000000000..399b62098
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-canvas.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 595934 - category: 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.js"></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 595934 - category "Canvas".</p>
+ <p><canvas width="200" height="200">Canvas support is required!</canvas></p>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-bug-595934-canvas.js b/browser/devtools/webconsole/test/test-bug-595934-canvas.js
new file mode 100644
index 000000000..7d77d7696
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-canvas.js
@@ -0,0 +1,11 @@
+/*
+ * 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 = document;
+}, false);
+
diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-loader.css b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css
new file mode 100644
index 000000000..b4224430f
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^
new file mode 100644
index 000000000..e7be84a71
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^
@@ -0,0 +1 @@
+Content-Type: image/png
diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-loader.html b/browser/devtools/webconsole/test/test-bug-595934-css-loader.html
new file mode 100644
index 000000000..6bb0d54c5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-css-parser.css b/browser/devtools/webconsole/test/test-bug-595934-css-parser.css
new file mode 100644
index 000000000..f6db82398
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-css-parser.html b/browser/devtools/webconsole/test/test-bug-595934-css-parser.html
new file mode 100644
index 000000000..a4ea74ba3
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html
new file mode 100644
index 000000000..a70f9011b
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js
new file mode 100644
index 000000000..dd94d716d
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js
@@ -0,0 +1,8 @@
+/*
+ * 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/browser/devtools/webconsole/test/test-bug-595934-html.html b/browser/devtools/webconsole/test/test-bug-595934-html.html
new file mode 100644
index 000000000..fe35afef6
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-image.html b/browser/devtools/webconsole/test/test-bug-595934-image.html
new file mode 100644
index 000000000..312ecd49f
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-image.jpg b/browser/devtools/webconsole/test/test-bug-595934-image.jpg
new file mode 100644
index 000000000..947e5f11b
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-image.jpg
Binary files differ
diff --git a/browser/devtools/webconsole/test/test-bug-595934-imagemap.html b/browser/devtools/webconsole/test/test-bug-595934-imagemap.html
new file mode 100644
index 000000000..007c3c01b
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html
new file mode 100644
index 000000000..2fd8beac5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml
new file mode 100644
index 000000000..4812786f1
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml b/browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml
new file mode 100644
index 000000000..62689c567
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-svg.xhtml b/browser/devtools/webconsole/test/test-bug-595934-svg.xhtml
new file mode 100644
index 000000000..572382c64
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-workers.html b/browser/devtools/webconsole/test/test-bug-595934-workers.html
new file mode 100644
index 000000000..baf5a6215
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-595934-workers.js b/browser/devtools/webconsole/test/test-bug-595934-workers.js
new file mode 100644
index 000000000..4e93c967b
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-595934-workers.js
@@ -0,0 +1,9 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+onmessage = function() {
+ fooBarWorker();
+}
+
diff --git a/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html
new file mode 100644
index 000000000..25bdeecc5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js
new file mode 100644
index 000000000..87c0aff8e
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js
@@ -0,0 +1,14 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 ***** */
+
+function f() {
+ bogus.g();
+}
+
diff --git a/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html b/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html
new file mode 100644
index 000000000..68e19e677
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs b/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs
new file mode 100644
index 000000000..2e78d6b7b
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-600183-charset.html b/browser/devtools/webconsole/test/test-bug-600183-charset.html
new file mode 100644
index 000000000..040490a6b
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^ b/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^
new file mode 100644
index 000000000..9f3e2302f
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=gb2312
diff --git a/browser/devtools/webconsole/test/test-bug-601177-log-levels.html b/browser/devtools/webconsole/test/test-bug-601177-log-levels.html
new file mode 100644
index 000000000..a59213907
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-601177-log-levels.js b/browser/devtools/webconsole/test/test-bug-601177-log-levels.js
new file mode 100644
index 000000000..ea37f533d
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-601177-log-levels.js
@@ -0,0 +1,8 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+foobarBug601177strictError = "strict error";
+
+window.foobarBug601177exception();
diff --git a/browser/devtools/webconsole/test/test-bug-603750-websocket.html b/browser/devtools/webconsole/test/test-bug-603750-websocket.html
new file mode 100644
index 000000000..f0097dd77
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-603750-websocket.js b/browser/devtools/webconsole/test/test-bug-603750-websocket.js
new file mode 100644
index 000000000..3746424cc
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-603750-websocket.js
@@ -0,0 +1,18 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+window.addEventListener("load", function () {
+ var ws1 = new WebSocket("ws://0.0.0.0:81");
+ ws1.onopen = function() {
+ ws1.send("test 1");
+ ws1.close();
+ };
+
+ var 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/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html b/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html
new file mode 100644
index 000000000..edf40e80e
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html b/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html
new file mode 100644
index 000000000..ac755e1b9
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html b/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html
new file mode 100644
index 000000000..09c986703
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs b/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs
new file mode 100644
index 000000000..f92e0fe65
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-632275-getters.html b/browser/devtools/webconsole/test/test-bug-632275-getters.html
new file mode 100644
index 000000000..349c301f3
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html b/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html
new file mode 100644
index 000000000..ae6e5201c
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html
@@ -0,0 +1,54 @@
+<!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.gen1 = genFunc();
+gen1.next();
+
+var obj = { foo: "bar", baz: "baaz", hay: "stack" };
+window.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);
+}
+
+window.iter2 = new Range(3, 15);
+
+window.gen2 = (i * 2 for (i in iter2));
+})();
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 632347 - iterators and generators.</p>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-bug-644419-log-limits.html b/browser/devtools/webconsole/test/test-bug-644419-log-limits.html
new file mode 100644
index 000000000..21d99ba14
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-646025-console-file-location.html b/browser/devtools/webconsole/test/test-bug-646025-console-file-location.html
new file mode 100644
index 000000000..7c80f1446
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-658368-time-methods.html b/browser/devtools/webconsole/test/test-bug-658368-time-methods.html
new file mode 100644
index 000000000..cc50b6313
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html b/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-766001-console-log.js b/browser/devtools/webconsole/test/test-bug-766001-console-log.js
new file mode 100644
index 000000000..41594d76a
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-766001-console-log.js
@@ -0,0 +1,8 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+window.addEventListener("load", function() {
+ console.log("Blah Blah");
+}, false);
diff --git a/browser/devtools/webconsole/test/test-bug-766001-js-console-links.html b/browser/devtools/webconsole/test/test-bug-766001-js-console-links.html
new file mode 100644
index 000000000..6a6ac6008
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-766001-js-errors.js b/browser/devtools/webconsole/test/test-bug-766001-js-errors.js
new file mode 100644
index 000000000..932204395
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-766001-js-errors.js
@@ -0,0 +1,7 @@
+/*
+ * 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/browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css b/browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css
new file mode 100644
index 000000000..ad7fd1999
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css b/browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css
new file mode 100644
index 000000000..91b14137a
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-782653-css-errors.html b/browser/devtools/webconsole/test/test-bug-782653-css-errors.html
new file mode 100644
index 000000000..7ca11fc34
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-821877-csperrors.html b/browser/devtools/webconsole/test/test-bug-821877-csperrors.html
new file mode 100644
index 000000000..25d9da1c0
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-821877-csperrors.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 821877 - Log CSP Errors to Web Console</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>This page is served with a deprecated CSP header.</p>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-bug-821877-csperrors.html^headers^ b/browser/devtools/webconsole/test/test-bug-821877-csperrors.html^headers^
new file mode 100644
index 000000000..426d8738c
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-821877-csperrors.html^headers^
@@ -0,0 +1 @@
+X-Content-Security-Policy: default-src *; options inline-script
diff --git a/browser/devtools/webconsole/test/test-bug-837351-security-errors.html b/browser/devtools/webconsole/test/test-bug-837351-security-errors.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html b/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html
new file mode 100644
index 000000000..509e424d0
--- /dev/null
+++ b/browser/devtools/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(20000)).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/browser/devtools/webconsole/test/test-bug-869003-iframe.html b/browser/devtools/webconsole/test/test-bug-869003-iframe.html
new file mode 100644
index 000000000..5a29728e5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-869003-top-window.html b/browser/devtools/webconsole/test/test-bug-869003-top-window.html
new file mode 100644
index 000000000..ab3b87542
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-bug-869003-iframe.html"></iframe>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-console-extras.html b/browser/devtools/webconsole/test/test-console-extras.html
new file mode 100644
index 000000000..6e05a96eb
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-extras.html
@@ -0,0 +1,25 @@
+<!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.exception()
+ console.assert()
+ console.clear()
+ console.dirxml()
+ console.profile()
+ console.profileEnd()
+ console.count()
+ console.table()
+ 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/browser/devtools/webconsole/test/test-console-replaced-api.html b/browser/devtools/webconsole/test/test-console-replaced-api.html
new file mode 100644
index 000000000..2b05d023a
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-console.html b/browser/devtools/webconsole/test/test-console.html
new file mode 100644
index 000000000..23126642a
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console.html
@@ -0,0 +1,23 @@
+<!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">
+ function test() {
+ var str = "Dolske Digs Bacon, Now and Forevermore."
+ for (var i=0; i < 5; i++) {
+ console.log(str);
+ }
+ }
+ 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>
+ <div id="myDiv"></div>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-data.json b/browser/devtools/webconsole/test/test-data.json
new file mode 100644
index 000000000..471d240b5
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-data.json^headers^ b/browser/devtools/webconsole/test/test-data.json^headers^
new file mode 100644
index 000000000..7b5e82d4b
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-data.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json
diff --git a/browser/devtools/webconsole/test/test-duplicate-error.html b/browser/devtools/webconsole/test/test-duplicate-error.html
new file mode 100644
index 000000000..1b2691672
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html b/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html
new file mode 100644
index 000000000..cf19629f4
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-error.html b/browser/devtools/webconsole/test/test-error.html
new file mode 100644
index 000000000..abf62a3f1
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-eval-in-stackframe.html b/browser/devtools/webconsole/test/test-eval-in-stackframe.html
new file mode 100644
index 000000000..ec1bf3f30
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-file-location.js b/browser/devtools/webconsole/test/test-file-location.js
new file mode 100644
index 000000000..f97ce5725
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-file-location.js
@@ -0,0 +1,9 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+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/browser/devtools/webconsole/test/test-filter.html b/browser/devtools/webconsole/test/test-filter.html
new file mode 100644
index 000000000..219177bb2
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-for-of.html b/browser/devtools/webconsole/test/test-for-of.html
new file mode 100644
index 000000000..876010c9e
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-image.png b/browser/devtools/webconsole/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-image.png
Binary files differ
diff --git a/browser/devtools/webconsole/test/test-mutation.html b/browser/devtools/webconsole/test/test-mutation.html
new file mode 100644
index 000000000..e80933b06
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-network-request.html b/browser/devtools/webconsole/test/test-network-request.html
new file mode 100644
index 000000000..3cb699865
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-network-request.html
@@ -0,0 +1,36 @@
+<!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 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="http://example.com/browser/browser/devtools/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/browser/devtools/webconsole/test/test-network.html b/browser/devtools/webconsole/test/test-network.html
new file mode 100644
index 000000000..9c591479e
--- /dev/null
+++ b/browser/devtools/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"></script>
+ </head>
+ <body>
+ <h1>Heads Up Display Network Test Page</h1>
+ <img src="test-image.png"></img>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-observe-http-ajax.html b/browser/devtools/webconsole/test/test-observe-http-ajax.html
new file mode 100644
index 000000000..5abcefdad
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-own-console.html b/browser/devtools/webconsole/test/test-own-console.html
new file mode 100644
index 000000000..d1d18ebc2
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-property-provider.html b/browser/devtools/webconsole/test/test-property-provider.html
new file mode 100644
index 000000000..532b00f44
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test-repeated-messages.html b/browser/devtools/webconsole/test/test-repeated-messages.html
new file mode 100644
index 000000000..5bbc8fd5a
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-repeated-messages.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Test for bugs 720180, 800510 and 865288</title>
+ <script>
+ function testConsole() {
+ console.log("foo repeat"); 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);
+ }
+ }
+ </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/browser/devtools/webconsole/test/test-result-format-as-string.html b/browser/devtools/webconsole/test/test-result-format-as-string.html
new file mode 100644
index 000000000..73b7b8be5
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-result-format-as-string.html
@@ -0,0 +1,24 @@
+<!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.textContent = "bug772506_content";
+ element.setAttribute("onmousemove",
+ "(function () {" +
+ " gBrowser._bug772506 = 'foobar';" +
+ "})();"
+ );
+ return element;
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/devtools/webconsole/test/test-webconsole-error-observer.html b/browser/devtools/webconsole/test/test-webconsole-error-observer.html
new file mode 100644
index 000000000..8466bc6f2
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html b/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html
new file mode 100644
index 000000000..eb0c52c3a
--- /dev/null
+++ b/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for Bug 770099 - bad policy-uri</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>
+</body>
+</html>
diff --git a/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html^headers^ b/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html^headers^
new file mode 100644
index 000000000..b64692028
--- /dev/null
+++ b/browser/devtools/webconsole/test/test_bug_770099_bad_policy_uri.html^headers^
@@ -0,0 +1,2 @@
+X-Content-Security-Policy: policy-uri http://example.com/some_policy
+Content-type: text/html; charset=utf-8
diff --git a/browser/devtools/webconsole/test/test_bug_770099_violation.html b/browser/devtools/webconsole/test/test_bug_770099_violation.html
new file mode 100644
index 000000000..ccbded87a
--- /dev/null
+++ b/browser/devtools/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/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^ b/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^
new file mode 100644
index 000000000..5374efd3e
--- /dev/null
+++ b/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^
@@ -0,0 +1 @@
+X-Content-Security-Policy: default-src 'self'
diff --git a/browser/devtools/webconsole/test/testscript.js b/browser/devtools/webconsole/test/testscript.js
new file mode 100644
index 000000000..c69919df4
--- /dev/null
+++ b/browser/devtools/webconsole/test/testscript.js
@@ -0,0 +1 @@
+console.log("running network console logging tests");
diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js
new file mode 100644
index 000000000..e0ccc5a77
--- /dev/null
+++ b/browser/devtools/webconsole/webconsole.js
@@ -0,0 +1,5114 @@
+/* -*- Mode: js2; js2-basic-offset: 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+XPCOMUtils.defineLazyModuleGetter(this, "GripClient",
+ "resource://gre/modules/devtools/dbg-client.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel",
+ "resource:///modules/NetworkPanel.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
+ "resource:///modules/devtools/AutocompletePopup.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource:///modules/devtools/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+ "resource:///modules/devtools/VariablesViewController.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+
+// The XUL namespace.
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent";
+
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
+
+const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
+
+const CONSOLE_DIR_VIEW_HEIGHT = 0.6;
+
+const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
+
+// The amount of time in milliseconds that must pass between messages to
+// trigger the display of a new group.
+const NEW_GROUP_DELAY = 5000;
+
+// 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 = 200;
+
+// 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;
+const CATEGORY_INPUT = 4; // always on
+const CATEGORY_OUTPUT = 5; // always on
+const CATEGORY_SECURITY = 6;
+
+// 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",
+];
+
+// 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).
+//
+// Most of these rather idiosyncratic names are historical and predate the
+// division of message type into "category" and "severity".
+const MESSAGE_PREFERENCE_KEYS = [
+// Error Warning Info Log
+ [ "network", null, 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
+];
+
+// A mapping from the console API log event levels to the Web Console
+// severities.
+const LEVELS = {
+ error: SEVERITY_ERROR,
+ warn: SEVERITY_WARNING,
+ info: SEVERITY_INFO,
+ log: SEVERITY_LOG,
+ trace: SEVERITY_LOG,
+ debug: SEVERITY_LOG,
+ dir: SEVERITY_LOG,
+ group: SEVERITY_LOG,
+ groupCollapsed: SEVERITY_LOG,
+ groupEnd: SEVERITY_LOG,
+ time: SEVERITY_LOG,
+ timeEnd: SEVERITY_LOG
+};
+
+// 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;
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+// 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 the Firefox UI too much.
+const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
+
+// The delay 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 = 50; // milliseconds
+
+// 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; // milliseconds
+
+// 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;
+
+// The maximum length of strings to be displayed by the Web Console.
+const MAX_LONG_STRING_LENGTH = 200000;
+
+const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
+const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
+
+/**
+ * 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.
+ *
+ * @param object aWebConsoleOwner
+ * The WebConsole owner object.
+ */
+function WebConsoleFrame(aWebConsoleOwner)
+{
+ this.owner = aWebConsoleOwner;
+ this.hudId = this.owner.hudId;
+
+ this._repeatNodes = {};
+ this._outputQueue = [];
+ this._pruneCategoriesQueue = {};
+ this._networkRequests = {};
+ this.filterPrefs = {};
+
+ this._toggleFilter = this._toggleFilter.bind(this);
+ this._flushMessageQueue = this._flushMessageQueue.bind(this);
+
+ this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._outputTimerInitialized = false;
+
+ EventEmitter.decorate(this);
+}
+
+WebConsoleFrame.prototype = {
+ /**
+ * The WebConsole instance that owns this frame.
+ * @see HUDService.jsm::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() this.owner.mainPopupSet,
+
+ /**
+ * Holds the initialization Promise object.
+ * @private
+ * @type object
+ */
+ _initDefer: 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,
+
+ /**
+ * 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 input element that allows the user to filter messages by string.
+ * @type nsIDOMElement
+ */
+ filterBox: null,
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null,
+
+ _destroyer: null,
+
+ // Used in tests.
+ _saveRequestAndResponseBodies: false,
+
+ /**
+ * Tells whether to save the bodies of network requests and responses.
+ * Disabled by default to save memory.
+ *
+ * @return boolean
+ * The saveRequestAndResponseBodies pref value.
+ */
+ getSaveRequestAndResponseBodies:
+ function WCF_getSaveRequestAndResponseBodies() {
+ let deferred = Promise.defer();
+ let toGet = [
+ "NetworkMonitor.saveRequestAndResponseBodies"
+ ];
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.getPreferences(toGet, aResponse => {
+ if (!aResponse.error) {
+ this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]];
+ deferred.resolve(this._saveRequestAndResponseBodies);
+ }
+ else {
+ deferred.reject(aResponse.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Setter for saving of network request and response bodies.
+ *
+ * @param boolean aValue
+ * The new value you want to set.
+ */
+ setSaveRequestAndResponseBodies:
+ function WCF_setSaveRequestAndResponseBodies(aValue) {
+ let deferred = Promise.defer();
+ let newValue = !!aValue;
+ let toSet = {
+ "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, aResponse => {
+ if (!aResponse.error) {
+ this._saveRequestAndResponseBodies = newValue;
+ deferred.resolve(aResponse);
+ }
+ else {
+ deferred.reject(aResponse.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ _persistLog: null,
+
+ /**
+ * Getter for the persistent logging preference. This value is cached per
+ * instance to avoid reading the pref too often.
+ * @type boolean
+ */
+ get persistLog() {
+ if (this._persistLog === null) {
+ this._persistLog = Services.prefs.getBoolPref(PREF_PERSISTLOG);
+ }
+ return this._persistLog;
+ },
+
+ /**
+ * Initialize the WebConsoleFrame instance.
+ * @return object
+ * A Promise object for the initialization.
+ */
+ init: function WCF_init()
+ {
+ this._initUI();
+ return this._initConnection();
+ },
+
+ /**
+ * 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 WCF__initConnection()
+ {
+ 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);
+ }, (aReason) => { // on failure
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
+ aReason.error + ": " + aReason.message);
+ this.outputMessage(CATEGORY_JS, node);
+ this._initDefer.reject(aReason);
+ }).then(() => {
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created", null);
+ });
+
+ return this._initDefer.promise;
+ },
+
+ /**
+ * Find the Web Console UI elements and setup event listeners as needed.
+ * @private
+ */
+ _initUI: function WCF__initUI()
+ {
+ // Remember that this script is loaded in the webconsole.xul context:
+ // |window| is the iframe global.
+ this.window = window;
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+
+ this._initDefaultFilterPrefs();
+
+ // Register the controller to handle "select all" properly.
+ this._commandController = new CommandController(this);
+ this.window.controllers.insertControllerAt(0, this._commandController);
+
+ let doc = this.document;
+
+ this.filterBox = doc.querySelector(".hud-filter-box");
+ this.outputNode = doc.querySelector(".hud-output-node");
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+
+ this._setFilterTextBoxEvents();
+ this._initFilterButtons();
+
+ let fontSize = Services.prefs.getIntPref("devtools.webconsole.fontSize");
+
+ if (fontSize != 0) {
+ fontSize = Math.max(MIN_FONT_SIZE, fontSize);
+
+ this.outputNode.style.fontSize = fontSize + "px";
+ this.completeNode.style.fontSize = fontSize + "px";
+ this.inputNode.style.fontSize = fontSize + "px";
+ }
+
+ let updateSaveBodiesPrefUI = (aElement) => {
+ this.getSaveRequestAndResponseBodies().then(aValue => {
+ aElement.setAttribute("checked", aValue);
+ this.emit("save-bodies-ui-toggled");
+ });
+ }
+
+ let reverseSaveBodiesPref = ({ target: aElement }) => {
+ this.getSaveRequestAndResponseBodies().then(aValue => {
+ this.setSaveRequestAndResponseBodies(!aValue);
+ aElement.setAttribute("checked", aValue);
+ this.emit("save-bodies-pref-reversed");
+ });
+ }
+
+ let saveBodies = doc.getElementById("saveBodies");
+ saveBodies.addEventListener("click", reverseSaveBodiesPref);
+ saveBodies.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("network");
+
+ let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
+ saveBodiesContextMenu.addEventListener("click", reverseSaveBodiesPref);
+ saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("network");
+
+ saveBodies.parentNode.addEventListener("popupshowing", () => {
+ updateSaveBodiesPrefUI(saveBodies);
+ saveBodies.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("network");
+ });
+
+ saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => {
+ updateSaveBodiesPrefUI(saveBodiesContextMenu);
+ saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("network");
+ });
+
+ let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0];
+ clearButton.addEventListener("command", () => {
+ this.owner._onClearButton();
+ this.jsterm.clearOutput(true);
+ });
+
+ this.jsterm = new JSTerm(this);
+ this.jsterm.init();
+ this.jsterm.inputNode.focus();
+ },
+
+ /**
+ * Initialize the default filter preferences.
+ * @private
+ */
+ _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
+ {
+ let prefs = ["network", "networkinfo", "csserror", "cssparser", "exception",
+ "jswarn", "jslog", "error", "info", "warn", "log", "secerror",
+ "secwarn"];
+ for (let pref of prefs) {
+ this.filterPrefs[pref] = Services.prefs
+ .getBoolPref(this._filterPrefsPrefix + pref);
+ }
+ },
+
+ /**
+ * Sets the events for the filter input field.
+ * @private
+ */
+ _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
+ {
+ 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 WCF__initFilterButtons()
+ {
+ let categories = this.document
+ .querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(categories, function(aButton) {
+ aButton.addEventListener("click", this._toggleFilter, false);
+
+ let someChecked = false;
+ let severities = aButton.querySelectorAll("menuitem[prefKey]");
+ Array.forEach(severities, function(aMenuItem) {
+ aMenuItem.addEventListener("command", this._toggleFilter, false);
+
+ let prefKey = aMenuItem.getAttribute("prefKey");
+ let checked = this.filterPrefs[prefKey];
+ aMenuItem.setAttribute("checked", checked);
+ someChecked = someChecked || checked;
+ }, this);
+
+ aButton.setAttribute("checked", someChecked);
+ }, this);
+
+ if (!this.owner._browserConsole) {
+ // 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;
+ }
+ },
+
+ /**
+ * Increase, decrease or reset the font size.
+ *
+ * @param string size
+ * The size of the font change. Accepted values are "+" and "-".
+ * An unmatched size assumes a font reset.
+ */
+ changeFontSize: function WCF_changeFontSize(aSize)
+ {
+ let fontSize = this.window
+ .getComputedStyle(this.outputNode, null)
+ .getPropertyValue("font-size").replace("px", "");
+
+ if (this.outputNode.style.fontSize) {
+ fontSize = this.outputNode.style.fontSize.replace("px", "");
+ }
+
+ if (aSize == "+" || aSize == "-") {
+ fontSize = parseInt(fontSize, 10);
+
+ if (aSize == "+") {
+ fontSize += 1;
+ }
+ else {
+ fontSize -= 1;
+ }
+
+ if (fontSize < MIN_FONT_SIZE) {
+ fontSize = MIN_FONT_SIZE;
+ }
+
+ Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize);
+ fontSize = fontSize + "px";
+
+ this.completeNode.style.fontSize = fontSize;
+ this.inputNode.style.fontSize = fontSize;
+ this.outputNode.style.fontSize = fontSize;
+ }
+ else {
+ this.completeNode.style.fontSize = "";
+ this.inputNode.style.fontSize = "";
+ this.outputNode.style.fontSize = "";
+ Services.prefs.clearUserPref("devtools.webconsole.fontSize");
+ }
+ },
+
+ /**
+ * The event handler that is called whenever a user switches a filter on or
+ * off.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ * The event that triggered the filter change.
+ */
+ _toggleFilter: function WCF__toggleFilter(aEvent)
+ {
+ let target = aEvent.target;
+ let tagName = target.tagName;
+ if (tagName != aEvent.currentTarget.tagName) {
+ return;
+ }
+
+ switch (tagName) {
+ case "toolbarbutton": {
+ let originalTarget = aEvent.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;
+ }
+
+ let state = target.getAttribute("checked") !== "true";
+ target.setAttribute("checked", 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.
+ let menuItems = target.querySelectorAll("menuitem");
+ for (let i = 0; i < menuItems.length; i++) {
+ menuItems[i].setAttribute("checked", state);
+ let prefKey = menuItems[i].getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+ }
+ break;
+ }
+
+ case "menuitem": {
+ let state = target.getAttribute("checked") !== "true";
+ target.setAttribute("checked", state);
+
+ let prefKey = target.getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+
+ // Disable the log response and request body if network logging is off.
+ if (prefKey == "networkinfo" || prefKey == "network") {
+ let checkState = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("network");
+ this.document.getElementById("saveBodies").disabled = checkState;
+ this.document.getElementById("saveBodiesContextMenu").disabled = checkState;
+ }
+
+ // 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);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Set the filter state for a specific toggle button.
+ *
+ * @param string aToggleType
+ * @param boolean aState
+ * @returns void
+ */
+ setFilterState: function WCF_setFilterState(aToggleType, aState)
+ {
+ this.filterPrefs[aToggleType] = aState;
+ this.adjustVisibilityForMessageType(aToggleType, aState);
+ Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
+ },
+
+ /**
+ * Get the filter state for a specific toggle button.
+ *
+ * @param string aToggleType
+ * @returns boolean
+ */
+ getFilterState: function WCF_getFilterState(aToggleType)
+ {
+ return this.filterPrefs[aToggleType];
+ },
+
+ /**
+ * Check that the passed string matches the filter arguments.
+ *
+ * @param String aString
+ * to search for filter words in.
+ * @param String aFilter
+ * is a string containing all of the words to filter on.
+ * @returns boolean
+ */
+ stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter)
+ {
+ if (!aFilter || !aString) {
+ return true;
+ }
+
+ let searchStr = aString.toLowerCase();
+ let filterStrings = aFilter.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 @aPrefKey.
+ *
+ * @param string aPrefKey
+ * The preference key for the message type being filtered: one of the
+ * values in the MESSAGE_PREFERENCE_KEYS table.
+ * @param boolean aState
+ * True if the filter named by @aMessageType is being turned on; false
+ * otherwise.
+ * @returns void
+ */
+ adjustVisibilityForMessageType:
+ function WCF_adjustVisibilityForMessageType(aPrefKey, aState)
+ {
+ let outputNode = this.outputNode;
+ let doc = this.document;
+
+ // Look for message nodes ("hud-msg-node") with the given preference key
+ // ("hud-msg-error", "hud-msg-cssparser", etc.) and add or remove the
+ // "hud-filtered-by-type" class, which turns on or off the display.
+
+ let xpath = ".//*[contains(@class, 'hud-msg-node') and " +
+ "contains(concat(@class, ' '), 'hud-" + aPrefKey + " ')]";
+ 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 (aState) {
+ node.classList.remove("hud-filtered-by-type");
+ }
+ else {
+ node.classList.add("hud-filtered-by-type");
+ }
+ }
+
+ this.regroupOutput();
+ },
+
+ /**
+ * Turns the display of log nodes on and off appropriately to reflect the
+ * adjustment of the search string.
+ */
+ adjustVisibilityOnSearchStringChange:
+ function WCF_adjustVisibilityOnSearchStringChange()
+ {
+ let nodes = this.outputNode.getElementsByClassName("hud-msg-node");
+ 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.clipboardText;
+
+ // if the text matches the words in aSearchString...
+ if (this.stringMatchesFilters(text, searchString)) {
+ node.classList.remove("hud-filtered-by-string");
+ }
+ else {
+ node.classList.add("hud-filtered-by-string");
+ }
+ }
+
+ this.regroupOutput();
+ },
+
+ /**
+ * Applies the user's filters to a newly-created message node via CSS
+ * classes.
+ *
+ * @param nsIDOMNode aNode
+ * The newly-created message node.
+ * @return boolean
+ * True if the message was filtered or false otherwise.
+ */
+ filterMessageNode: function WCF_filterMessageNode(aNode)
+ {
+ let isFiltered = false;
+
+ // Filter by the message type.
+ let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
+ if (prefKey && !this.getFilterState(prefKey)) {
+ // The node is filtered by type.
+ aNode.classList.add("hud-filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter on the search string.
+ let search = this.filterBox.value;
+ let text = aNode.clipboardText;
+
+ // if string matches the filter text
+ if (!this.stringMatchesFilters(text, search)) {
+ aNode.classList.add("hud-filtered-by-string");
+ isFiltered = true;
+ }
+
+ if (isFiltered && aNode.classList.contains("webconsole-msg-inspector")) {
+ aNode.classList.add("hidden-message");
+ }
+
+ return isFiltered;
+ },
+
+ /**
+ * Merge the attributes of the two nodes that are about to be filtered.
+ * Increment the number of repeats of aOriginal.
+ *
+ * @param nsIDOMNode aOriginal
+ * The Original Node. The one being merged into.
+ * @param nsIDOMNode aFiltered
+ * The node being filtered out because it is repeated.
+ */
+ mergeFilteredMessageNode:
+ function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
+ {
+ // childNodes[3].firstChild is the node containing the number of repetitions
+ // of a node.
+ let repeatNode = aOriginal.childNodes[3].firstChild;
+ if (!repeatNode) {
+ return; // no repeat node, return early.
+ }
+
+ let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
+ repeatNode.setAttribute("value", occurrences);
+ },
+
+ /**
+ * Filter the message node from the output if it is a repeat.
+ *
+ * @private
+ * @param nsIDOMNode aNode
+ * The message node to be filtered or not.
+ * @returns nsIDOMNode|null
+ * Returns the duplicate node if the message was filtered, null
+ * otherwise.
+ */
+ _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode)
+ {
+ let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0];
+ if (!repeatNode) {
+ return null;
+ }
+
+ let uid = repeatNode._uid;
+ let dupeNode = null;
+
+ if (aNode.classList.contains("webconsole-msg-cssparser") ||
+ aNode.classList.contains("webconsole-msg-security")) {
+ dupeNode = this._repeatNodes[uid];
+ if (!dupeNode) {
+ this._repeatNodes[uid] = aNode;
+ }
+ }
+ else if (!aNode.classList.contains("webconsole-msg-network") &&
+ !aNode.classList.contains("webconsole-msg-inspector") &&
+ (aNode.classList.contains("webconsole-msg-console") ||
+ aNode.classList.contains("webconsole-msg-exception") ||
+ aNode.classList.contains("webconsole-msg-error"))) {
+ let lastMessage = this.outputNode.lastChild;
+ if (!lastMessage) {
+ return null;
+ }
+
+ let lastRepeatNode = lastMessage
+ .getElementsByClassName("webconsole-msg-repeat")[0];
+ if (lastRepeatNode && lastRepeatNode._uid == uid) {
+ dupeNode = lastMessage;
+ }
+ }
+
+ if (dupeNode) {
+ this.mergeFilteredMessageNode(dupeNode, aNode);
+ return dupeNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Display cached messages that may have been collected before the UI is
+ * displayed.
+ *
+ * @param array aRemoteMessages
+ * Array of cached messages coming from the remote Web Console
+ * content instance.
+ */
+ displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages)
+ {
+ if (!aRemoteMessages.length) {
+ return;
+ }
+
+ aRemoteMessages.forEach(function(aMessage) {
+ switch (aMessage._type) {
+ case "PageError": {
+ let category = Utils.categoryForScriptError(aMessage);
+ this.outputMessage(category, this.reportPageError,
+ [category, aMessage]);
+ break;
+ }
+ case "LogMessage":
+ this.handleLogMessage(aMessage);
+ break;
+ case "ConsoleAPI":
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
+ [aMessage]);
+ break;
+ }
+ }, this);
+ },
+
+ /**
+ * Logs a message to the Web Console that originates from the Web Console
+ * server.
+ *
+ * @param object aMessage
+ * The message received from the server.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage)
+ {
+ let body = null;
+ let clipboardText = null;
+ let sourceURL = aMessage.filename;
+ let sourceLine = aMessage.lineNumber;
+ let level = aMessage.level;
+ let args = aMessage.arguments;
+ let objectActors = new Set();
+
+ // Gather the actor IDs.
+ args.forEach((aValue) => {
+ if (WebConsoleUtils.isActorGrip(aValue)) {
+ objectActors.add(aValue.actor);
+ }
+ });
+
+ switch (level) {
+ case "log":
+ case "info":
+ case "warn":
+ case "error":
+ case "debug":
+ case "dir": {
+ body = { arguments: args };
+ let clipboardArray = [];
+ args.forEach((aValue) => {
+ clipboardArray.push(VariablesView.getString(aValue));
+ if (aValue && typeof aValue == "object" &&
+ aValue.type == "longString") {
+ clipboardArray.push(l10n.getStr("longStringEllipsis"));
+ }
+ });
+ clipboardText = clipboardArray.join(" ");
+ break;
+ }
+
+ case "trace": {
+ let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename);
+ let functionName = aMessage.functionName ||
+ l10n.getStr("stacktrace.anonymousFunction");
+
+ body = l10n.getFormatStr("stacktrace.outputMessage",
+ [filename, functionName, sourceLine]);
+
+ clipboardText = "";
+
+ aMessage.stacktrace.forEach(function(aFrame) {
+ clipboardText += aFrame.filename + " :: " +
+ aFrame.functionName + " :: " +
+ aFrame.lineNumber + "\n";
+ });
+
+ clipboardText = clipboardText.trimRight();
+ break;
+ }
+
+ case "group":
+ case "groupCollapsed":
+ clipboardText = body = aMessage.groupName;
+ this.groupDepth++;
+ break;
+
+ case "groupEnd":
+ if (this.groupDepth > 0) {
+ this.groupDepth--;
+ }
+ break;
+
+ case "time": {
+ let timer = aMessage.timer;
+ if (!timer) {
+ return null;
+ }
+ if (timer.error) {
+ Cu.reportError(l10n.getStr(timer.error));
+ return null;
+ }
+ body = l10n.getFormatStr("timerStarted", [timer.name]);
+ clipboardText = body;
+ break;
+ }
+
+ case "timeEnd": {
+ let timer = aMessage.timer;
+ if (!timer) {
+ return null;
+ }
+ let duration = Math.round(timer.duration * 100) / 100;
+ body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
+ clipboardText = body;
+ break;
+ }
+
+ default:
+ Cu.reportError("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 "trace":
+ case "time":
+ case "timeEnd":
+ for (let actor of objectActors) {
+ this._releaseObject(actor);
+ }
+ objectActors.clear();
+ }
+
+ if (level == "groupEnd") {
+ return null; // no need to continue
+ }
+
+ let node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
+ sourceURL, sourceLine, clipboardText,
+ level, aMessage.timeStamp);
+ if (aMessage.private) {
+ node.setAttribute("private", true);
+ }
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+
+ let repeatNode = node.querySelector(".webconsole-msg-repeat");
+ repeatNode._uid += [...objectActors].join("-");
+ }
+
+ // Make the node bring up the variables view, to allow the user to inspect
+ // the stack trace.
+ if (level == "trace") {
+ node._stacktrace = aMessage.stacktrace;
+
+ this.makeOutputMessageLink(node, () =>
+ this.jsterm.openVariablesView({
+ rawObject: node._stacktrace,
+ autofocus: true,
+ }));
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle ConsoleAPICall objects received from the server. This method outputs
+ * the window.console API call.
+ *
+ * @param object aMessage
+ * The console API message received from the server.
+ */
+ handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage)
+ {
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
+ },
+
+ /**
+ * The click event handler for objects shown inline coming from the
+ * window.console API.
+ *
+ * @private
+ * @param nsIDOMNode aAnchor
+ * The object inspector anchor element. This is the clickable element
+ * in the console.log message we display.
+ * @param object aObjectActor
+ * The object actor grip.
+ */
+ _consoleLogClick: function WCF__consoleLogClick(aAnchor, aObjectActor)
+ {
+ this.jsterm.openVariablesView({
+ label: aAnchor.textContent,
+ objectActor: aObjectActor,
+ autofocus: true,
+ });
+ },
+
+ /**
+ * Reports an error in the page source, either JavaScript or CSS.
+ *
+ * @param nsIScriptError aScriptError
+ * The error message to report.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ reportPageError: function WCF_reportPageError(aCategory, aScriptError)
+ {
+ // Warnings and legacy strict errors become warnings; other types become
+ // errors.
+ let severity = SEVERITY_ERROR;
+ if (aScriptError.warning || aScriptError.strict) {
+ severity = SEVERITY_WARNING;
+ }
+
+ let objectActors = new Set();
+
+ // Gather the actor IDs.
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = aScriptError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ objectActors.add(grip.actor);
+ }
+ }
+
+ let errorMessage = aScriptError.errorMessage;
+ if (errorMessage.type && errorMessage.type == "longString") {
+ errorMessage = errorMessage.initial;
+ }
+
+ let node = this.createMessageNode(aCategory, severity,
+ errorMessage,
+ aScriptError.sourceName,
+ aScriptError.lineNumber, null, null,
+ aScriptError.timeStamp);
+ if (aScriptError.private) {
+ node.setAttribute("private", 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 aPageError
+ * The error received from the server.
+ */
+ handlePageError: function WCF_handlePageError(aPageError)
+ {
+ let category = Utils.categoryForScriptError(aPageError);
+ this.outputMessage(category, this.reportPageError, [category, aPageError]);
+ },
+
+ /**
+ * Handle log messages received from the server. This method outputs the given
+ * message.
+ *
+ * @param object aPacket
+ * The message packet received from the server.
+ */
+ handleLogMessage: function WCF_handleLogMessage(aPacket)
+ {
+ if (aPacket.message) {
+ this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]);
+ }
+ },
+
+ /**
+ * Display log messages received from the server.
+ *
+ * @private
+ * @param object aPacket
+ * The message packet received from the server.
+ * @return nsIDOMElement
+ * The message element to render for the given log message.
+ */
+ _reportLogMessage: function WCF__reportLogMessage(aPacket)
+ {
+ let msg = aPacket.message;
+ if (msg.type && msg.type == "longString") {
+ msg = msg.initial;
+ }
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
+ null, null, null, aPacket.timeStamp);
+ if (WebConsoleUtils.isActorGrip(aPacket.message)) {
+ node._objectActors = new Set([aPacket.message.actor]);
+ }
+ return node;
+ },
+
+ /**
+ * Log network event.
+ *
+ * @param object aActorId
+ * The network event actor ID to log.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logNetEvent: function WCF_logNetEvent(aActorId)
+ {
+ let networkInfo = this._networkRequests[aActorId];
+ if (!networkInfo) {
+ return null;
+ }
+
+ let request = networkInfo.request;
+
+ let msgNode = this.document.createElementNS(XUL_NS, "hbox");
+
+ let methodNode = this.document.createElementNS(XUL_NS, "label");
+ methodNode.setAttribute("value", request.method);
+ methodNode.classList.add("webconsole-msg-body-piece");
+ msgNode.appendChild(methodNode);
+
+ let linkNode = this.document.createElementNS(XUL_NS, "hbox");
+ linkNode.flex = 1;
+ linkNode.classList.add("webconsole-msg-body-piece");
+ linkNode.classList.add("webconsole-msg-link");
+ msgNode.appendChild(linkNode);
+
+ let urlNode = this.document.createElementNS(XUL_NS, "label");
+ urlNode.flex = 1;
+ urlNode.setAttribute("crop", "center");
+ urlNode.setAttribute("title", request.url);
+ urlNode.setAttribute("tooltiptext", request.url);
+ urlNode.setAttribute("value", request.url);
+ urlNode.classList.add("hud-clickable");
+ urlNode.classList.add("webconsole-msg-body-piece");
+ urlNode.classList.add("webconsole-msg-url");
+ linkNode.appendChild(urlNode);
+
+ let severity = SEVERITY_LOG;
+ let mixedRequest =
+ WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
+ if (mixedRequest) {
+ urlNode.classList.add("webconsole-mixed-content");
+ this.makeMixedContentNode(linkNode);
+ // If we define a SEVERITY_SECURITY in the future, switch this to
+ // SEVERITY_SECURITY.
+ severity = SEVERITY_WARNING;
+ }
+
+ let statusNode = this.document.createElementNS(XUL_NS, "label");
+ statusNode.setAttribute("value", "");
+ statusNode.classList.add("hud-clickable");
+ statusNode.classList.add("webconsole-msg-body-piece");
+ statusNode.classList.add("webconsole-msg-status");
+ linkNode.appendChild(statusNode);
+
+ let clipboardText = request.method + " " + request.url;
+
+ let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
+ msgNode, null, null, clipboardText);
+ if (networkInfo.private) {
+ messageNode.setAttribute("private", true);
+ }
+
+ messageNode._connectionId = aActorId;
+ messageNode.url = request.url;
+
+ this.makeOutputMessageLink(messageNode, function WCF_net_message_link() {
+ if (!messageNode._panelOpen) {
+ this.openNetworkPanel(messageNode, networkInfo);
+ }
+ }.bind(this));
+
+ networkInfo.node = messageNode;
+
+ this._updateNetMessage(aActorId);
+
+ return messageNode;
+ },
+
+ /**
+ * Create a mixed content warning Node.
+ *
+ * @param aLinkNode
+ * Parent to the requested urlNode.
+ */
+ makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
+ {
+ let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
+
+ // Mixed content warning message links to a Learn More page
+ let mixedContentWarningNode = this.document.createElement("label");
+ mixedContentWarningNode.setAttribute("value", mixedContentWarning);
+ mixedContentWarningNode.setAttribute("title", mixedContentWarning);
+ mixedContentWarningNode.classList.add("hud-clickable");
+ mixedContentWarningNode.classList.add("webconsole-mixed-content-link");
+
+ aLinkNode.appendChild(mixedContentWarningNode);
+
+ mixedContentWarningNode.addEventListener("click", function(aEvent) {
+ this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }.bind(this));
+ },
+
+ /**
+ * Log file activity.
+ *
+ * @param string aFileURI
+ * The file URI that was loaded.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ logFileActivity: function WCF_logFileActivity(aFileURI)
+ {
+ let urlNode = this.document.createElementNS(XUL_NS, "label");
+ urlNode.flex = 1;
+ urlNode.setAttribute("crop", "center");
+ urlNode.setAttribute("title", aFileURI);
+ urlNode.setAttribute("tooltiptext", aFileURI);
+ urlNode.setAttribute("value", aFileURI);
+ urlNode.classList.add("hud-clickable");
+ urlNode.classList.add("webconsole-msg-url");
+
+ let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
+ urlNode, null, null, aFileURI);
+
+ this.makeOutputMessageLink(outputNode, function WCF__onFileClick() {
+ this.owner.viewSource(aFileURI);
+ }.bind(this));
+
+ return outputNode;
+ },
+
+ /**
+ * Handle the file activity messages coming from the remote Web Console.
+ *
+ * @param string aFileURI
+ * The file URI that was requested.
+ */
+ handleFileActivity: function WCF_handleFileActivity(aFileURI)
+ {
+ this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
+ },
+
+ /**
+ * Inform user that the window.console API has been replaced by a script
+ * in a content page.
+ */
+ logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
+ {
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
+ l10n.getStr("ConsoleAPIDisabled"));
+ this.outputMessage(CATEGORY_JS, node);
+ },
+
+ /**
+ * Inform user that the string he tries to view is too long.
+ */
+ logWarningAboutStringTooLong: function WCF_logWarningAboutStringTooLong()
+ {
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
+ l10n.getStr("longStringTooLong"));
+ this.outputMessage(CATEGORY_JS, node);
+ },
+
+ /**
+ * Handle the network events coming from the remote Web Console.
+ *
+ * @param object aActor
+ * The NetworkEventActor grip.
+ */
+ handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
+ {
+ let networkInfo = {
+ node: null,
+ actor: aActor.actor,
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: aActor.startedDateTime,
+ request: {
+ url: aActor.url,
+ method: aActor.method,
+ },
+ response: {},
+ timings: {},
+ updates: [], // track the list of network event updates
+ private: aActor.private,
+ };
+
+ this._networkRequests[aActor.actor] = networkInfo;
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]);
+ },
+
+ /**
+ * Handle network event updates coming from the server.
+ *
+ * @param string aActorId
+ * The network event actor ID.
+ * @param string aType
+ * Update type.
+ * @param object aPacket
+ * Update details.
+ */
+ handleNetworkEventUpdate:
+ function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
+ {
+ let networkInfo = this._networkRequests[aActorId];
+ if (!networkInfo) {
+ return;
+ }
+
+ networkInfo.updates.push(aType);
+
+ switch (aType) {
+ case "requestHeaders":
+ networkInfo.request.headersSize = aPacket.headersSize;
+ break;
+ case "requestPostData":
+ networkInfo.discardRequestBody = aPacket.discardRequestBody;
+ networkInfo.request.bodySize = aPacket.dataSize;
+ break;
+ case "responseStart":
+ networkInfo.response.httpVersion = aPacket.response.httpVersion;
+ networkInfo.response.status = aPacket.response.status;
+ networkInfo.response.statusText = aPacket.response.statusText;
+ networkInfo.response.headersSize = aPacket.response.headersSize;
+ networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
+ break;
+ case "responseContent":
+ networkInfo.response.content = {
+ mimeType: aPacket.mimeType,
+ };
+ networkInfo.response.bodySize = aPacket.contentSize;
+ networkInfo.discardResponseBody = aPacket.discardResponseBody;
+ break;
+ case "eventTimings":
+ networkInfo.totalTime = aPacket.totalTime;
+ break;
+ }
+
+ if (networkInfo.node) {
+ this._updateNetMessage(aActorId);
+ }
+
+ // 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 aActorId
+ * The network event actor ID for which you want to update the message.
+ */
+ _updateNetMessage: function WCF__updateNetMessage(aActorId)
+ {
+ let networkInfo = this._networkRequests[aActorId];
+ if (!networkInfo || !networkInfo.node) {
+ return;
+ }
+
+ 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 response = networkInfo.response;
+
+ 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 linkNode = messageNode.querySelector(".webconsole-msg-link");
+ let statusNode = linkNode.querySelector(".webconsole-msg-status");
+ statusNode.setAttribute("value", statusText);
+
+ messageNode.clipboardText = [request.method, 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);
+ }
+ }
+
+ if (messageNode._netPanel) {
+ messageNode._netPanel.update();
+ }
+ },
+
+ /**
+ * Opens a NetworkPanel.
+ *
+ * @param nsIDOMNode aNode
+ * The message node you want the panel to be anchored to.
+ * @param object aHttpActivity
+ * The HTTP activity object that holds network request and response
+ * information. This object is given to the NetworkPanel constructor.
+ * @return object
+ * The new NetworkPanel instance.
+ */
+ openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity)
+ {
+ let actor = aHttpActivity.actor;
+
+ if (actor) {
+ this.webConsoleClient.getRequestHeaders(actor, function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.headers = aResponse.headers;
+
+ this.webConsoleClient.getRequestCookies(actor, onRequestCookies);
+ }.bind(this));
+ }
+
+ let onRequestCookies = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestCookies:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.cookies = aResponse.cookies;
+
+ this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders);
+ }.bind(this);
+
+ let onResponseHeaders = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.headers = aResponse.headers;
+
+ this.webConsoleClient.getResponseCookies(actor, onResponseCookies);
+ }.bind(this);
+
+ let onResponseCookies = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseCookies:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.cookies = aResponse.cookies;
+
+ this.webConsoleClient.getRequestPostData(actor, onRequestPostData);
+ }.bind(this);
+
+ let onRequestPostData = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestPostData:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.postData = aResponse.postData;
+ aHttpActivity.discardRequestBody = aResponse.postDataDiscarded;
+
+ this.webConsoleClient.getResponseContent(actor, onResponseContent);
+ }.bind(this);
+
+ let onResponseContent = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseContent:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.content = aResponse.content;
+ aHttpActivity.discardResponseBody = aResponse.contentDiscarded;
+
+ this.webConsoleClient.getEventTimings(actor, onEventTimings);
+ }.bind(this);
+
+ let onEventTimings = function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getEventTimings:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.timings = aResponse.timings;
+
+ openPanel();
+ }.bind(this);
+
+ let openPanel = function() {
+ aNode._netPanel = netPanel;
+
+ let panel = netPanel.panel;
+ panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
+ panel.sizeTo(450, 500);
+ panel.setAttribute("hudId", this.hudId);
+
+ panel.addEventListener("popuphiding", function WCF_netPanel_onHide() {
+ panel.removeEventListener("popuphiding", WCF_netPanel_onHide);
+
+ aNode._panelOpen = false;
+ aNode._netPanel = null;
+ });
+
+ aNode._panelOpen = true;
+ }.bind(this);
+
+ let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this);
+ netPanel.linkNode = aNode;
+
+ if (!actor) {
+ openPanel();
+ }
+
+ return netPanel;
+ },
+
+ /**
+ * Handler for page location changes.
+ *
+ * @param string aURI
+ * New page location.
+ * @param string aTitle
+ * New page title.
+ */
+ onLocationChange: function WCF_onLocationChange(aURI, aTitle)
+ {
+ this.contentLocation = aURI;
+ if (this.owner.onLocationChange) {
+ this.owner.onLocationChange(aURI, aTitle);
+ }
+ },
+
+ /**
+ * 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 aCategory
+ * The category of the message you want to output. See the CATEGORY_*
+ * constants.
+ * @param function|nsIDOMElement aMethodOrNode
+ * 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 |aArguments|.
+ * @param array [aArguments]
+ * If a method is given to output the message element then the method
+ * will be invoked with the list of arguments given here.
+ */
+ outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments)
+ {
+ 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([aCategory, aMethodOrNode, aArguments]);
+
+ if (!this._outputTimerInitialized) {
+ 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 WCF__flushMessageQueue()
+ {
+ if (!this._outputTimer) {
+ return;
+ }
+
+ let timeSinceFlush = Date.now() - this._lastOutputFlush;
+ if (this._outputQueue.length > MESSAGES_IN_INTERVAL &&
+ timeSinceFlush < THROTTLE_UPDATES) {
+ this._initOutputTimer();
+ return;
+ }
+
+ // Determine how many messages we can display now.
+ let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
+ if (toDisplay < 1) {
+ this._outputTimerInitialized = false;
+ 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);
+ if (!batch.length) {
+ this._outputTimerInitialized = false;
+ return;
+ }
+
+ let outputNode = this.outputNode;
+ let lastVisibleNode = null;
+ let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode);
+ let scrollBox = outputNode.scrollBoxObject.element;
+
+ let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
+
+ // Output the current batch of messages.
+ let newMessages = new Set();
+ let updatedMessages = new Set();
+ for (let item of batch) {
+ let result = this._outputMessageFromQueue(hudIdSupportsString, item);
+ if (result) {
+ if (result.isRepeated) {
+ updatedMessages.add(result.isRepeated);
+ }
+ else {
+ newMessages.add(result.node);
+ }
+ if (result.visible && result.node == this.outputNode.lastChild) {
+ lastVisibleNode = result.node;
+ }
+ }
+ }
+
+ let oldScrollHeight = 0;
+
+ // Prune messages if needed. We do not do this for every flush call to
+ // improve performance.
+ let removedNodes = 0;
+ if (shouldPrune || !this._outputQueue.length) {
+ oldScrollHeight = scrollBox.scrollHeight;
+
+ let categories = Object.keys(this._pruneCategoriesQueue);
+ categories.forEach(function _pruneOutput(aCategory) {
+ removedNodes += this.pruneOutputIfNecessary(aCategory);
+ }, this);
+ this._pruneCategoriesQueue = {};
+ }
+
+ // Regroup messages at the end of the queue.
+ if (!this._outputQueue.length) {
+ this.regroupOutput();
+ }
+
+ let isInputOutput = lastVisibleNode &&
+ (lastVisibleNode.classList.contains("webconsole-msg-input") ||
+ lastVisibleNode.classList.contains("webconsole-msg-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 != scrollBox.scrollHeight) {
+ // If there were pruned messages and if scroll is not at the bottom, then
+ // we need to adjust the scroll location.
+ scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
+ }
+
+ if (newMessages.size) {
+ this.emit("messages-added", newMessages);
+ }
+ if (updatedMessages.size) {
+ this.emit("messages-updated", updatedMessages);
+ }
+
+ // If the queue is not empty, schedule another flush.
+ if (this._outputQueue.length > 0) {
+ this._initOutputTimer();
+ }
+ else {
+ this._outputTimerInitialized = false;
+ this._flushCallback && this._flushCallback();
+ }
+
+ this._lastOutputFlush = Date.now();
+ },
+
+ /**
+ * Initialize the output timer.
+ * @private
+ */
+ _initOutputTimer: function WCF__initOutputTimer()
+ {
+ if (!this._outputTimer) {
+ 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 aHudIdSupportsString
+ * The HUD ID as an nsISupportsString.
+ * @param array aItem
+ * 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 WCF__outputMessageFromQueue(aHudIdSupportsString, aItem)
+ {
+ let [category, methodOrNode, args] = aItem;
+
+ let node = typeof methodOrNode == "function" ?
+ methodOrNode.apply(this, args || []) :
+ methodOrNode;
+ if (!node) {
+ return null;
+ }
+
+ let afterNode = node._outputAfterNode;
+ if (afterNode) {
+ delete node._outputAfterNode;
+ }
+
+ let isFiltered = this.filterMessageNode(node);
+
+ let isRepeated = this._filterRepeatedMessage(node);
+
+ let visible = !isRepeated && !isFiltered;
+ if (!isRepeated) {
+ this.outputNode.insertBefore(node,
+ afterNode ? afterNode.nextSibling : null);
+ this._pruneCategoriesQueue[node.category] = true;
+
+ let nodeID = node.getAttribute("id");
+ Services.obs.notifyObservers(aHudIdSupportsString,
+ "web-console-message-created", nodeID);
+
+ }
+
+ if (node._onOutput) {
+ node._onOutput();
+ delete node._onOutput;
+ }
+
+ return {
+ visible: visible,
+ node: node,
+ isRepeated: isRepeated,
+ };
+ },
+
+ /**
+ * 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 WCF__pruneOutputQueue()
+ {
+ let nodes = {};
+
+ // Group the messages per category.
+ this._outputQueue.forEach(function(aItem, aIndex) {
+ let [category] = aItem;
+ if (!(category in nodes)) {
+ nodes[category] = [];
+ }
+ nodes[category].push(aIndex);
+ }, 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._pruneItemFromQueue(this._outputQueue[indexes[i]]);
+ this._outputQueue.splice(indexes[i], 1);
+ }
+ }
+ }
+
+ return pruned;
+ },
+
+ /**
+ * Prune an item from the output queue.
+ *
+ * @private
+ * @param array aItem
+ * The item you want to remove from the output queue.
+ */
+ _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem)
+ {
+ let [category, methodOrNode, args] = aItem;
+ if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
+ for (let actor of methodOrNode._objectActors) {
+ this._releaseObject(actor);
+ }
+ methodOrNode._objectActors.clear();
+ }
+
+ if (category == CATEGORY_NETWORK) {
+ let connectionId = null;
+ if (methodOrNode == this.logNetEvent) {
+ connectionId = args[0];
+ }
+ else if (typeof methodOrNode != "function") {
+ connectionId = methodOrNode._connectionId;
+ }
+ if (connectionId && connectionId in this._networkRequests) {
+ delete this._networkRequests[connectionId];
+ this._releaseObject(connectionId);
+ }
+ }
+ else if (category == CATEGORY_WEBDEV &&
+ methodOrNode == this.logConsoleAPIMessage) {
+ args[0].arguments.forEach((aValue) => {
+ if (WebConsoleUtils.isActorGrip(aValue)) {
+ this._releaseObject(aValue.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);
+ }
+ }
+ },
+
+ /**
+ * Ensures that the number of message nodes of type aCategory don't exceed that
+ * category's line limit by removing old messages as needed.
+ *
+ * @param integer aCategory
+ * The category of message nodes to prune if needed.
+ * @return number
+ * The number of removed nodes.
+ */
+ pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory)
+ {
+ let outputNode = this.outputNode;
+ let logLimit = Utils.logLimitForCategory(aCategory);
+
+ let messageNodes = outputNode.getElementsByClassName("webconsole-msg-" +
+ CATEGORY_CLASS_FRAGMENTS[aCategory]);
+ let n = Math.max(0, messageNodes.length - logLimit);
+ let toRemove = Array.prototype.slice.call(messageNodes, 0, n);
+ toRemove.forEach(this.removeOutputMessage, this);
+
+ return n;
+ },
+
+ /**
+ * Remove a given message from the output.
+ *
+ * @param nsIDOMNode aNode
+ * The message node you want to remove.
+ */
+ removeOutputMessage: function WCF_removeOutputMessage(aNode)
+ {
+ if (aNode._objectActors) {
+ for (let actor of aNode._objectActors) {
+ this._releaseObject(actor);
+ }
+ aNode._objectActors.clear();
+ }
+
+ if (aNode.classList.contains("webconsole-msg-cssparser") ||
+ aNode.classList.contains("webconsole-msg-security")) {
+ let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0];
+ if (repeatNode && repeatNode._uid) {
+ delete this._repeatNodes[repeatNode._uid];
+ }
+ }
+ else if (aNode._connectionId &&
+ aNode.classList.contains("webconsole-msg-network")) {
+ delete this._networkRequests[aNode._connectionId];
+ this._releaseObject(aNode._connectionId);
+ }
+ else if (aNode.classList.contains("webconsole-msg-inspector")) {
+ let view = aNode._variablesView;
+ if (view) {
+ view.controller.releaseActors();
+ }
+ aNode._variablesView = null;
+ }
+
+ if (aNode.parentNode) {
+ aNode.parentNode.removeChild(aNode);
+ }
+ },
+
+ /**
+ * Splits the given console messages into groups based on their timestamps.
+ */
+ regroupOutput: function WCF_regroupOutput()
+ {
+ // Go through the nodes and adjust the placement of "webconsole-new-group"
+ // classes.
+ let nodes = this.outputNode.querySelectorAll(".hud-msg-node" +
+ ":not(.hud-filtered-by-string):not(.hud-filtered-by-type)");
+ let lastTimestamp;
+ for (let i = 0, n = nodes.length; i < n; i++) {
+ let thisTimestamp = nodes[i].timestamp;
+ if (lastTimestamp != null &&
+ thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) {
+ nodes[i].classList.add("webconsole-new-group");
+ }
+ else {
+ nodes[i].classList.remove("webconsole-new-group");
+ }
+ lastTimestamp = thisTimestamp;
+ }
+ },
+
+ /**
+ * Given a category and message body, creates a DOM node to represent an
+ * incoming message. The timestamp is automatically added.
+ *
+ * @param number aCategory
+ * The category of the message: one of the CATEGORY_* constants.
+ * @param number aSeverity
+ * The severity of the message: one of the SEVERITY_* constants;
+ * @param string|nsIDOMNode aBody
+ * The body of the message, either a simple string or a DOM node.
+ * @param string aSourceURL [optional]
+ * The URL of the source file that emitted the error.
+ * @param number aSourceLine [optional]
+ * The line number on which the error occurred. If zero or omitted,
+ * there is no line number associated with this message.
+ * @param string aClipboardText [optional]
+ * The text that should be copied to the clipboard when this node is
+ * copied. If omitted, defaults to the body text. If `aBody` is not
+ * a string, then the clipboard text must be supplied.
+ * @param number aLevel [optional]
+ * The level of the console API message.
+ * @param number aTimeStamp [optional]
+ * The timestamp to use for this message node. If omitted, the current
+ * date and time is used.
+ * @return nsIDOMNode
+ * The message node: a XUL richlistitem ready to be inserted into
+ * the Web Console output node.
+ */
+ createMessageNode:
+ function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
+ aSourceLine, aClipboardText, aLevel, aTimeStamp)
+ {
+ if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
+ aClipboardText = aBody.innerText;
+ }
+
+ // 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(XUL_NS, "vbox");
+ iconContainer.classList.add("webconsole-msg-icon-container");
+ // Apply the curent group by indenting appropriately.
+ iconContainer.style.marginLeft = this.groupDepth * GROUP_INDENT + "px";
+
+ // Make the icon node. It's sprited and the actual region of the image is
+ // determined by CSS rules.
+ let iconNode = this.document.createElementNS(XUL_NS, "image");
+ iconNode.classList.add("webconsole-msg-icon");
+ iconContainer.appendChild(iconNode);
+
+ // Make the spacer that positions the icon.
+ let spacer = this.document.createElementNS(XUL_NS, "spacer");
+ spacer.flex = 1;
+ iconContainer.appendChild(spacer);
+
+ // Create the message body, which contains the actual text of the message.
+ let bodyNode = this.document.createElementNS(XUL_NS, "description");
+ bodyNode.flex = 1;
+ bodyNode.classList.add("webconsole-msg-body");
+
+ // Store the body text, since it is needed later for the variables view.
+ let body = aBody;
+ // If a string was supplied for the body, turn it into a DOM node and an
+ // associated clipboard string now.
+ aClipboardText = aClipboardText ||
+ (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
+ (aSourceLine ? ":" + aSourceLine : ""));
+
+ // Create the containing node and append all its elements to it.
+ let node = this.document.createElementNS(XUL_NS, "richlistitem");
+
+ if (aBody instanceof Ci.nsIDOMNode) {
+ bodyNode.appendChild(aBody);
+ }
+ else {
+ let str = undefined;
+ if (aLevel == "dir") {
+ str = VariablesView.getString(aBody.arguments[0]);
+ }
+ else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 &&
+ typeof aBody == "object") {
+ this._makeConsoleLogMessageBody(node, bodyNode, aBody);
+ }
+ else {
+ str = aBody;
+ }
+
+ if (str !== undefined) {
+ aBody = this.document.createTextNode(str);
+ bodyNode.appendChild(aBody);
+ }
+ }
+
+ let repeatContainer = this.document.createElementNS(XUL_NS, "hbox");
+ repeatContainer.setAttribute("align", "start");
+ let repeatNode = this.document.createElementNS(XUL_NS, "label");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.classList.add("webconsole-msg-repeat");
+ repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
+ aSourceURL, aSourceLine].join(":");
+ repeatContainer.appendChild(repeatNode);
+
+ // Create the timestamp.
+ let timestampNode = this.document.createElementNS(XUL_NS, "label");
+ timestampNode.classList.add("webconsole-timestamp");
+ let timestamp = aTimeStamp || Date.now();
+ let timestampString = l10n.timestampString(timestamp);
+ timestampNode.setAttribute("value", 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 (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
+ locationNode = this.createLocationNode(aSourceURL, aSourceLine);
+ }
+
+ node.clipboardText = aClipboardText;
+ node.classList.add("hud-msg-node");
+
+ node.timestamp = timestamp;
+ this.setMessageType(node, aCategory, aSeverity);
+
+ node.appendChild(timestampNode);
+ node.appendChild(iconContainer);
+
+ // Display the variables view after the message node.
+ if (aLevel == "dir") {
+ let viewContainer = this.document.createElement("hbox");
+ viewContainer.flex = 1;
+ viewContainer.height = this.outputNode.clientHeight *
+ CONSOLE_DIR_VIEW_HEIGHT;
+
+ let options = {
+ objectActor: body.arguments[0],
+ targetElement: viewContainer,
+ hideFilterInput: true,
+ };
+ this.jsterm.openVariablesView(options).then((aView) => {
+ node._variablesView = aView;
+ if (node.classList.contains("hidden-message")) {
+ node.classList.remove("hidden-message");
+ }
+ });
+
+ let bodyContainer = this.document.createElement("vbox");
+ bodyContainer.flex = 1;
+ bodyContainer.appendChild(bodyNode);
+ bodyContainer.appendChild(viewContainer);
+ node.appendChild(bodyContainer);
+ node.classList.add("webconsole-msg-inspector");
+ }
+ else {
+ node.appendChild(bodyNode);
+ }
+ node.appendChild(repeatContainer);
+ if (locationNode) {
+ node.appendChild(locationNode);
+ }
+
+ node.setAttribute("id", "console-msg-" + gSequenceId());
+
+ return node;
+ },
+
+ /**
+ * Make the message body for console.log() calls.
+ *
+ * @private
+ * @param nsIDOMElement aMessage
+ * The message element that holds the output for the given call.
+ * @param nsIDOMElement aContainer
+ * The specific element that will hold each part of the console.log
+ * output.
+ * @param object aBody
+ * The object given by this.logConsoleAPIMessage(). This object holds
+ * the call information that we need to display - mainly the arguments
+ * array of the given API call.
+ */
+ _makeConsoleLogMessageBody:
+ function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody)
+ {
+ Object.defineProperty(aMessage, "_panelOpen", {
+ get: function() {
+ let nodes = aContainer.querySelectorAll(".hud-clickable");
+ return Array.prototype.some.call(nodes, function(aNode) {
+ return aNode._panelOpen;
+ });
+ },
+ enumerable: true,
+ configurable: false
+ });
+
+ aBody.arguments.forEach(function(aItem) {
+ if (aContainer.firstChild) {
+ aContainer.appendChild(this.document.createTextNode(" "));
+ }
+
+ let text = VariablesView.getString(aItem);
+ let inspectable = !VariablesView.isPrimitive({ value: aItem });
+
+ if (aItem && typeof aItem != "object" || !inspectable) {
+ aContainer.appendChild(this.document.createTextNode(text));
+
+ if (aItem.type && aItem.type == "longString") {
+ let ellipsis = this.document.createElement("description");
+ ellipsis.classList.add("hud-clickable");
+ ellipsis.classList.add("longStringEllipsis");
+ ellipsis.textContent = l10n.getStr("longStringEllipsis");
+
+ let formatter = function(s) '"' + s + '"';
+
+ this._addMessageLinkCallback(ellipsis,
+ this._longStringClick.bind(this, aMessage, aItem, formatter));
+
+ aContainer.appendChild(ellipsis);
+ }
+ return;
+ }
+
+ // For inspectable objects.
+ let elem = this.document.createElement("description");
+ elem.classList.add("hud-clickable");
+ elem.setAttribute("aria-haspopup", "true");
+ elem.appendChild(this.document.createTextNode(text));
+
+ this._addMessageLinkCallback(elem,
+ this._consoleLogClick.bind(this, elem, aItem));
+
+ aContainer.appendChild(elem);
+ }, this);
+ },
+
+ /**
+ * Click event handler for the ellipsis shown immediately after a long string.
+ * This method retrieves the full string and updates the console output to
+ * show it.
+ *
+ * @private
+ * @param nsIDOMElement aMessage
+ * The message element.
+ * @param object aActor
+ * The LongStringActor instance we work with.
+ * @param [function] aFormatter
+ * Optional function you can use to format the string received from the
+ * server, before being displayed in the console.
+ * @param nsIDOMElement aEllipsis
+ * The DOM element the user can click on to expand the string.
+ * @param nsIDOMEvent aEvent
+ * The DOM click event triggered by the user.
+ */
+ _longStringClick:
+ function WCF__longStringClick(aMessage, aActor, aFormatter, aEllipsis, aEvent)
+ {
+ aEvent.preventDefault();
+
+ if (!aFormatter) {
+ aFormatter = function(s) s;
+ }
+
+ let longString = this.webConsoleClient.longString(aActor);
+ let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
+ longString.substring(longString.initial.length, toIndex,
+ function WCF__onSubstring(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("WCF__longStringClick substring failure: " +
+ aResponse.error);
+ return;
+ }
+
+ let node = aEllipsis.previousSibling;
+ node.textContent = aFormatter(longString.initial + aResponse.substring);
+ aEllipsis.parentNode.removeChild(aEllipsis);
+
+ if (aMessage.category == CATEGORY_WEBDEV ||
+ aMessage.category == CATEGORY_OUTPUT) {
+ aMessage.clipboardText = aMessage.textContent;
+ }
+
+ this.emit("messages-updated", new Set([aMessage]));
+
+ if (toIndex != longString.length) {
+ this.logWarningAboutStringTooLong();
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Creates the XUL label that displays the textual location of an incoming
+ * message.
+ *
+ * @param string aSourceURL
+ * The URL of the source file responsible for the error.
+ * @param number aSourceLine [optional]
+ * The line number on which the error occurred. If zero or omitted,
+ * there is no line number associated with this message.
+ * @return nsIDOMNode
+ * The new XUL label node, ready to be added to the message node.
+ */
+ createLocationNode: function WCF_createLocationNode(aSourceURL, aSourceLine)
+ {
+ let locationNode = this.document.createElementNS(XUL_NS, "label");
+
+ // Create the text, which consists of an abbreviated version of the URL
+ // plus an optional line number. Scratchpad URLs should not be abbreviated.
+ let displayLocation;
+ let fullURL;
+
+ if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
+ displayLocation = aSourceURL;
+ fullURL = aSourceURL;
+ }
+ else {
+ fullURL = aSourceURL.split(" -> ").pop();
+ displayLocation = WebConsoleUtils.abbreviateSourceURL(fullURL);
+ }
+
+ if (aSourceLine) {
+ displayLocation += ":" + aSourceLine;
+ locationNode.sourceLine = aSourceLine;
+ }
+
+ locationNode.setAttribute("value", displayLocation);
+
+ // Style appropriately.
+ locationNode.setAttribute("crop", "center");
+ locationNode.setAttribute("title", aSourceURL);
+ locationNode.setAttribute("tooltiptext", aSourceURL);
+ locationNode.classList.add("webconsole-location");
+ locationNode.classList.add("text-link");
+
+ // Make the location clickable.
+ locationNode.addEventListener("click", () => {
+ if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+
+ if (win.Scratchpad.uniqueName === aSourceURL) {
+ win.focus();
+ return;
+ }
+ }
+ }
+ else if (locationNode.parentNode.category == CATEGORY_CSS) {
+ this.owner.viewSourceInStyleEditor(fullURL, aSourceLine);
+ }
+ else if (locationNode.parentNode.category == CATEGORY_JS ||
+ locationNode.parentNode.category == CATEGORY_WEBDEV) {
+ this.owner.viewSourceInDebugger(fullURL, aSourceLine);
+ }
+ else {
+ this.owner.viewSource(fullURL, aSourceLine);
+ }
+ }, true);
+
+ return locationNode;
+ },
+
+ /**
+ * Adjusts the category and severity of the given message, clearing the old
+ * category and severity if present.
+ *
+ * @param nsIDOMNode aMessageNode
+ * The message node to alter.
+ * @param number aNewCategory
+ * The new category for the message; one of the CATEGORY_ constants.
+ * @param number aNewSeverity
+ * The new severity for the message; one of the SEVERITY_ constants.
+ * @return void
+ */
+ setMessageType:
+ function WCF_setMessageType(aMessageNode, aNewCategory, aNewSeverity)
+ {
+ // Remove the old CSS classes, if applicable.
+ if ("category" in aMessageNode) {
+ let oldCategory = aMessageNode.category;
+ let oldSeverity = aMessageNode.severity;
+ aMessageNode.classList.remove("webconsole-msg-" +
+ CATEGORY_CLASS_FRAGMENTS[oldCategory]);
+ aMessageNode.classList.remove("webconsole-msg-" +
+ SEVERITY_CLASS_FRAGMENTS[oldSeverity]);
+ let key = "hud-" + MESSAGE_PREFERENCE_KEYS[oldCategory][oldSeverity];
+ aMessageNode.classList.remove(key);
+ }
+
+ // Add in the new CSS classes.
+ aMessageNode.category = aNewCategory;
+ aMessageNode.severity = aNewSeverity;
+ aMessageNode.classList.add("webconsole-msg-" +
+ CATEGORY_CLASS_FRAGMENTS[aNewCategory]);
+ aMessageNode.classList.add("webconsole-msg-" +
+ SEVERITY_CLASS_FRAGMENTS[aNewSeverity]);
+ let key = "hud-" + MESSAGE_PREFERENCE_KEYS[aNewCategory][aNewSeverity];
+ aMessageNode.classList.add(key);
+ },
+
+ /**
+ * Make a link given an output element.
+ *
+ * @param nsIDOMNode aNode
+ * The message element you want to make a link for.
+ * @param function aCallback
+ * The function you want invoked when the user clicks on the message
+ * element.
+ */
+ makeOutputMessageLink: function WCF_makeOutputMessageLink(aNode, aCallback)
+ {
+ let linkNode;
+ if (aNode.category === CATEGORY_NETWORK) {
+ linkNode = aNode.querySelector(".webconsole-msg-link, .webconsole-msg-url");
+ }
+ else {
+ linkNode = aNode.querySelector(".webconsole-msg-body");
+ linkNode.classList.add("hud-clickable");
+ }
+
+ linkNode.setAttribute("aria-haspopup", "true");
+
+ this._addMessageLinkCallback(aNode, aCallback);
+ },
+
+ /**
+ * Add the mouse event handlers needed to make a link.
+ *
+ * @private
+ * @param nsIDOMNode aNode
+ * The node for which you want to add the event handlers.
+ * @param function aCallback
+ * The function you want to invoke on click.
+ */
+ _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
+ {
+ aNode.addEventListener("mousedown", function(aEvent) {
+ this._startX = aEvent.clientX;
+ this._startY = aEvent.clientY;
+ }, false);
+
+ aNode.addEventListener("click", function(aEvent) {
+ if (aEvent.detail != 1 || aEvent.button != 0 ||
+ (this._startX != aEvent.clientX &&
+ this._startY != aEvent.clientY)) {
+ return;
+ }
+
+ aCallback(this, aEvent);
+ }, false);
+ },
+
+ /**
+ * Copies the selected items to the system clipboard.
+ *
+ * @param object aOptions
+ * - linkOnly:
+ * An optional flag to copy only URL without timestamp and
+ * other meta-information. Default is false.
+ */
+ copySelectedItems: function WCF_copySelectedItems(aOptions)
+ {
+ aOptions = aOptions || { linkOnly: false };
+
+ // Gather up the selected items and concatenate their clipboard text.
+ let strings = [];
+ let newGroup = false;
+
+ let children = this.outputNode.children;
+
+ for (let i = 0; i < children.length; i++) {
+ let item = children[i];
+ if (!item.selected) {
+ continue;
+ }
+
+ // Add dashes between groups so that group boundaries show up in the
+ // copied output.
+ if (i > 0 && item.classList.contains("webconsole-new-group")) {
+ newGroup = true;
+ }
+
+ // Ensure the selected item hasn't been filtered by type or string.
+ if (!item.classList.contains("hud-filtered-by-type") &&
+ !item.classList.contains("hud-filtered-by-string")) {
+ let timestampString = l10n.timestampString(item.timestamp);
+ if (newGroup) {
+ strings.push("--");
+ newGroup = false;
+ }
+
+ if (aOptions.linkOnly) {
+ strings.push(item.url);
+ }
+ else {
+ strings.push("[" + timestampString + "] " + item.clipboardText);
+ }
+ }
+ }
+
+ clipboardHelper.copyString(strings.join("\n"), this.document);
+ },
+
+ /**
+ * Object properties provider. This function gives you the properties of the
+ * remote object you want.
+ *
+ * @param string aActor
+ * The object actor ID from which you want the properties.
+ * @param function aCallback
+ * Function you want invoked once the properties are received.
+ */
+ objectPropertiesProvider:
+ function WCF_objectPropertiesProvider(aActor, aCallback)
+ {
+ this.webConsoleClient.inspectObjectProperties(aActor,
+ function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("Failed to retrieve the object properties from the " +
+ "server. Error: " + aResponse.error);
+ return;
+ }
+ aCallback(aResponse.properties);
+ });
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string aActor
+ * The actor ID you want to release.
+ */
+ _releaseObject: function WCF__releaseObject(aActor)
+ {
+ if (this.proxy) {
+ this.proxy.releaseActor(aActor);
+ }
+ },
+
+ /**
+ * Open the selected item's URL in a new tab.
+ */
+ openSelectedItemInTab: function WCF_openSelectedItemInTab()
+ {
+ let item = this.outputNode.selectedItem;
+
+ 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 WCF_destroy()
+ {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = Promise.defer();
+
+ this._repeatNodes = {};
+ this._outputQueue = [];
+ this._pruneCategoriesQueue = {};
+ this._networkRequests = {};
+
+ if (this._outputTimerInitialized) {
+ this._outputTimerInitialized = false;
+ this._outputTimer.cancel();
+ }
+ this._outputTimer = null;
+
+ if (this.jsterm) {
+ this.jsterm.destroy();
+ this.jsterm = null;
+ }
+
+ this._commandController = null;
+
+ let onDestroy = function() {
+ this._destroyer.resolve(null);
+ }.bind(this);
+
+ if (this.proxy) {
+ this.proxy.disconnect().then(onDestroy);
+ this.proxy = null;
+ }
+ else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
+}
+
+
+
+/**
+ * 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 aWebConsoleFrame
+ * The WebConsoleFrame object that owns this JSTerm instance.
+ */
+function JSTerm(aWebConsoleFrame)
+{
+ this.hud = aWebConsoleFrame;
+ this.hudId = this.hud.hudId;
+
+ this.lastCompletion = { value: null };
+ this.history = [];
+
+ // Holds the number of entries in history. This value is incremented in
+ // this.execute().
+ this.historyIndex = 0; // incremented on this.execute()
+
+ // 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 = 0;
+ 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);
+
+ EventEmitter.decorate(this);
+}
+
+JSTerm.prototype = {
+ SELECTED_FRAME: -1,
+
+ /**
+ * Stores the data for the last completion.
+ * @type object
+ */
+ lastCompletion: 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() this.hud.outputNode,
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() this.hud.webConsoleClient,
+
+ COMPLETE_FORWARD: 0,
+ COMPLETE_BACKWARD: 1,
+ COMPLETE_HINT_ONLY: 2,
+
+ /**
+ * Initialize the JSTerminal UI.
+ */
+ init: function JST_init()
+ {
+ let chromeDocument = this.hud.owner.chromeWindow.document;
+ let autocompleteOptions = {
+ onSelect: this.onAutocompleteSelect.bind(this),
+ onClick: this.acceptProposedCompletion.bind(this),
+ panelId: "webConsole_autocompletePopup",
+ listBoxId: "webConsole_autocompletePopupListBox",
+ position: "before_start",
+ theme: "light",
+ direction: "ltr",
+ autoSelect: true
+ };
+ this.autocompletePopup = new AutocompletePopup(chromeDocument,
+ autocompleteOptions);
+
+ let doc = this.hud.document;
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+ this.inputNode.addEventListener("keypress", this._keyPress, false);
+ this.inputNode.addEventListener("input", this._inputEventHandler, false);
+ this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.addEventListener("focus", this._focusEventHandler, false);
+
+ this.lastInputValue && this.setInputValue(this.lastInputValue);
+ },
+
+ /**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param nsIDOMElement [aAfterNode]
+ * Optional DOM element after which the evaluation result will be
+ * inserted.
+ * @param function [aCallback]
+ * Optional function to invoke when the evaluation result is added to
+ * the output.
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _executeResultCallback:
+ function JST__executeResultCallback(aAfterNode, aCallback, aResponse)
+ {
+ if (!this.hud) {
+ return;
+ }
+ if (aResponse.error) {
+ Cu.reportError("Evaluation error " + aResponse.error + ": " +
+ aResponse.message);
+ return;
+ }
+ let errorMessage = aResponse.exceptionMessage;
+ let result = aResponse.result;
+ let inspectable = false;
+ if (result && !VariablesView.isPrimitive({ value: result })) {
+ inspectable = true;
+ }
+ let helperResult = aResponse.helperResult;
+ let helperHasRawOutput = !!(helperResult || {}).rawOutput;
+ let resultString = VariablesView.getString(result);
+
+ if (helperResult && helperResult.type) {
+ switch (helperResult.type) {
+ case "clearOutput":
+ this.clearOutput();
+ break;
+ case "inspectObject":
+ if (aAfterNode) {
+ if (!aAfterNode._objectActors) {
+ aAfterNode._objectActors = new Set();
+ }
+ aAfterNode._objectActors.add(helperResult.object.actor);
+ }
+ this.openVariablesView({
+ label: VariablesView.getString(helperResult.object),
+ 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;
+ }
+ }
+
+ // Hide undefined results coming from JSTerm helper functions.
+ if (!errorMessage && result && typeof result == "object" &&
+ result.type == "undefined" &&
+ helperResult && !helperHasRawOutput) {
+ aCallback && aCallback();
+ return;
+ }
+
+ if (aCallback) {
+ let oldFlushCallback = this.hud._flushCallback;
+ this.hud._flushCallback = function() {
+ aCallback();
+ oldFlushCallback && oldFlushCallback();
+ this.hud._flushCallback = oldFlushCallback;
+ }.bind(this);
+ }
+
+ let node;
+
+ if (errorMessage) {
+ node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR,
+ aAfterNode, aResponse.timestamp);
+ }
+ else if (inspectable) {
+ node = this.writeOutputJS(resultString,
+ this._evalOutputClick.bind(this, aResponse),
+ aAfterNode, aResponse.timestamp);
+ }
+ else {
+ node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG,
+ aAfterNode, aResponse.timestamp);
+ }
+
+ node._objectActors = new Set();
+
+ let error = aResponse.exception;
+ if (WebConsoleUtils.isActorGrip(error)) {
+ node._objectActors.add(error.actor);
+ }
+
+ if (WebConsoleUtils.isActorGrip(result)) {
+ node._objectActors.add(result.actor);
+
+ if (result.type == "longString") {
+ // Add an ellipsis to expand the short string if the object is not
+ // inspectable.
+
+ let body = node.querySelector(".webconsole-msg-body");
+ let ellipsis = this.hud.document.createElement("description");
+ ellipsis.classList.add("hud-clickable");
+ ellipsis.classList.add("longStringEllipsis");
+ ellipsis.textContent = l10n.getStr("longStringEllipsis");
+
+ let formatter = function(s) '"' + s + '"';
+ let onclick = this.hud._longStringClick.bind(this.hud, node, result,
+ formatter);
+ this.hud._addMessageLinkCallback(ellipsis, onclick);
+
+ body.appendChild(ellipsis);
+
+ node.clipboardText += " " + ellipsis.textContent;
+ }
+ }
+ },
+
+ /**
+ * Execute a string. Execution happens asynchronously in the content process.
+ *
+ * @param string [aExecuteString]
+ * The string you want to execute. If this is not provided, the current
+ * user input is used - taken from |this.inputNode.value|.
+ * @param function [aCallback]
+ * Optional function to invoke when the result is displayed.
+ */
+ execute: function JST_execute(aExecuteString, aCallback)
+ {
+ // attempt to execute the content of the inputNode
+ aExecuteString = aExecuteString || this.inputNode.value;
+ if (!aExecuteString) {
+ return;
+ }
+
+ let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);
+ let onResult = this._executeResultCallback.bind(this, node, aCallback);
+
+ let options = { frame: this.SELECTED_FRAME };
+ this.requestEvaluation(aExecuteString, 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++] = aExecuteString;
+ this.historyPlaceHolder = this.history.length;
+ this.setInputValue("");
+ this.clearCompletion();
+ },
+
+ /**
+ * Request a JavaScript string evaluation from the server.
+ *
+ * @param string aString
+ * String to execute.
+ * @param object [aOptions]
+ * 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.
+ * @return object
+ * A Promise object that is resolved when the server response is
+ * received.
+ */
+ requestEvaluation: function JST_requestEvaluation(aString, aOptions = {})
+ {
+ let deferred = Promise.defer();
+
+ function onResult(aResponse) {
+ if (!aResponse.error) {
+ deferred.resolve(aResponse);
+ }
+ else {
+ deferred.reject(aResponse);
+ }
+ }
+
+ let frameActor = null;
+ if ("frame" in aOptions) {
+ frameActor = this.getFrameActor(aOptions.frame);
+ }
+
+ let evalOptions = {
+ bindObjectActor: aOptions.bindObjectActor,
+ frameActor: frameActor,
+ };
+
+ this.webConsoleClient.evaluateJS(aString, onResult, evalOptions);
+ return deferred.promise;
+ },
+
+ /**
+ * Retrieve the FrameActor ID given a frame depth.
+ *
+ * @param number aFrame
+ * Frame depth.
+ * @return string|null
+ * The FrameActor ID for the given frame depth.
+ */
+ getFrameActor: function JST_getFrameActor(aFrame)
+ {
+ let state = this.hud.owner.getDebuggerFrames();
+ if (!state) {
+ return null;
+ }
+
+ let grip;
+ if (aFrame == this.SELECTED_FRAME) {
+ grip = state.frames[state.selected];
+ }
+ else {
+ grip = state.frames[aFrame];
+ }
+
+ return grip ? grip.actor : null;
+ },
+
+ /**
+ * Opens a new variables view that allows the inspection of the given object.
+ *
+ * @param object aOptions
+ * 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 JST_openVariablesView(aOptions)
+ {
+ let onContainerReady = (aWindow) => {
+ let container = aWindow.document.querySelector("#variables");
+ let view = this._variablesView;
+ if (!view || aOptions.targetElement) {
+ let viewOptions = {
+ container: container,
+ hideFilterInput: aOptions.hideFilterInput,
+ };
+ view = this._createVariablesView(viewOptions);
+ if (!aOptions.targetElement) {
+ this._variablesView = view;
+ aWindow.addEventListener("keypress", this._onKeypressInVariablesView);
+ }
+ }
+ aOptions.view = view;
+ this._updateVariablesView(aOptions);
+
+ if (!aOptions.targetElement && aOptions.autofocus) {
+ aWindow.focus();
+ }
+
+ this.emit("variablesview-open", view, aOptions);
+ return view;
+ };
+
+ let promise;
+ if (aOptions.targetElement) {
+ let deferred = Promise.defer();
+ promise = deferred.promise;
+ let document = aOptions.targetElement.ownerDocument;
+ let iframe = document.createElement("iframe");
+
+ iframe.addEventListener("load", function onIframeLoad(aEvent) {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ deferred.resolve(iframe.contentWindow);
+ }, true);
+
+ iframe.flex = 1;
+ iframe.setAttribute("src", VARIABLES_VIEW_URL);
+ aOptions.targetElement.appendChild(iframe);
+ }
+ else {
+ if (!this.sidebar) {
+ this._createSidebar();
+ }
+ promise = this._addVariablesViewSidebarTab();
+ }
+
+ return promise.then(onContainerReady);
+ },
+
+ /**
+ * Create the Web Console sidebar.
+ *
+ * @see devtools/framework/sidebar.js
+ * @private
+ */
+ _createSidebar: function JST__createSidebar()
+ {
+ let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
+ let ToolSidebar = devtools.require("devtools/framework/sidebar").ToolSidebar;
+ this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
+ this.sidebar.show();
+ },
+
+ /**
+ * Add the variables view tab to the sidebar.
+ *
+ * @private
+ * @return object
+ * A Promise object for the adding of the new tab.
+ */
+ _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab()
+ {
+ let deferred = Promise.defer();
+
+ let onTabReady = () => {
+ let window = this.sidebar.getWindowForTab("variablesview");
+ deferred.resolve(window);
+ };
+
+ let tab = this.sidebar.getTab("variablesview");
+ if (tab) {
+ 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, 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 aEvent
+ * The keypress DOM event object.
+ */
+ _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent)
+ {
+ let tag = aEvent.target.nodeName;
+ if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey ||
+ aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey ||
+ ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
+ return;
+ }
+
+ this._sidebarDestroy();
+ this.inputNode.focus();
+ },
+
+ /**
+ * Create a variables view instance.
+ *
+ * @private
+ * @param object aOptions
+ * 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 JST__createVariablesView(aOptions)
+ {
+ let view = new VariablesView(aOptions.container);
+ view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
+ view.emptyText = l10n.getStr("emptyPropertiesList");
+ view.searchEnabled = !aOptions.hideFilterInput;
+ view.lazyEmpty = this._lazyVariablesView;
+ view.lazyAppend = this._lazyVariablesView;
+
+ VariablesViewController.attach(view, {
+ getGripClient: aGrip => {
+ return new GripClient(this.hud.proxy.client, aGrip);
+ },
+ getLongStringClient: aGrip => {
+ return this.webConsoleClient.longString(aGrip);
+ },
+ releaseActor: aActor => {
+ this.hud._releaseObject(aActor);
+ },
+ simpleValueEvalMacro: simpleValueEvalMacro,
+ overrideValueEvalMacro: overrideValueEvalMacro,
+ getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+ });
+
+ // Relay events from the VariablesView.
+ view.on("fetched", (aEvent, aType, aVar) => {
+ this.emit("variablesview-fetched", aVar);
+ });
+
+ return view;
+ },
+
+ /**
+ * Update the variables view.
+ *
+ * @private
+ * @param object aOptions
+ * 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 JST__updateVariablesView(aOptions)
+ {
+ let view = aOptions.view;
+ view.createHierarchy();
+ 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(aActor => {
+ return view._consoleLastObjectActor != aActor;
+ });
+
+ if (aOptions.objectActor) {
+ // Make sure eval works in the correct context.
+ view.eval = this._variablesViewEvaluate.bind(this, aOptions);
+ view.switch = this._variablesViewSwitch.bind(this, aOptions);
+ view.delete = this._variablesViewDelete.bind(this, aOptions);
+ }
+ else {
+ view.eval = null;
+ view.switch = null;
+ view.delete = null;
+ }
+
+ let scope = view.addScope(aOptions.label);
+ scope.expanded = true;
+ scope.locked = true;
+
+ let container = scope.addItem();
+ container.evaluationMacro = simpleValueEvalMacro;
+
+ if (aOptions.objectActor) {
+ view.controller.expand(container, aOptions.objectActor);
+ view._consoleLastObjectActor = aOptions.objectActor.actor;
+ }
+ else if (aOptions.rawObject) {
+ container.populate(aOptions.rawObject);
+ view.commitHierarchy();
+ view._consoleLastObjectActor = null;
+ }
+ else {
+ throw new Error("Variables View cannot open without giving it an object " +
+ "display.");
+ }
+
+ this.emit("variablesview-updated", view, aOptions);
+ },
+
+ /**
+ * The evaluation function used by the variables view when editing a property
+ * value.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param string aString
+ * The string that the variables view wants to evaluate.
+ */
+ _variablesViewEvaluate: function JST__variablesViewEvaluate(aOptions, aString)
+ {
+ let updater = this._updateVariablesView.bind(this, aOptions);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ this.requestEvaluation(aString, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property deletion function used by the variables view when a property
+ * is deleted.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param object aVar
+ * The Variable object instance for the deleted property.
+ */
+ _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar)
+ {
+ let onEval = this._silentEvalCallback.bind(this, null);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions)
+ .then(onEval, onEval);
+ },
+
+ /**
+ * The property rename function used by the variables view when a property
+ * is renamed.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param object aVar
+ * The Variable object instance for the renamed property.
+ * @param string aNewName
+ * The new name for the property.
+ */
+ _variablesViewSwitch:
+ function JST__variablesViewSwitch(aOptions, aVar, aNewName)
+ {
+ let updater = this._updateVariablesView.bind(this, aOptions);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]';
+ if (newSymbolicName == aVar.symbolicName) {
+ return;
+ }
+
+ let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" +
+ "delete _self" + aVar.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 aCallback
+ * Function to invoke once the response is received.
+ * @param object aResponse
+ * The response packet received from the server.
+ */
+ _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" +
+ aResponse.message);
+
+ aCallback && aCallback(aResponse);
+ return;
+ }
+
+ let exception = aResponse.exception;
+ if (exception) {
+ let node = this.writeOutput(aResponse.exceptionMessage,
+ CATEGORY_OUTPUT, SEVERITY_ERROR,
+ null, aResponse.timestamp);
+ node._objectActors = new Set();
+ if (WebConsoleUtils.isActorGrip(exception)) {
+ node._objectActors.add(exception.actor);
+ }
+ }
+
+ let helper = aResponse.helperResult || { type: null };
+ let helperGrip = null;
+ if (helper.type == "inspectObject") {
+ helperGrip = helper.object;
+ }
+
+ let grips = [aResponse.result, helperGrip];
+ for (let grip of grips) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this.hud._releaseObject(grip.actor);
+ }
+ }
+
+ aCallback && aCallback(aResponse);
+ },
+
+
+
+ /**
+ * Writes a JS object to the JSTerm outputNode.
+ *
+ * @param string aOutputMessage
+ * The message to display.
+ * @param function [aCallback]
+ * Optional function to invoke when users click the message.
+ * @param nsIDOMNode [aNodeAfter]
+ * Optional DOM node after which you want to insert the new message.
+ * This is used when execution results need to be inserted immediately
+ * after the user input.
+ * @param number [aTimestamp]
+ * Optional timestamp to show for the output message (millisconds since
+ * the UNIX epoch). If no timestamp is provided then Date.now() is
+ * used.
+ * @return nsIDOMNode
+ * The new message node.
+ */
+ writeOutputJS:
+ function JST_writeOutputJS(aOutputMessage, aCallback, aNodeAfter, aTimestamp)
+ {
+ let node = this.writeOutput(aOutputMessage, CATEGORY_OUTPUT, SEVERITY_LOG,
+ aNodeAfter, aTimestamp);
+ if (aCallback) {
+ this.hud.makeOutputMessageLink(node, aCallback);
+ }
+ return node;
+ },
+
+ /**
+ * Writes a message to the HUD that originates from the interactive
+ * JavaScript console.
+ *
+ * @param string aOutputMessage
+ * The message to display.
+ * @param number aCategory
+ * The category of message: one of the CATEGORY_ constants.
+ * @param number aSeverity
+ * The severity of message: one of the SEVERITY_ constants.
+ * @param nsIDOMNode [aNodeAfter]
+ * Optional DOM node after which you want to insert the new message.
+ * This is used when execution results need to be inserted immediately
+ * after the user input.
+ * @param number [aTimestamp]
+ * Optional timestamp to show for the output message (millisconds since
+ * the UNIX epoch). If no timestamp is provided then Date.now() is
+ * used.
+ * @return nsIDOMNode
+ * The new message node.
+ */
+ writeOutput:
+ function JST_writeOutput(aOutputMessage, aCategory, aSeverity, aNodeAfter,
+ aTimestamp)
+ {
+ let node = this.hud.createMessageNode(aCategory, aSeverity, aOutputMessage,
+ null, null, null, null, aTimestamp);
+ node._outputAfterNode = aNodeAfter;
+ this.hud.outputMessage(aCategory, node);
+ return node;
+ },
+
+ /**
+ * Clear the Web Console output.
+ *
+ * This method emits the "messages-cleared" notification.
+ *
+ * @param boolean aClearStorage
+ * True if you want to clear the console messages storage associated to
+ * this Web Console.
+ */
+ clearOutput: function JST_clearOutput(aClearStorage)
+ {
+ let hud = this.hud;
+ let outputNode = hud.outputNode;
+ let node;
+ while ((node = outputNode.firstChild)) {
+ hud.removeOutputMessage(node);
+ }
+
+ hud.groupDepth = 0;
+ hud._outputQueue.forEach(hud._pruneItemFromQueue, hud);
+ hud._outputQueue = [];
+ hud._networkRequests = {};
+ hud._repeatNodes = {};
+
+ if (aClearStorage) {
+ this.webConsoleClient.clearMessagesCache();
+ }
+
+ 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 JST_clearPrivateMessages()
+ {
+ let nodes = this.hud.outputNode.querySelectorAll("richlistitem[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 JST_resizeInput()
+ {
+ 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 aNewValue
+ * The new value to set.
+ * @returns void
+ */
+ setInputValue: function JST_setInputValue(aNewValue)
+ {
+ this.inputNode.value = aNewValue;
+ this.lastInputValue = aNewValue;
+ this.completeNode.value = "";
+ this.resizeInput();
+ this._inputChanged = true;
+ },
+
+ /**
+ * The inputNode "input" and "keyup" event handler.
+ * @private
+ */
+ _inputEventHandler: function JST__inputEventHandler()
+ {
+ if (this.lastInputValue != this.inputNode.value) {
+ this.resizeInput();
+ this.complete(this.COMPLETE_HINT_ONLY);
+ this.lastInputValue = this.inputNode.value;
+ this._inputChanged = true;
+ }
+ },
+
+ /**
+ * The inputNode "keypress" event handler.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _keyPress: function JST__keyPress(aEvent)
+ {
+ let inputNode = this.inputNode;
+ let inputUpdated = false;
+
+ if (aEvent.ctrlKey) {
+ switch (aEvent.charCode) {
+ case 97:
+ // control-a
+ this.clearCompletion();
+
+ if (Services.appinfo.OS == "WINNT") {
+ // Allow Select All on Windows.
+ break;
+ }
+
+ let lineBeginPos = 0;
+ if (this.hasMultilineInput()) {
+ // find index of closest newline <= to cursor
+ for (let i = inputNode.selectionStart-1; i >= 0; i--) {
+ if (inputNode.value.charAt(i) == "\r" ||
+ inputNode.value.charAt(i) == "\n") {
+ lineBeginPos = i+1;
+ break;
+ }
+ }
+ }
+ inputNode.setSelectionRange(lineBeginPos, lineBeginPos);
+ aEvent.preventDefault();
+ break;
+
+ case 101:
+ // control-e
+ if (Services.appinfo.OS == "WINNT") {
+ break;
+ }
+ let lineEndPos = inputNode.value.length;
+ if (this.hasMultilineInput()) {
+ // find index of closest newline >= cursor
+ for (let i = inputNode.selectionEnd; i<lineEndPos; i++) {
+ if (inputNode.value.charAt(i) == "\r" ||
+ inputNode.value.charAt(i) == "\n") {
+ lineEndPos = i;
+ break;
+ }
+ }
+ }
+ inputNode.setSelectionRange(lineEndPos, lineEndPos);
+ aEvent.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)) {
+ aEvent.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.
+ inputNode.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)) {
+ aEvent.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.
+ inputNode.focus();
+ }
+ this.clearCompletion();
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ else if (aEvent.shiftKey &&
+ aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+ // shift return
+ // TODO: expand the inputNode height by one line
+ return;
+ }
+
+ switch (aEvent.keyCode) {
+ case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ aEvent.preventDefault();
+ }
+ else if (this.sidebar) {
+ this._sidebarDestroy();
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
+ if (this._autocompletePopupNavigated &&
+ this.autocompletePopup.isOpen &&
+ this.autocompletePopup.selectedIndex > -1) {
+ this.acceptProposedCompletion();
+ }
+ else {
+ this.execute();
+ this._inputChanged = false;
+ }
+ aEvent.preventDefault();
+ break;
+
+ case Ci.nsIDOMKeyEvent.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) {
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.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) {
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
+ case Ci.nsIDOMKeyEvent.DOM_VK_END:
+ case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
+ if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
+ this.clearCompletion();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: {
+ let cursorAtTheEnd = this.inputNode.selectionStart ==
+ this.inputNode.selectionEnd &&
+ this.inputNode.selectionStart ==
+ this.inputNode.value.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()) {
+ aEvent.preventDefault();
+ }
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ }
+ break;
+ }
+ case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
+ // Generate a completion and accept the first proposed value.
+ if (this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion &&
+ this.acceptProposedCompletion()) {
+ aEvent.preventDefault();
+ }
+ else if (this._inputChanged) {
+ this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
+ aEvent.preventDefault();
+ }
+ break;
+ default:
+ break;
+ }
+ },
+
+ /**
+ * The inputNode "focus" event handler.
+ * @private
+ */
+ _focusEventHandler: function JST__focusEventHandler()
+ {
+ this._inputChanged = false;
+ },
+
+ /**
+ * Go up/down the history stack of input values.
+ *
+ * @param number aDirection
+ * History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
+ *
+ * @returns boolean
+ * True if the input value changed, false otherwise.
+ */
+ historyPeruse: function JST_historyPeruse(aDirection)
+ {
+ if (!this.history.length) {
+ return false;
+ }
+
+ // Up Arrow key
+ if (aDirection == 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.inputNode.value || "";
+ }
+
+ this.setInputValue(inputVal);
+ }
+ // Down Arrow key
+ else if (aDirection == HISTORY_FORWARD) {
+ 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 JST_hasMultilineInput()
+ {
+ return /[\r\n]/.test(this.inputNode.value);
+ },
+
+ /**
+ * 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 JST_canCaretGoPrevious()
+ {
+ 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 JST_canCaretGoNext()
+ {
+ 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 aType 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_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 inputNode.value is set to this value and the selection is set
+ * from the current cursor position to the end of the completed text.
+ * @param function aCallback
+ * 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 JSTF_complete(aType, aCallback)
+ {
+ let inputNode = this.inputNode;
+ let inputValue = inputNode.value;
+ // If the inputNode has no value, then don't try to complete on it.
+ if (!inputValue) {
+ this.clearCompletion();
+ return false;
+ }
+
+ // Only complete if the selection is empty and at the end of the input.
+ if (inputNode.selectionStart == inputNode.selectionEnd &&
+ inputNode.selectionEnd != inputValue.length) {
+ this.clearCompletion();
+ return false;
+ }
+
+ // Update the completion results.
+ if (this.lastCompletion.value != inputValue) {
+ this._updateCompletionResult(aType, aCallback);
+ return false;
+ }
+
+ let popup = this.autocompletePopup;
+ let accepted = false;
+
+ if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ accepted = true;
+ }
+ else if (aType == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ }
+ else if (aType == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ }
+
+ aCallback && aCallback(this);
+ 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 aType
+ * Completion type. See this.complete() for details.
+ * @param function [aCallback]
+ * Optional, function to invoke when completion results are received.
+ */
+ _updateCompletionResult:
+ function JST__updateCompletionResult(aType, aCallback)
+ {
+ if (this.lastCompletion.value == this.inputNode.value) {
+ return;
+ }
+
+ let requestId = gSequenceId();
+ let input = this.inputNode.value;
+ let cursor = this.inputNode.selectionStart;
+
+ // TODO: Bug 787986 - throttle/disable updates, deal with slow/high latency
+ // network connections.
+ this.lastCompletion = {
+ requestId: requestId,
+ completionType: aType,
+ value: null,
+ };
+
+ let callback = this._receiveAutocompleteProperties.bind(this, requestId,
+ aCallback);
+ this.webConsoleClient.autocomplete(input, cursor, callback);
+ },
+
+ /**
+ * Handler for the autocompletion results. This method takes
+ * the completion result received from the server and updates the UI
+ * accordingly.
+ *
+ * @param number aRequestId
+ * Request ID.
+ * @param function [aCallback=null]
+ * Optional, function to invoke when the completion result is received.
+ * @param object aMessage
+ * The JSON message which holds the completion results received from
+ * the content process.
+ */
+ _receiveAutocompleteProperties:
+ function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage)
+ {
+ let inputNode = this.inputNode;
+ let inputValue = inputNode.value;
+ if (this.lastCompletion.value == inputValue ||
+ aRequestId != this.lastCompletion.requestId) {
+ return;
+ }
+
+ let matches = aMessage.matches;
+ let lastPart = aMessage.matchProp;
+ if (!matches.length) {
+ this.clearCompletion();
+ return;
+ }
+
+ let items = matches.reverse().map(function(aMatch) {
+ return { preLabel: lastPart, label: aMatch };
+ });
+
+ let popup = this.autocompletePopup;
+ popup.setItems(items);
+
+ let completionType = this.lastCompletion.completionType;
+ this.lastCompletion = {
+ value: inputValue,
+ matchProp: lastPart,
+ };
+
+ if (items.length > 1 && !popup.isOpen) {
+ popup.openPopup(inputNode);
+ 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();
+ }
+
+ aCallback && aCallback(this);
+ },
+
+ onAutocompleteSelect: function JSTF_onAutocompleteSelect()
+ {
+ 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 JSTF_clearCompletion()
+ {
+ this.autocompletePopup.clearItems();
+ this.lastCompletion = { value: null };
+ this.updateCompleteNode("");
+ if (this.autocompletePopup.isOpen) {
+ 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 JSTF_acceptProposedCompletion()
+ {
+ let updated = false;
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix = currentItem.label.substring(this.lastCompletion.
+ matchProp.length);
+ this.setInputValue(this.inputNode.value + suffix);
+ updated = true;
+ }
+
+ this.clearCompletion();
+
+ return updated;
+ },
+
+ /**
+ * Update the node that displays the currently selected autocomplete proposal.
+ *
+ * @param string aSuffix
+ * The proposed suffix for the inputNode value.
+ */
+ updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
+ {
+ // completion prefix = input, with non-control chars replaced by spaces
+ let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
+ this.completeNode.value = prefix + aSuffix;
+ },
+
+ /**
+ * The click event handler for evaluation results in the output.
+ *
+ * @private
+ * @param object aResponse
+ * The JavaScript evaluation response received from the server.
+ */
+ _evalOutputClick: function JST__evalOutputClick(aResponse)
+ {
+ this.openVariablesView({
+ label: VariablesView.getString(aResponse.result),
+ objectActor: aResponse.result,
+ autofocus: true,
+ });
+ },
+
+ /**
+ * Destroy the sidebar.
+ * @private
+ */
+ _sidebarDestroy: function JST__sidebarDestroy()
+ {
+ 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 JST_destroy()
+ {
+ this._sidebarDestroy();
+
+ this.clearCompletion();
+ this.clearOutput();
+
+ this.autocompletePopup.destroy();
+ this.autocompletePopup = null;
+
+ let popup = this.hud.owner.chromeWindow.document
+ .getElementById("webConsole_autocompletePopup");
+ if (popup) {
+ popup.parentNode.removeChild(popup);
+ }
+
+ 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 = null;
+ },
+};
+
+/**
+ * Utils: a collection of globally used functions.
+ */
+var Utils = {
+ /**
+ * Flag to turn on and off scrolling.
+ */
+ scroll: true,
+
+ /**
+ * Scrolls a node so that it's visible in its containing XUL "scrollbox"
+ * element.
+ *
+ * @param nsIDOMNode aNode
+ * The node to make visible.
+ * @returns void
+ */
+ scrollToVisible: function Utils_scrollToVisible(aNode)
+ {
+ if (!this.scroll) {
+ return;
+ }
+
+ // Find the enclosing richlistbox node.
+ let richListBoxNode = aNode.parentNode;
+ while (richListBoxNode.tagName != "richlistbox") {
+ richListBoxNode = richListBoxNode.parentNode;
+ }
+
+ // Use the scroll box object interface to ensure the element is visible.
+ let boxObject = richListBoxNode.scrollBoxObject;
+ let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ nsIScrollBoxObject.ensureElementIsVisible(aNode);
+ },
+
+ /**
+ * Check if the given output node is scrolled to the bottom.
+ *
+ * @param nsIDOMNode aOutputNode
+ * @return boolean
+ * True if the output node is scrolled to the bottom, or false
+ * otherwise.
+ */
+ isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode)
+ {
+ let lastNodeHeight = aOutputNode.lastChild ?
+ aOutputNode.lastChild.clientHeight : 0;
+ let scrollBox = aOutputNode.scrollBoxObject.element;
+
+ return scrollBox.scrollTop + scrollBox.clientHeight >=
+ scrollBox.scrollHeight - lastNodeHeight / 2;
+ },
+
+ /**
+ * Determine the category of a given nsIScriptError.
+ *
+ * @param nsIScriptError aScriptError
+ * 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 Utils_categoryForScriptError(aScriptError)
+ {
+ switch (aScriptError.category) {
+ case "CSS Parser":
+ case "CSS Loader":
+ return CATEGORY_CSS;
+
+ case "Mixed Content Blocker":
+ case "CSP":
+ return CATEGORY_SECURITY;
+
+ default:
+ return CATEGORY_JS;
+ }
+ },
+
+ /**
+ * Retrieve the limit of messages for a specific category.
+ *
+ * @param number aCategory
+ * 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 Utils_logLimitForCategory(aCategory)
+ {
+ let logLimit = DEFAULT_LOG_LIMIT;
+
+ try {
+ let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
+ logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
+ logLimit = Math.max(logLimit, 1);
+ }
+ catch (e) { }
+
+ return logLimit;
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// CommandController
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A controller (an instance of nsIController) that makes editing actions
+ * behave appropriately in the context of the Web Console.
+ */
+function CommandController(aWebConsole)
+{
+ this.owner = aWebConsole;
+}
+
+CommandController.prototype = {
+ /**
+ * Copies the currently-selected entries in the Web Console output to the
+ * clipboard.
+ */
+ copy: function CommandController_copy()
+ {
+ this.owner.copySelectedItems();
+ },
+
+ /**
+ * Selects all the text in the HUD output.
+ */
+ selectAll: function CommandController_selectAll()
+ {
+ this.owner.outputNode.selectAll();
+ },
+
+ /**
+ * Open the URL of the selected message in a new tab.
+ */
+ openURL: function CommandController_openURL()
+ {
+ this.owner.openSelectedItemInTab();
+ },
+
+ copyURL: function CommandController_copyURL()
+ {
+ this.owner.copySelectedItems({ linkOnly: true });
+ },
+
+ supportsCommand: function CommandController_supportsCommand(aCommand)
+ {
+ return this.isCommandEnabled(aCommand);
+ },
+
+ isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_copy":
+ // Only enable "copy" if nodes are selected.
+ return this.owner.outputNode.selectedCount > 0;
+ case "consoleCmd_openURL":
+ case "consoleCmd_copyURL": {
+ // Only enable URL-related actions if node is Net Activity.
+ let selectedItem = this.owner.outputNode.selectedItem;
+ return selectedItem && "url" in selectedItem;
+ }
+ case "consoleCmd_clearOutput":
+ case "cmd_fontSizeEnlarge":
+ case "cmd_fontSizeReduce":
+ case "cmd_fontSizeReset":
+ case "cmd_selectAll":
+ case "cmd_find":
+ return true;
+ case "cmd_close":
+ return this.owner.owner._browserConsole;
+ }
+ return false;
+ },
+
+ doCommand: function CommandController_doCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_copy":
+ this.copy();
+ break;
+ case "consoleCmd_openURL":
+ this.openURL();
+ break;
+ case "consoleCmd_copyURL":
+ this.copyURL();
+ break;
+ case "consoleCmd_clearOutput":
+ this.owner.jsterm.clearOutput(true);
+ break;
+ case "cmd_find":
+ this.owner.filterBox.focus();
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ case "cmd_fontSizeEnlarge":
+ this.owner.changeFontSize("+");
+ break;
+ case "cmd_fontSizeReduce":
+ this.owner.changeFontSize("-");
+ break;
+ case "cmd_fontSizeReset":
+ this.owner.changeFontSize("");
+ break;
+ case "cmd_close":
+ this.owner.window.close();
+ 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 aWebConsole
+ * The Web Console instance that owns this connection proxy.
+ * @param RemoteTarget aTarget
+ * The target that the console will connect to.
+ */
+function WebConsoleConnectionProxy(aWebConsole, aTarget)
+{
+ this.owner = aWebConsole;
+ this.target = aTarget;
+
+ 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._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 instance.
+ *
+ * @see WebConsoleFrame
+ * @type object
+ */
+ owner: 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 WCCP_connect()
+ {
+ 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 promise = this._connectDefer.promise;
+ promise.then(function _onSucess() {
+ this._connectTimer.cancel();
+ this._connectTimer = null;
+ }.bind(this), function _onFailure() {
+ this._connectTimer = null;
+ }.bind(this));
+
+ let client = this.client = this.target.client;
+
+ client.addListener("logMessage", this._onLogMessage);
+ client.addListener("pageError", this._onPageError);
+ client.addListener("consoleAPICall", this._onConsoleAPICall);
+ client.addListener("networkEvent", this._onNetworkEvent);
+ client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
+ client.addListener("fileActivity", this._onFileActivity);
+ 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.chrome) {
+ let tab = this.target.form;
+ this.owner.onLocationChange(tab.url, tab.title);
+ }
+ this._attachConsole();
+
+ return promise;
+ },
+
+ /**
+ * Connection timeout handler.
+ * @private
+ */
+ _connectionTimeout: function WCCP__connectionTimeout()
+ {
+ let error = {
+ error: "timeout",
+ message: l10n.getStr("connectionTimeout"),
+ };
+
+ this._connectDefer.reject(error);
+ },
+
+ /**
+ * Attach to the Web Console actor.
+ * @private
+ */
+ _attachConsole: function WCCP__attachConsole()
+ {
+ let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
+ "FileActivity"];
+ this.client.attachConsole(this._consoleActor, listeners,
+ this._onAttachConsole);
+ },
+
+ /**
+ * The "attachConsole" response handler.
+ *
+ * @private
+ * @param object aResponse
+ * The JSON response object received from the server.
+ * @param object aWebConsoleClient
+ * The WebConsoleClient instance for the attached console, for the
+ * specific tab we work with.
+ */
+ _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient)
+ {
+ if (aResponse.error) {
+ Cu.reportError("attachConsole failed: " + aResponse.error + " " +
+ aResponse.message);
+ this._connectDefer.reject(aResponse);
+ return;
+ }
+
+ this.webConsoleClient = aWebConsoleClient;
+
+ this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
+
+ let msgs = ["PageError", "ConsoleAPI"];
+ this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
+ },
+
+ /**
+ * The "cachedMessages" response handler.
+ *
+ * @private
+ * @param object aResponse
+ * The JSON response object received from the server.
+ */
+ _onCachedMessages: function WCCP__onCachedMessages(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("Web Console getCachedMessages error: " + aResponse.error +
+ " " + aResponse.message);
+ this._connectDefer.reject(aResponse);
+ return;
+ }
+
+ if (!this._connectTimer) {
+ // This happens if the Promise is rejected (eg. a timeout), but the
+ // connection attempt is successful, nonetheless.
+ Cu.reportError("Web Console getCachedMessages error: invalid state.");
+ }
+
+ this.owner.displayCachedMessages(aResponse.messages);
+
+ if (!this._hasNativeConsoleAPI) {
+ this.owner.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 aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onPageError: function WCCP__onPageError(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handlePageError(aPacket.pageError);
+ }
+ },
+
+ /**
+ * The "logMessage" message type handler. We redirect any message to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onLogMessage: function WCCP__onLogMessage(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleLogMessage(aPacket);
+ }
+ },
+
+ /**
+ * The "consoleAPICall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleConsoleAPICall(aPacket.message);
+ }
+ },
+
+ /**
+ * The "networkEvent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleNetworkEvent(aPacket.eventActor);
+ }
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
+ {
+ if (this.owner) {
+ this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
+ aPacket);
+ }
+ },
+
+ /**
+ * The "fileActivity" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onFileActivity: function WCCP__onFileActivity(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleFileActivity(aPacket.uri);
+ }
+ },
+
+ /**
+ * The "lastPrivateContextExited" message type handler. When this message is
+ * received the Web Console UI is cleared.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onLastPrivateContextExited:
+ function WCCP__onLastPrivateContextExited(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.jsterm.clearPrivateMessages();
+ }
+ },
+
+ /**
+ * The "will-navigate" and "navigate" event handlers. We redirect any message
+ * to the UI for displaying.
+ *
+ * @private
+ * @param string aEvent
+ * Event type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket)
+ {
+ if (!this.owner) {
+ return;
+ }
+
+ if (aEvent == "will-navigate" && !this.owner.persistLog) {
+ this.owner.jsterm.clearOutput();
+ }
+
+ if (aPacket.url) {
+ this.owner.onLocationChange(aPacket.url, aPacket.title);
+ }
+
+ if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) {
+ this.owner.logWarningAboutReplacedAPI();
+ }
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string aActor
+ * The actor ID to send the request to.
+ */
+ releaseActor: function WCCP_releaseActor(aActor)
+ {
+ if (this.client) {
+ this.client.release(aActor);
+ }
+ },
+
+ /**
+ * Disconnect the Web Console from the remote server.
+ *
+ * @return object
+ * A Promise object that is resolved when disconnect completes.
+ */
+ disconnect: function WCCP_disconnect()
+ {
+ 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("networkEvent", this._onNetworkEvent);
+ this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
+ this.client.removeListener("fileActivity", this._onFileActivity);
+ this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
+ 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.owner = null;
+ this._disconnecter.resolve(null);
+
+ return this._disconnecter.promise;
+ },
+};
+
+function gSequenceId()
+{
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+
+function goUpdateConsoleCommands() {
+ goUpdateCommand("consoleCmd_openURL");
+ goUpdateCommand("consoleCmd_copyURL");
+}
+
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Context Menu
+///////////////////////////////////////////////////////////////////////////////
+
+const CONTEXTMENU_ID = "output-contextmenu";
+
+/*
+ * ConsoleContextMenu: This handle to show/hide a context menu item.
+ */
+let ConsoleContextMenu = {
+ /*
+ * Handle to show/hide context menu item.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ build: function CCM_build(aEvent)
+ {
+ let popup = aEvent.target;
+ if (popup.id !== CONTEXTMENU_ID) {
+ return;
+ }
+
+ let view = document.querySelector(".hud-output-node");
+ let metadata = this.getSelectionMetadata(view);
+
+ for (let i = 0, l = popup.childNodes.length; i < l; ++i) {
+ let element = popup.childNodes[i];
+ element.hidden = this.shouldHideMenuItem(element, metadata);
+ }
+ },
+
+ /*
+ * Get selection information from the view.
+ *
+ * @param nsIDOMElement aView
+ * This should be <xul:richlistbox>.
+ *
+ * @return object
+ * Selection metadata.
+ */
+ getSelectionMetadata: function CCM_getSelectionMetadata(aView)
+ {
+ let metadata = {
+ selectionType: "",
+ selection: new Set(),
+ };
+ let selectedItems = aView.selectedItems;
+
+ metadata.selectionType = (selectedItems > 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;
+ }
+ }
+
+ return metadata;
+ },
+
+ /*
+ * Determine if an item should be hidden.
+ *
+ * @param nsIDOMElement aMenuItem
+ * @param object aMetadata
+ * @return boolean
+ * Whether the given item should be hidden or not.
+ */
+ shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata)
+ {
+ let selectionType = aMenuItem.getAttribute("selectiontype");
+ if (selectionType && !aMetadata.selectionType == selectionType) {
+ return true;
+ }
+
+ let selection = aMenuItem.getAttribute("selection");
+ if (!selection) {
+ return false;
+ }
+
+ let shouldHide = true;
+ let itemData = selection.split("|");
+ for (let type of aMetadata.selection) {
+ // check whether this menu item should show or not.
+ if (itemData.indexOf(type) !== -1) {
+ shouldHide = false;
+ break;
+ }
+ }
+
+ return shouldHide;
+ },
+};
diff --git a/browser/devtools/webconsole/webconsole.xul b/browser/devtools/webconsole/webconsole.xul
new file mode 100644
index 000000000..b4e07aa49
--- /dev/null
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -0,0 +1,183 @@
+<?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://browser/locale/devtools/webConsole.dtd">
+%webConsoleDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/webconsole.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="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="webconsole.js"/>
+
+ <commandset id="editMenuCommands"/>
+
+ <commandset id="consoleCommands"
+ commandupdater="true"
+ events="richlistbox-select"
+ oncommandupdate="goUpdateConsoleCommands();">
+ <command id="consoleCmd_openURL"
+ oncommand="goDoCommand('consoleCmd_openURL');"/>
+ <command id="consoleCmd_copyURL"
+ oncommand="goDoCommand('consoleCmd_copyURL');"/>
+ <command id="consoleCmd_clearOutput"
+ oncommand="goDoCommand('consoleCmd_clearOutput');"/>
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find');"/>
+ <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');"/>
+ <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');"/>
+ <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');"/>
+ <command id="cmd_close" oncommand="goDoCommand('cmd_close');" disabled="true"/>
+ </commandset>
+ <keyset id="consoleKeys">
+ <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&findCmd.key;" command="cmd_find" modifiers="accel"/>
+ <key key="&clearOutputCmd.key;" command="consoleCmd_clearOutput" modifiers="accel"/>
+ <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
+ </keyset>
+ <keyset id="editMenuKeys"/>
+
+ <popupset id="mainPopupSet">
+ <menupopup id="output-contextmenu"
+ onpopupshowing="ConsoleContextMenu.build(event);">
+ <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;"
+ accesskey="&saveBodies.accesskey;"/>
+ <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="cMenu_copy"/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <box class="hud-outer-wrapper devtools-responsive-container" flex="1">
+ <vbox class="hud-console-wrapper" flex="1">
+ <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
+ <toolbarbutton label="&btnPageNet.label;" type="menu-button"
+ category="net" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageNet.tooltip;"
+#ifdef XP_MACOSX
+ accesskey="&btnPageNet.accesskeyMacOSX;"
+#else
+ accesskey="&btnPageNet.accesskey;"
+#endif
+ tabindex="3">
+ <menupopup>
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="network"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="networkinfo"/>
+ <menuseparator id="saveBodiesSeparator" />
+ <menuitem id="saveBodies" type="checkbox" label="&saveBodies.label;"
+ accesskey="&saveBodies.accesskey;"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageCSS.label;" type="menu-button"
+ category="css" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageCSS.tooltip;"
+ accesskey="&btnPageCSS.accesskey;"
+ tabindex="4">
+ <menupopup>
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="csserror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="cssparser"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageJS.label;" type="menu-button"
+ category="js" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageJS.tooltip;"
+ accesskey="&btnPageJS.accesskey;"
+ tabindex="5">
+ <menupopup>
+ <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="6">
+ <menupopup>
+ <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.accesskey;"
+ tabindex="7">
+ <menupopup>
+ <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"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
+ label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"
+ accesskey="&btnClear.accesskey;"
+ tabindex="8"/>
+
+ <spacer flex="1"/>
+
+ <textbox class="compact hud-filter-box devtools-searchinput" type="search"
+ placeholder="&filterOutput.placeholder;" tabindex="2"/>
+ </toolbar>
+
+ <richlistbox class="hud-output-node" orient="vertical" flex="1"
+ seltype="multiple" context="output-contextmenu"
+ style="direction:ltr;" tabindex="1"/>
+
+ <hbox class="jsterm-input-container" style="direction:ltr">
+ <stack class="jsterm-stack-node" flex="1">
+ <textbox class="jsterm-complete-node" multiline="true" rows="1"
+ tabindex="-1"/>
+ <textbox class="jsterm-input-node" multiline="true" rows="1" tabindex="0"/>
+ </stack>
+ </hbox>
+ </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>