From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../inspector/animation/actions/animations.js | 86 + .../client/inspector/animation/actions/index.js | 33 + .../client/inspector/animation/actions/moz.build | 8 + devtools/client/inspector/animation/animation.js | 802 + .../animation/components/AnimatedPropertyItem.js | 64 + .../animation/components/AnimatedPropertyList.js | 140 + .../components/AnimatedPropertyListContainer.js | 90 + .../animation/components/AnimatedPropertyName.js | 37 + .../components/AnimationDetailContainer.js | 90 + .../animation/components/AnimationDetailHeader.js | 52 + .../animation/components/AnimationItem.js | 121 + .../animation/components/AnimationList.js | 72 + .../animation/components/AnimationListContainer.js | 224 + .../animation/components/AnimationTarget.js | 182 + .../animation/components/AnimationToolbar.js | 75 + .../client/inspector/animation/components/App.js | 164 + .../animation/components/CurrentTimeLabel.js | 76 + .../animation/components/CurrentTimeScrubber.js | 131 + .../animation/components/KeyframesProgressBar.js | 108 + .../animation/components/NoAnimationPanel.js | 61 + .../animation/components/PauseResumeButton.js | 104 + .../animation/components/PlaybackRateSelector.js | 108 + .../components/ProgressInspectionPanel.js | 49 + .../inspector/animation/components/RewindButton.js | 38 + .../inspector/animation/components/TickLabels.js | 46 + .../inspector/animation/components/TickLines.js | 40 + .../animation/components/graph/AnimationName.js | 38 + .../components/graph/ComputedTimingPath.js | 104 + .../animation/components/graph/DelaySign.js | 42 + .../animation/components/graph/EffectTimingPath.js | 84 + .../animation/components/graph/EndDelaySign.js | 44 + .../components/graph/NegativeDelayPath.js | 27 + .../components/graph/NegativeEndDelayPath.js | 27 + .../animation/components/graph/NegativePath.js | 101 + .../animation/components/graph/SummaryGraph.js | 205 + .../animation/components/graph/SummaryGraphPath.js | 282 + .../animation/components/graph/TimingPath.js | 450 + .../inspector/animation/components/graph/moz.build | 17 + .../components/keyframes-graph/ColorPath.js | 209 + .../keyframes-graph/ComputedStylePath.js | 245 + .../components/keyframes-graph/DiscretePath.js | 67 + .../components/keyframes-graph/DistancePath.js | 34 + .../keyframes-graph/KeyframeMarkerItem.js | 33 + .../keyframes-graph/KeyframeMarkerList.js | 37 + .../components/keyframes-graph/KeyframesGraph.js | 52 + .../keyframes-graph/KeyframesGraphPath.js | 111 + .../animation/components/keyframes-graph/moz.build | 14 + .../inspector/animation/components/moz.build | 30 + .../inspector/animation/current-time-timer.js | 75 + devtools/client/inspector/animation/moz.build | 12 + .../inspector/animation/reducers/animations.js | 117 + .../client/inspector/animation/reducers/moz.build | 7 + .../client/inspector/animation/test/browser.toml | 221 + .../browser_animation_animated-property-list.js | 52 + ...ation_animated-property-list_unchanged-items.js | 58 + .../browser_animation_animated-property-name.js | 127 + ...wser_animation_animation-detail_close-button.js | 27 + .../browser_animation_animation-detail_title.js | 44 + ...rowser_animation_animation-detail_visibility.js | 52 + .../test/browser_animation_animation-list.js | 36 + ...nimation_animation-list_one-animation-select.js | 30 + .../browser_animation_animation-list_select.js | 38 + .../test/browser_animation_animation-target.js | 61 + ...browser_animation_animation-target_highlight.js | 118 + .../browser_animation_animation-target_select.js | 64 + .../browser_animation_animation-timeline-tick.js | 109 + ...animation_css-transition-with-playstate-idle.js | 81 + .../test/browser_animation_current-time-label.js | 73 + .../browser_animation_current-time-scrubber-rtl.js | 18 + ...on_current-time-scrubber-with-negative-delay.js | 48 + .../browser_animation_current-time-scrubber.js | 14 + ...bber_each-different-creation-time-animations.js | 34 + .../browser_animation_empty_on_invalid_nodes.js | 49 + .../browser_animation_fission_switch-target.js | 99 + .../test/browser_animation_indication-bar.js | 42 + ...tion_infinity-duration_current-time-scrubber.js | 40 + ...er_animation_infinity-duration_summary-graph.js | 147 + ...owser_animation_infinity-duration_tick-label.js | 29 + ...ation_keyframes-graph_computed-value-path-01.js | 164 + ...ation_keyframes-graph_computed-value-path-02.js | 90 + ...ation_keyframes-graph_computed-value-path-03.js | 190 + ...frames-graph_computed-value-path_easing-hint.js | 380 + ...nimation_keyframes-graph_keyframe-marker-rtl.js | 14 + ...er_animation_keyframes-graph_keyframe-marker.js | 13 + ...ser_animation_keyframes-graph_special-colors.js | 40 + .../browser_animation_keyframes-progress-bar.js | 103 + ...mation_keyframes-progress-bar_after-resuming.js | 64 + ...imation_logic_adjust-time-with-playback-rate.js | 50 + .../test/browser_animation_logic_adjust-time.js | 50 + .../test/browser_animation_logic_auto-stop.js | 94 + ...animation_logic_avoid-updating-during-hiding.js | 92 + .../test/browser_animation_logic_created-time.js | 57 + .../test/browser_animation_logic_mutations.js | 113 + ...ation_logic_mutations_add_remove_immediately.js | 29 + .../test/browser_animation_logic_mutations_fast.js | 24 + ...browser_animation_logic_mutations_properties.js | 103 + ...r_animation_logic_overflowed_delay_end-delay.js | 29 + .../test/browser_animation_logic_scroll-amount.js | 70 + .../test/browser_animation_pause-resume-button.js | 41 + ...owser_animation_pause-resume-button_end-time.js | 79 + ...r_animation_pause-resume-button_respectively.js | 96 + ...owser_animation_pause-resume-button_spacebar.js | 49 + .../browser_animation_playback-rate-selector.js | 81 + .../test/browser_animation_pseudo-element.js | 129 + .../test/browser_animation_rewind-button.js | 33 + .../test/browser_animation_short-duration.js | 43 + ...owser_animation_summary-graph_animation-name.js | 63 + .../browser_animation_summary-graph_compositor.js | 121 + ...imation_summary-graph_computed-timing-path_1.js | 208 + ...imation_summary-graph_computed-timing-path_2.js | 192 + ...aph_computed-timing-path_different-timescale.js | 43 + ...owser_animation_summary-graph_delay-sign-rtl.js | 14 + .../browser_animation_summary-graph_delay-sign.js | 13 + ...r_animation_summary-graph_effect-timing-path.js | 64 + ...r_animation_summary-graph_end-delay-sign-rtl.js | 15 + ...owser_animation_summary-graph_end-delay-sign.js | 13 + ...owser_animation_summary-graph_layout-by-seek.js | 150 + ..._animation_summary-graph_negative-delay-path.js | 128 + ...mation_summary-graph_negative-end-delay-path.js | 56 + .../browser_animation_summary-graph_tooltip.js | 294 + ...negative-playback-rate_current-time-scrubber.js | 47 + ..._timing_negative-playback-rate_summary-graph.js | 235 + .../animation/test/current-time-scrubber_head.js | 101 + .../animation/test/doc_custom_playback_rate.html | 30 + .../animation/test/doc_infinity_duration.html | 41 + .../animation/test/doc_multi_easings.html | 121 + .../animation/test/doc_multi_keyframes.html | 229 + .../animation/test/doc_multi_timings.html | 169 + .../test/doc_mutations_add_remove_immediately.html | 22 + .../animation/test/doc_mutations_fast.html | 53 + .../animation/test/doc_negative_playback_rate.html | 38 + .../test/doc_overflowed_delay_end_delay.html | 75 + .../inspector/animation/test/doc_pseudo.html | 91 + .../animation/test/doc_short_duration.html | 26 + .../animation/test/doc_simple_animation.html | 193 + .../animation/test/doc_special_colors.html | 28 + devtools/client/inspector/animation/test/head.js | 1028 ++ .../test/keyframes-graph_keyframe-marker_head.js | 237 + .../summary-graph_computed-timing-path_head.js | 103 + .../test/summary-graph_delay-sign_head.js | 106 + .../test/summary-graph_end-delay-sign_head.js | 100 + .../inspector/animation/utils/graph-helper.js | 332 + devtools/client/inspector/animation/utils/l10n.js | 46 + .../client/inspector/animation/utils/moz.build | 10 + .../client/inspector/animation/utils/timescale.js | 145 + devtools/client/inspector/animation/utils/utils.js | 70 + .../boxmodel/actions/box-model-highlighter.js | 86 + .../client/inspector/boxmodel/actions/box-model.js | 46 + .../client/inspector/boxmodel/actions/index.js | 21 + .../client/inspector/boxmodel/actions/moz.build | 11 + devtools/client/inspector/boxmodel/box-model.js | 446 + .../inspector/boxmodel/components/BoxModel.js | 97 + .../boxmodel/components/BoxModelEditable.js | 109 + .../inspector/boxmodel/components/BoxModelInfo.js | 79 + .../inspector/boxmodel/components/BoxModelMain.js | 774 + .../boxmodel/components/BoxModelProperties.js | 142 + .../boxmodel/components/ComputedProperty.js | 123 + .../client/inspector/boxmodel/components/moz.build | 14 + devtools/client/inspector/boxmodel/moz.build | 19 + .../inspector/boxmodel/reducers/box-model.js | 45 + .../client/inspector/boxmodel/reducers/moz.build | 9 + .../client/inspector/boxmodel/test/browser.toml | 72 + .../inspector/boxmodel/test/browser_boxmodel.js | 201 + ...xmodel_edit-position-visible-position-change.js | 56 + .../test/browser_boxmodel_editablemodel.js | 279 + ...browser_boxmodel_editablemodel_allproperties.js | 191 + .../browser_boxmodel_editablemodel_bluronclick.js | 97 + .../test/browser_boxmodel_editablemodel_border.js | 75 + .../test/browser_boxmodel_editablemodel_pseudo.js | 76 + .../browser_boxmodel_editablemodel_stylerules.js | 153 + .../boxmodel/test/browser_boxmodel_guides.js | 69 + .../test/browser_boxmodel_jump-to-rule-on-hover.js | 56 + .../browser_boxmodel_layout-accordion-state.js | 103 + .../boxmodel/test/browser_boxmodel_navigation.js | 200 + .../boxmodel/test/browser_boxmodel_offsetparent.js | 104 + .../boxmodel/test/browser_boxmodel_positions.js | 67 + .../boxmodel/test/browser_boxmodel_properties.js | 129 + .../test/browser_boxmodel_pseudo-element.js | 122 + .../browser_boxmodel_rotate-labels-on-sides.js | 54 + ..._boxmodel_show-tooltip-for-unassociated-rule.js | 50 + .../boxmodel/test/browser_boxmodel_sync.js | 39 + .../boxmodel/test/browser_boxmodel_tooltips.js | 166 + .../browser_boxmodel_update-after-navigation.js | 96 + .../test/browser_boxmodel_update-after-reload.js | 42 + .../test/browser_boxmodel_update-in-iframes.js | 84 + .../boxmodel/test/doc_boxmodel_iframe1.html | 3 + .../boxmodel/test/doc_boxmodel_iframe2.html | 3 + devtools/client/inspector/boxmodel/test/head.js | 122 + devtools/client/inspector/boxmodel/types.js | 21 + .../inspector/boxmodel/utils/editing-session.js | 188 + devtools/client/inspector/boxmodel/utils/moz.build | 9 + devtools/client/inspector/breadcrumbs.js | 973 + .../client/inspector/changes/ChangesContextMenu.js | 110 + devtools/client/inspector/changes/ChangesView.js | 284 + .../client/inspector/changes/actions/changes.js | 25 + devtools/client/inspector/changes/actions/index.js | 18 + .../client/inspector/changes/actions/moz.build | 10 + .../inspector/changes/components/CSSDeclaration.js | 47 + .../inspector/changes/components/ChangesApp.js | 241 + .../client/inspector/changes/components/moz.build | 10 + devtools/client/inspector/changes/moz.build | 24 + .../client/inspector/changes/reducers/changes.js | 385 + .../client/inspector/changes/reducers/moz.build | 9 + .../client/inspector/changes/selectors/changes.js | 261 + .../client/inspector/changes/selectors/moz.build | 9 + .../client/inspector/changes/test/browser.toml | 45 + .../test/browser_changes_background_tracking.js | 46 + .../test/browser_changes_copy_all_changes.js | 53 + .../test/browser_changes_copy_declaration.js | 67 + .../changes/test/browser_changes_copy_rule.js | 64 + ...er_changes_declaration_add_special_character.js | 78 + .../test/browser_changes_declaration_disable.js | 48 + .../test/browser_changes_declaration_duplicate.js | 107 + .../test/browser_changes_declaration_edit_value.js | 170 + .../browser_changes_declaration_identical_rules.js | 71 + .../test/browser_changes_declaration_remove.js | 43 + .../browser_changes_declaration_remove_ahead.js | 53 + .../browser_changes_declaration_remove_disabled.js | 107 + .../test/browser_changes_declaration_rename.js | 68 + .../changes/test/browser_changes_nested_rules.js | 189 + .../changes/test/browser_changes_rule_add.js | 64 + .../changes/test/browser_changes_rule_selector.js | 60 + devtools/client/inspector/changes/test/head.js | 93 + .../inspector/changes/test/xpcshell/.eslintrc.js | 6 + .../client/inspector/changes/test/xpcshell/head.js | 8 + .../inspector/changes/test/xpcshell/mocks.js | 67 + .../test/xpcshell/test_changes_stylesheet.js | 60 + .../inspector/changes/test/xpcshell/xpcshell.toml | 7 + .../inspector/changes/utils/changes-utils.js | 44 + devtools/client/inspector/changes/utils/l10n.js | 15 + devtools/client/inspector/changes/utils/moz.build | 10 + .../inspector/compatibility/CompatibilityView.js | 277 + devtools/client/inspector/compatibility/README.md | 25 + .../compatibility/actions/compatibility.js | 332 + .../inspector/compatibility/actions/index.js | 81 + .../inspector/compatibility/actions/moz.build | 10 + .../compatibility/components/BrowserIcon.js | 82 + .../compatibility/components/CompatibilityApp.js | 126 + .../inspector/compatibility/components/Footer.js | 85 + .../compatibility/components/IssueItem.js | 245 + .../compatibility/components/IssueList.js | 45 + .../compatibility/components/IssuePane.js | 55 + .../inspector/compatibility/components/NodeItem.js | 59 + .../inspector/compatibility/components/NodeList.js | 45 + .../inspector/compatibility/components/NodePane.js | 55 + .../inspector/compatibility/components/Settings.js | 197 + .../components/UnsupportedBrowserItem.js | 60 + .../components/UnsupportedBrowserList.js | 76 + .../inspector/compatibility/components/moz.build | 20 + devtools/client/inspector/compatibility/moz.build | 23 + .../compatibility/reducers/compatibility.js | 262 + .../inspector/compatibility/reducers/moz.build | 9 + .../compatibility/test/browser/browser.toml | 41 + .../browser_compatibility_css-property_issue.js | 92 + ...er_compatibility_dynamic_js-attribute-change.js | 126 + .../browser_compatibility_dynamic_js-dom-change.js | 149 + ...wser_compatibility_dynamic_markup-dom-change.js | 160 + ...patibility_dynamic_ruleview-attribute-change.js | 116 + .../browser_compatibility_event_document-reload.js | 95 + .../browser_compatibility_event_panel-select.js | 173 + .../browser_compatibility_event_rule-change.js | 179 + ...ser_compatibility_event_selected-node-change.js | 86 + ..._compatibility_event_top-level-target-change.js | 84 + .../browser/browser_compatibility_issue-node.js | 49 + .../test/browser/browser_compatibility_settings.js | 113 + .../test/browser/browser_compatibility_throbber.js | 69 + ...owser_compatibility_unsupported-browsers_all.js | 33 + ...wser_compatibility_unsupported-browsers_some.js | 47 + .../inspector/compatibility/test/browser/head.js | 281 + .../inspector/compatibility/test/node/.eslintrc.js | 10 + .../compatibility/test/node/babel.config.js | 14 + ...nts-compatibility-CompatibilityApp.test.js.snap | 84 + .../components-compatibility-Footer.test.js.snap | 36 + ...components-compatibility-IssueItem.test.js.snap | 348 + ...components-compatibility-IssueList.test.js.snap | 29 + ...components-compatibility-IssuePane.test.js.snap | 41 + .../components-compatibility-NodeItem.test.js.snap | 21 + .../components-compatibility-NodeList.test.js.snap | 45 + .../components-compatibility-NodePane.test.js.snap | 67 + .../components-compatibility-Settings.test.js.snap | 367 + ...mpatibility-UnsupportedBrowserItem.test.js.snap | 30 + ...mpatibility-UnsupportedBrowserList.test.js.snap | 118 + ...mponents-compatibility-CompatibilityApp.test.js | 54 + .../components-compatibility-Footer.test.js | 29 + .../components-compatibility-IssueItem.test.js | 167 + .../components-compatibility-IssueList.test.js | 47 + .../components-compatibility-IssuePane.test.js | 52 + .../components-compatibility-NodeItem.test.js | 31 + .../components-compatibility-NodeList.test.js | 57 + .../components-compatibility-NodePane.test.js | 57 + .../components-compatibility-Settings.test.js | 71 + ...ts-compatibility-UnsupportedBrowserItem.test.js | 42 + ...ts-compatibility-UnsupportedBrowserList.test.js | 56 + .../compatibility/test/node/jest.config.js | 14 + .../inspector/compatibility/test/node/package.json | 28 + .../inspector/compatibility/test/node/setup.js | 15 + .../inspector/compatibility/test/node/yarn.lock | 4334 +++++ .../compatibility/test/xpcshell/.eslintrc.js | 6 + .../inspector/compatibility/test/xpcshell/head.js | 10 + .../test/xpcshell/test_default-browsers.js | 27 + .../compatibility/test/xpcshell/xpcshell.toml | 7 + devtools/client/inspector/compatibility/types.js | 52 + .../client/inspector/compatibility/utils/cases.js | 22 + .../client/inspector/compatibility/utils/moz.build | 9 + .../inspector/components/InspectorTabPanel.css | 8 + .../inspector/components/InspectorTabPanel.js | 73 + devtools/client/inspector/components/moz.build | 9 + devtools/client/inspector/computed/computed.js | 1713 ++ devtools/client/inspector/computed/moz.build | 11 + .../client/inspector/computed/test/browser.toml | 86 + .../test/browser_computed_browser-styles.js | 59 + .../test/browser_computed_custom_properties.js | 106 + .../computed/test/browser_computed_cycle_color.js | 90 + .../computed/test/browser_computed_default_tab.js | 39 + .../computed/test/browser_computed_getNodeInfo.js | 176 + .../test/browser_computed_keybindings_01.js | 92 + .../test/browser_computed_keybindings_02.js | 69 + .../browser_computed_matched-selectors-order.js | 889 + .../browser_computed_matched-selectors-toggle.js | 125 + .../test/browser_computed_matched-selectors_01.js | 48 + .../test/browser_computed_matched-selectors_02.js | 40 + .../test/browser_computed_media-queries.js | 42 + .../browser_computed_no-results-placeholder.js | 69 + .../test/browser_computed_original-source-link.js | 71 + .../test/browser_computed_pseudo-element_01.js | 38 + .../browser_computed_refresh-on-ruleview-change.js | 93 + .../browser_computed_refresh-on-style-change_01.js | 32 + .../test/browser_computed_search-filter.js | 67 + .../test/browser_computed_search-filter_clear.js | 72 + .../browser_computed_search-filter_context-menu.js | 101 + ...owser_computed_search-filter_escape-keypress.js | 76 + .../browser_computed_search-filter_noproperties.js | 68 + .../browser_computed_select-and-copy-styles-01.js | 67 + .../browser_computed_select-and-copy-styles-02.js | 35 + .../computed/test/browser_computed_shadow_host.js | 74 + .../test/browser_computed_style-editor-link.js | 210 + .../computed/test/doc_matched_selectors.html | 54 + .../test/doc_matched_selectors_imported_1.css | 7 + .../test/doc_matched_selectors_imported_2.css | 7 + .../test/doc_matched_selectors_imported_3.css | 8 + .../test/doc_matched_selectors_imported_4.css | 7 + .../test/doc_matched_selectors_imported_5.css | 10 + .../test/doc_matched_selectors_imported_6.css | 7 + .../inspector/computed/test/doc_media_queries.html | 21 + .../inspector/computed/test/doc_pseudoelement.html | 131 + .../inspector/computed/test/doc_sourcemaps.css | 7 + .../inspector/computed/test/doc_sourcemaps.css.map | 7 + .../inspector/computed/test/doc_sourcemaps.html | 11 + .../inspector/computed/test/doc_sourcemaps.scss | 10 + devtools/client/inspector/computed/test/head.js | 279 + devtools/client/inspector/configs/development.json | 21 + .../client/inspector/extensions/actions/index.js | 24 + .../client/inspector/extensions/actions/moz.build | 10 + .../client/inspector/extensions/actions/sidebar.js | 58 + .../extensions/components/ExpressionResultView.js | 110 + .../extensions/components/ExtensionPage.js | 56 + .../extensions/components/ExtensionSidebar.js | 106 + .../extensions/components/ObjectTreeView.js | 67 + .../inspector/extensions/components/moz.build | 12 + .../inspector/extensions/extension-sidebar.js | 189 + devtools/client/inspector/extensions/moz.build | 18 + .../client/inspector/extensions/reducers/moz.build | 9 + .../inspector/extensions/reducers/sidebar.js | 67 + .../client/inspector/extensions/test/browser.toml | 15 + .../test/browser_inspector_extension_sidebar.js | 451 + devtools/client/inspector/extensions/test/head.js | 21 + .../test/head_devtools_inspector_sidebar.js | 224 + devtools/client/inspector/extensions/types.js | 15 + .../flexbox/actions/flexbox-highlighter.js | 39 + .../client/inspector/flexbox/actions/flexbox.js | 59 + devtools/client/inspector/flexbox/actions/index.js | 24 + .../client/inspector/flexbox/actions/moz.build | 11 + .../inspector/flexbox/components/FlexContainer.js | 125 + .../inspector/flexbox/components/FlexItem.js | 60 + .../inspector/flexbox/components/FlexItemList.js | 62 + .../flexbox/components/FlexItemSelector.js | 81 + .../flexbox/components/FlexItemSizingOutline.js | 173 + .../flexbox/components/FlexItemSizingProperties.js | 326 + .../client/inspector/flexbox/components/Flexbox.js | 128 + .../client/inspector/flexbox/components/Header.js | 142 + .../client/inspector/flexbox/components/moz.build | 16 + devtools/client/inspector/flexbox/flexbox.js | 569 + devtools/client/inspector/flexbox/moz.build | 18 + .../client/inspector/flexbox/reducers/flexbox.js | 90 + .../client/inspector/flexbox/reducers/index.js | 7 + .../client/inspector/flexbox/reducers/moz.build | 10 + devtools/client/inspector/flexbox/test/Ahem.ttf | Bin 0 -> 12480 bytes .../client/inspector/flexbox/test/browser.toml | 91 + .../test/browser_flexbox_accordion_state.js | 120 + .../test/browser_flexbox_container_and_item.js | 43 + ...r_flexbox_container_and_item_accordion_state.js | 107 + ...flexbox_container_and_item_updates_on_change.js | 54 + .../test/browser_flexbox_container_element_rep.js | 51 + .../test/browser_flexbox_container_properties.js | 59 + .../flexbox/test/browser_flexbox_empty_state.js | 24 + .../test/browser_flexbox_grand_parent_flex.js | 55 + ...wser_flexbox_highlighter_color_picker_on_ESC.js | 71 + ...r_flexbox_highlighter_color_picker_on_RETURN.js | 92 + ...browser_flexbox_highlighter_opened_telemetry.js | 37 + .../flexbox/test/browser_flexbox_item_list_01.js | 49 + .../flexbox/test/browser_flexbox_item_list_02.js | 35 + .../browser_flexbox_item_list_updates_on_change.js | 47 + .../test/browser_flexbox_item_outline_exists.js | 30 + ...wser_flexbox_item_outline_has_correct_layout.js | 67 + ...ser_flexbox_item_outline_hidden_when_useless.js | 46 + ..._outline_renders_basisfinal_points_correctly.js | 40 + ...wser_flexbox_item_outline_rotates_for_column.js | 53 + ..._outline_rotates_for_different_writing_modes.js | 42 + .../browser_flexbox_non_flex_item_is_not_shown.js | 30 + .../browser_flexbox_pseudo_elements_are_listed.js | 28 + ...izing_flexibility_not_displayed_when_useless.js | 48 + ...g_info_do_not_show_unspecified_min_dimension.js | 45 + .../test/browser_flexbox_sizing_info_exists.js | 36 + ...xbox_sizing_info_for_different_writing_modes.js | 75 + .../browser_flexbox_sizing_info_for_pseudos.js | 40 + .../browser_flexbox_sizing_info_for_text_nodes.js | 40 + ...ser_flexbox_sizing_info_has_correct_sections.js | 86 + ...zing_info_matches_properties_with_!important.js | 41 + ...rowser_flexbox_sizing_info_updates_on_change.js | 50 + ...lexbox_sizing_wanted_to_grow_but_was_clamped.js | 44 + .../test/browser_flexbox_text_nodes_are_listed.js | 28 + .../browser_flexbox_text_nodes_are_not_inlined.js | 52 + ...rowser_flexbox_toggle_flexbox_highlighter_01.js | 55 + ...rowser_flexbox_toggle_flexbox_highlighter_02.js | 102 + .../doc_flexbox_CSS_property_with_!important.html | 22 + .../flexbox/test/doc_flexbox_pseudos.html | 27 + .../flexbox/test/doc_flexbox_specific_cases.html | 121 + .../flexbox/test/doc_flexbox_text_nodes.html | 20 + .../test/doc_flexbox_unauthored_min_dimension.html | 29 + .../flexbox/test/doc_flexbox_writing_modes.html | 40 + devtools/client/inspector/flexbox/test/head.js | 81 + devtools/client/inspector/flexbox/types.js | 145 + .../client/inspector/fonts/actions/font-editor.js | 70 + .../client/inspector/fonts/actions/font-options.js | 21 + devtools/client/inspector/fonts/actions/fonts.js | 21 + devtools/client/inspector/fonts/actions/index.js | 42 + devtools/client/inspector/fonts/actions/moz.build | 12 + devtools/client/inspector/fonts/components/Font.js | 139 + .../client/inspector/fonts/components/FontAxis.js | 75 + .../inspector/fonts/components/FontEditor.js | 357 + .../client/inspector/fonts/components/FontList.js | 82 + .../client/inspector/fonts/components/FontName.js | 53 + .../inspector/fonts/components/FontOrigin.js | 79 + .../inspector/fonts/components/FontOverview.js | 80 + .../inspector/fonts/components/FontPreview.js | 40 + .../inspector/fonts/components/FontPreviewInput.js | 77 + .../fonts/components/FontPropertyValue.js | 434 + .../client/inspector/fonts/components/FontSize.js | 87 + .../client/inspector/fonts/components/FontStyle.js | 69 + .../inspector/fonts/components/FontWeight.js | 45 + .../client/inspector/fonts/components/FontsApp.js | 71 + .../inspector/fonts/components/LetterSpacing.js | 105 + .../inspector/fonts/components/LineHeight.js | 101 + .../client/inspector/fonts/components/moz.build | 24 + devtools/client/inspector/fonts/fonts.js | 1112 ++ devtools/client/inspector/fonts/moz.build | 19 + .../client/inspector/fonts/reducers/font-editor.js | 157 + .../inspector/fonts/reducers/font-options.js | 27 + devtools/client/inspector/fonts/reducers/fonts.js | 28 + devtools/client/inspector/fonts/reducers/moz.build | 11 + .../client/inspector/fonts/test/OstrichLicense.txt | 41 + devtools/client/inspector/fonts/test/browser.toml | 55 + .../inspector/fonts/test/browser_fontinspector.js | 94 + .../fonts/test/browser_fontinspector_all-fonts.js | 82 + .../fonts/test/browser_fontinspector_copy-URL.js | 27 + .../test/browser_fontinspector_edit-previews.js | 70 + ...er_fontinspector_editor-font-size-conversion.js | 85 + .../test/browser_fontinspector_editor-keywords.js | 44 + ...ntinspector_editor-letter-spacing-conversion.js | 71 + .../test/browser_fontinspector_editor-values.js | 36 + .../test/browser_fontinspector_expand-css-code.js | 34 + .../browser_fontinspector_font-type-telemetry.js | 20 + ...rowser_fontinspector_input-element-used-font.js | 23 + .../fonts/test/browser_fontinspector_no-fonts.js | 25 + .../test/browser_fontinspector_reveal-in-page.js | 100 + .../fonts/test/browser_fontinspector_text-node.js | 35 + .../test/browser_fontinspector_theme-change.js | 66 + .../fonts/test/doc_browser_fontinspector.html | 67 + .../test/doc_browser_fontinspector_iframe.html | 5 + devtools/client/inspector/fonts/test/head.js | 277 + .../client/inspector/fonts/test/ostrich-black.ttf | Bin 0 -> 12872 bytes .../inspector/fonts/test/ostrich-regular.ttf | Bin 0 -> 12476 bytes .../client/inspector/fonts/test/test_iframe.html | 11 + devtools/client/inspector/fonts/types.js | 109 + .../client/inspector/fonts/utils/font-utils.js | 111 + devtools/client/inspector/fonts/utils/l10n.js | 14 + devtools/client/inspector/fonts/utils/moz.build | 10 + .../inspector/grids/actions/grid-highlighter.js | 39 + devtools/client/inspector/grids/actions/grids.js | 55 + .../grids/actions/highlighter-settings.js | 52 + devtools/client/inspector/grids/actions/index.js | 30 + devtools/client/inspector/grids/actions/moz.build | 12 + devtools/client/inspector/grids/components/Grid.js | 106 + .../grids/components/GridDisplaySettings.js | 116 + .../client/inspector/grids/components/GridItem.js | 178 + .../client/inspector/grids/components/GridList.js | 79 + .../inspector/grids/components/GridOutline.js | 436 + .../client/inspector/grids/components/moz.build | 13 + devtools/client/inspector/grids/grid-inspector.js | 783 + devtools/client/inspector/grids/moz.build | 20 + devtools/client/inspector/grids/reducers/grids.js | 87 + .../grids/reducers/highlighter-settings.js | 54 + devtools/client/inspector/grids/reducers/moz.build | 10 + devtools/client/inspector/grids/test/browser.toml | 86 + .../grids/test/browser_grids_accordion-state.js | 108 + .../browser_grids_color-in-rules-grid-toggle.js | 93 + ...wser_grids_display-setting-extend-grid-lines.js | 59 + ...rowser_grids_display-setting-show-grid-areas.js | 59 + ...grids_display-setting-show-grid-line-numbers.js | 65 + .../browser_grids_grid-list-color-picker-on-ESC.js | 75 + ...owser_grids_grid-list-color-picker-on-RETURN.js | 94 + .../test/browser_grids_grid-list-element-rep.js | 68 + .../grids/test/browser_grids_grid-list-no-grids.js | 37 + .../browser_grids_grid-list-on-iframe-reloaded.js | 57 + ...er_grids_grid-list-on-mutation-element-added.js | 100 + ..._grids_grid-list-on-mutation-element-removed.js | 63 + ...wser_grids_grid-list-on-target-added-removed.js | 203 + .../browser_grids_grid-list-subgrids-z-order.js | 83 + .../test/browser_grids_grid-list-subgrids_01.js | 138 + .../test/browser_grids_grid-list-subgrids_02.js | 72 + .../browser_grids_grid-list-toggle-grids_01.js | 63 + .../browser_grids_grid-list-toggle-grids_02.js | 96 + ...rowser_grids_grid-list-toggle-multiple-grids.js | 243 + ...owser_grids_grid-outline-cannot-show-outline.js | 57 + .../browser_grids_grid-outline-highlight-area.js | 77 + .../browser_grids_grid-outline-highlight-cell.js | 65 + .../browser_grids_grid-outline-multiple-grids.js | 76 + .../browser_grids_grid-outline-selected-grid.js | 51 + ...er_grids_grid-outline-updates-on-grid-change.js | 64 + .../browser_grids_grid-outline-writing-mode.js | 156 + ..._grids_highlighter-setting-rules-grid-toggle.js | 75 + .../browser_grids_highlighter-toggle-telemetry.js | 63 + .../browser_grids_number-of-css-grids-telemetry.js | 56 + .../test/browser_grids_persist-color-palette.js | 58 + .../test/browser_grids_restored-after-reload.js | 115 + ...r_grids_restored-multiple-grids-after-reload.js | 156 + .../inspector/grids/test/doc_iframe_reloaded.html | 9 + .../client/inspector/grids/test/doc_subgrid.html | 56 + devtools/client/inspector/grids/test/head.js | 40 + .../inspector/grids/test/xpcshell/.eslintrc.js | 6 + .../client/inspector/grids/test/xpcshell/head.js | 10 + .../xpcshell/test_compare_fragments_geometry.js | 129 + .../inspector/grids/test/xpcshell/xpcshell.toml | 6 + devtools/client/inspector/grids/types.js | 57 + devtools/client/inspector/grids/utils/moz.build | 9 + devtools/client/inspector/grids/utils/utils.js | 58 + devtools/client/inspector/index.xhtml | 294 + devtools/client/inspector/inspector-search.js | 534 + devtools/client/inspector/inspector.js | 2031 +++ .../inspector/layout/components/LayoutApp.js | 202 + .../client/inspector/layout/components/moz.build | 9 + devtools/client/inspector/layout/layout.js | 136 + devtools/client/inspector/layout/moz.build | 17 + devtools/client/inspector/layout/utils/l10n.js | 17 + devtools/client/inspector/layout/utils/moz.build | 9 + .../client/inspector/markup/components/TextNode.js | 88 + .../client/inspector/markup/components/moz.build | 9 + .../client/inspector/markup/markup-context-menu.js | 950 + devtools/client/inspector/markup/markup.js | 2707 +++ devtools/client/inspector/markup/markup.xhtml | 43 + devtools/client/inspector/markup/moz.build | 19 + devtools/client/inspector/markup/test/browser.toml | 434 + .../browser_markup_accessibility_focus_blur.js | 77 + .../browser_markup_accessibility_navigation.js | 277 + ...r_markup_accessibility_navigation_after_edit.js | 126 + .../browser_markup_accessibility_new_selection.js | 34 + .../test/browser_markup_accessibility_semantics.js | 146 + .../markup/test/browser_markup_anonymous_01.js | 46 + .../markup/test/browser_markup_anonymous_03.js | 41 + .../markup/test/browser_markup_anonymous_04.js | 38 + .../markup/test/browser_markup_container_badge.js | 95 + .../markup/test/browser_markup_copy_html.js | 93 + .../markup/test/browser_markup_copy_image_data.js | 81 + ...ser_markup_css_completion_style_attribute_01.js | 90 + ...ser_markup_css_completion_style_attribute_02.js | 103 + ...ser_markup_css_completion_style_attribute_03.js | 52 + .../markup/test/browser_markup_display_node_01.js | 91 + .../markup/test/browser_markup_display_node_02.js | 216 + .../browser_markup_dom_mutation_breakpoints.js | 268 + .../test/browser_markup_dragdrop_autoscroll_01.js | 48 + .../test/browser_markup_dragdrop_autoscroll_02.js | 46 + ...browser_markup_dragdrop_before_marker_pseudo.js | 78 + .../test/browser_markup_dragdrop_distance.js | 48 + .../test/browser_markup_dragdrop_dragRootNode.js | 21 + .../test/browser_markup_dragdrop_draggable.js | 62 + .../test/browser_markup_dragdrop_escapeKeyPress.js | 38 + .../test/browser_markup_dragdrop_invalidNodes.js | 67 + .../markup/test/browser_markup_dragdrop_reorder.js | 111 + .../markup/test/browser_markup_dragdrop_tooltip.js | 36 + .../markup/test/browser_markup_events-overflow.js | 104 + .../test/browser_markup_events-windowed-host.js | 70 + .../markup/test/browser_markup_events_01.js | 132 + .../markup/test/browser_markup_events_02.js | 123 + .../markup/test/browser_markup_events_03.js | 88 + .../markup/test/browser_markup_events_04.js | 124 + .../test/browser_markup_events_chrome_blocked.js | 46 + .../browser_markup_events_chrome_not_blocked.js | 53 + .../test/browser_markup_events_click_to_close.js | 102 + .../test/browser_markup_events_jquery_1.0.js | 171 + .../test/browser_markup_events_jquery_1.1.js | 178 + .../test/browser_markup_events_jquery_1.11.1.js | 107 + .../test/browser_markup_events_jquery_1.2.js | 88 + .../test/browser_markup_events_jquery_1.3.js | 97 + .../test/browser_markup_events_jquery_1.4.js | 132 + .../test/browser_markup_events_jquery_1.6.js | 298 + .../test/browser_markup_events_jquery_1.7.js | 148 + .../test/browser_markup_events_jquery_2.1.1.js | 121 + .../browser_markup_events_keyboard_navigation.js | 145 + .../test/browser_markup_events_object_listener.js | 43 + ...owser_markup_events_react_development_15.4.1.js | 113 + ...r_markup_events_react_development_15.4.1_jsx.js | 116 + ...rowser_markup_events_react_production_15.3.1.js | 113 + ...er_markup_events_react_production_15.3.1_jsx.js | 116 + ...rowser_markup_events_react_production_16.2.0.js | 133 + ...er_markup_events_react_production_16.2.0_jsx.js | 114 + .../test/browser_markup_events_source_map.js | 54 + .../markup/test/browser_markup_events_toggle.js | 295 + .../test/browser_markup_flex_display_badge.js | 163 + .../browser_markup_flex_display_badge_telemetry.js | 53 + .../test/browser_markup_grid_display_badge_01.js | 91 + .../test/browser_markup_grid_display_badge_02.js | 256 + .../test/browser_markup_grid_display_badge_03.js | 82 + .../browser_markup_grid_display_badge_telemetry.js | 45 + .../markup/test/browser_markup_html_edit_01.js | 111 + .../markup/test/browser_markup_html_edit_02.js | 157 + .../markup/test/browser_markup_html_edit_03.js | 305 + .../markup/test/browser_markup_html_edit_04.js | 101 + .../test/browser_markup_html_edit_undo-redo.js | 88 + .../test/browser_markup_iframe_blocked_by_csp.js | 53 + .../markup/test/browser_markup_image_tooltip.js | 63 + .../test/browser_markup_image_tooltip_mutations.js | 95 + .../markup/test/browser_markup_keybindings_01.js | 48 + .../markup/test/browser_markup_keybindings_02.js | 31 + .../markup/test/browser_markup_keybindings_03.js | 64 + .../markup/test/browser_markup_keybindings_04.js | 71 + ...browser_markup_keybindings_delete_attributes.js | 73 + .../browser_markup_keybindings_scrolltonode.js | 100 + .../markup/test/browser_markup_links_01.js | 202 + .../markup/test/browser_markup_links_02.js | 40 + .../markup/test/browser_markup_links_03.js | 39 + .../markup/test/browser_markup_links_04.js | 150 + .../markup/test/browser_markup_links_05.js | 74 + .../markup/test/browser_markup_links_06.js | 60 + .../markup/test/browser_markup_links_07.js | 141 + .../test/browser_markup_links_aria_attributes.js | 129 + .../markup/test/browser_markup_load_01.js | 74 + .../markup/test/browser_markup_mutation_01.js | 421 + .../markup/test/browser_markup_mutation_02.js | 191 + .../markup/test/browser_markup_navigation.js | 128 + .../markup/test/browser_markup_node_names.js | 36 + .../test/browser_markup_node_names_namespaced.js | 54 + .../test/browser_markup_node_not_displayed_01.js | 37 + .../test/browser_markup_node_not_displayed_02.js | 147 + .../markup/test/browser_markup_overflow_badge.js | 101 + .../markup/test/browser_markup_pagesize_01.js | 91 + .../markup/test/browser_markup_pagesize_02.js | 48 + .../markup/test/browser_markup_pseudo_on_reload.js | 44 + .../test/browser_markup_remove_xul_attributes.js | 36 + .../markup/test/browser_markup_screenshot_node.js | 25 + .../browser_markup_screenshot_node_about_page.js | 46 + .../test/browser_markup_screenshot_node_iframe.js | 46 + .../browser_markup_screenshot_node_shadowdom.js | 41 + .../test/browser_markup_screenshot_node_warning.js | 38 + .../markup/test/browser_markup_scrollable_badge.js | 67 + .../test/browser_markup_scrollable_badge_click.js | 199 + .../markup/test/browser_markup_search_01.js | 56 + .../markup/test/browser_markup_shadowdom.js | 290 + .../test/browser_markup_shadowdom_clickreveal.js | 108 + .../browser_markup_shadowdom_clickreveal_scroll.js | 88 + .../test/browser_markup_shadowdom_copy_paths.js | 80 + .../markup/test/browser_markup_shadowdom_delete.js | 105 + .../test/browser_markup_shadowdom_dynamic.js | 155 + .../markup/test/browser_markup_shadowdom_hover.js | 78 + ...r_markup_shadowdom_marker_and_before_pseudos.js | 117 + .../test/browser_markup_shadowdom_maxchildren.js | 122 + .../browser_markup_shadowdom_mutations_shadow.js | 85 + .../test/browser_markup_shadowdom_navigation.js | 97 + ...browser_markup_shadowdom_nested_pick_inspect.js | 132 + .../markup/test/browser_markup_shadowdom_noslot.js | 107 + .../test/browser_markup_shadowdom_open_debugger.js | 152 + ...arkup_shadowdom_open_debugger_pretty_printed.js | 53 + .../browser_markup_shadowdom_shadowroot_mode.js | 52 + .../browser_markup_shadowdom_show_nodes_button.js | 52 + ...wser_markup_shadowdom_slotted_keyboard_focus.js | 72 + .../test/browser_markup_shadowdom_slotupdate.js | 68 + .../test/browser_markup_shadowdom_ua_widgets.js | 104 + ...browser_markup_shadowdom_ua_widgets_with_nac.js | 70 + .../test/browser_markup_subgrid_display_badge.js | 78 + .../browser_markup_tag_delete_whitespace_node.js | 79 + .../markup/test/browser_markup_tag_edit_01.js | 73 + .../markup/test/browser_markup_tag_edit_02.js | 46 + .../markup/test/browser_markup_tag_edit_03.js | 68 + .../test/browser_markup_tag_edit_04-backspace.js | 64 + .../test/browser_markup_tag_edit_04-delete.js | 64 + .../markup/test/browser_markup_tag_edit_05.js | 85 + .../markup/test/browser_markup_tag_edit_06.js | 95 + .../markup/test/browser_markup_tag_edit_07.js | 152 + .../markup/test/browser_markup_tag_edit_08.js | 140 + .../markup/test/browser_markup_tag_edit_09.js | 74 + .../markup/test/browser_markup_tag_edit_10.js | 39 + .../markup/test/browser_markup_tag_edit_11.js | 37 + .../markup/test/browser_markup_tag_edit_12.js | 103 + .../test/browser_markup_tag_edit_13-other.js | 39 + .../test/browser_markup_tag_edit_avoid_refocus.js | 51 + .../test/browser_markup_tag_edit_long-classname.js | 51 + .../markup/test/browser_markup_template.js | 55 + .../test/browser_markup_textcontent_display.js | 123 + .../test/browser_markup_textcontent_edit_01.js | 110 + .../test/browser_markup_textcontent_edit_02.js | 121 + .../markup/test/browser_markup_toggle_01.js | 59 + .../markup/test/browser_markup_toggle_02.js | 61 + .../markup/test/browser_markup_toggle_03.js | 51 + .../markup/test/browser_markup_toggle_04.js | 40 + .../test/browser_markup_toggle_closing_tag_line.js | 55 + .../test/browser_markup_update-on-navigtion.js | 57 + .../test/browser_markup_view-original-source.js | 55 + .../markup/test/browser_markup_view-source.js | 126 + .../test/browser_markup_void_elements_html.js | 60 + .../test/browser_markup_void_elements_xhtml.js | 38 + .../markup/test/browser_markup_whitespace.js | 106 + .../markup/test/doc_markup_anonymous.html | 32 + .../inspector/markup/test/doc_markup_dragdrop.html | 45 + .../test/doc_markup_dragdrop_autoscroll_01.html | 87 + .../test/doc_markup_dragdrop_autoscroll_02.html | 40 + .../inspector/markup/test/doc_markup_edit.html | 48 + .../markup/test/doc_markup_events-overflow.html | 19 + .../markup/test/doc_markup_events-source_map.html | 10 + .../markup/test/doc_markup_events_01.html | 118 + .../markup/test/doc_markup_events_02.html | 115 + .../markup/test/doc_markup_events_03.html | 103 + .../markup/test/doc_markup_events_04.html | 101 + .../test/doc_markup_events_chrome_listeners.html | 9 + .../markup/test/doc_markup_events_jquery.html | 79 + .../test/doc_markup_events_object_listener.html | 40 + ...doc_markup_events_react_development_15.4.1.html | 73 + ...markup_events_react_development_15.4.1_jsx.html | 51 + .../doc_markup_events_react_production_15.3.1.html | 73 + ..._markup_events_react_production_15.3.1_jsx.html | 51 + .../doc_markup_events_react_production_16.2.0.html | 80 + ..._markup_events_react_production_16.2.0_jsx.html | 50 + .../markup/test/doc_markup_events_toggle.html | 25 + .../inspector/markup/test/doc_markup_flashing.html | 15 + .../markup/test/doc_markup_html_mixed_case.html | 12 + .../markup/test/doc_markup_image_and_canvas.html | 24 + .../markup/test/doc_markup_image_and_canvas_2.html | 25 + .../inspector/markup/test/doc_markup_links.html | 46 + .../test/doc_markup_links_aria_attributes.html | 44 + .../inspector/markup/test/doc_markup_mutation.html | 42 + .../markup/test/doc_markup_navigation.html | 28 + .../markup/test/doc_markup_not_displayed.html | 18 + .../markup/test/doc_markup_pagesize_01.html | 32 + .../markup/test/doc_markup_pagesize_02.html | 33 + .../inspector/markup/test/doc_markup_pseudo.html | 11 + .../inspector/markup/test/doc_markup_search.html | 11 + ...kup_shadowdom_open_debugger_pretty_printed.html | 11 + .../inspector/markup/test/doc_markup_subgrid.html | 53 + .../markup/test/doc_markup_svg_attributes.html | 8 + .../inspector/markup/test/doc_markup_toggle.html | 28 + .../inspector/markup/test/doc_markup_tooltip.png | Bin 0 -> 1095 bytes .../test/doc_markup_update-on-navigtion_1.html | 1 + .../test/doc_markup_update-on-navigtion_2.html | 1 + .../test/doc_markup_view-original-source.html | 9 + .../markup/test/doc_markup_void_elements.html | 18 + .../markup/test/doc_markup_void_elements.xhtml | 21 + .../markup/test/doc_markup_whitespace.html | 25 + .../inspector/markup/test/doc_markup_xul.xhtml | 9 + .../client/inspector/markup/test/events_bundle.js | 94 + .../inspector/markup/test/events_bundle.js.map | 1 + .../inspector/markup/test/events_original.js | 15 + devtools/client/inspector/markup/test/head.js | 671 + .../markup/test/helper_attributes_test_runner.js | 161 + .../client/inspector/markup/test/helper_diff.js | 286 + .../markup/test/helper_events_test_runner.js | 297 + .../test/helper_markup_accessibility_navigation.js | 92 + .../markup/test/helper_outerhtml_test_runner.js | 99 + .../markup/test/helper_style_attr_test_runner.js | 151 + .../inspector/markup/test/lib_babel_6.21.0_min.js | 24 + .../client/inspector/markup/test/lib_jquery_1.0.js | 1814 ++ .../client/inspector/markup/test/lib_jquery_1.1.js | 2172 +++ .../inspector/markup/test/lib_jquery_1.11.1_min.js | 4 + .../inspector/markup/test/lib_jquery_1.2_min.js | 32 + .../inspector/markup/test/lib_jquery_1.3_min.js | 19 + .../inspector/markup/test/lib_jquery_1.4_min.js | 151 + .../inspector/markup/test/lib_jquery_1.6_min.js | 16 + .../inspector/markup/test/lib_jquery_1.7_min.js | 4 + .../inspector/markup/test/lib_jquery_2.1.1_min.js | 4 + .../inspector/markup/test/lib_react_16.2.0_min.js | 21 + .../markup/test/lib_react_dom_15.3.1_min.js | 12 + .../inspector/markup/test/lib_react_dom_15.4.1.js | 18239 +++++++++++++++++++ .../markup/test/lib_react_dom_16.2.0_min.js | 193 + .../test/lib_react_with_addons_15.3.1_min.js | 16 + .../markup/test/lib_react_with_addons_15.4.1.js | 5408 ++++++ .../markup/test/react_external_listeners.js | 10 + .../markup/test/shadowdom_open_debugger.min.js | 1 + devtools/client/inspector/markup/utils.js | 136 + devtools/client/inspector/markup/utils/l10n.js | 15 + devtools/client/inspector/markup/utils/moz.build | 9 + .../inspector/markup/views/element-container.js | 260 + .../inspector/markup/views/element-editor.js | 1213 ++ .../client/inspector/markup/views/html-editor.js | 177 + .../inspector/markup/views/markup-container.js | 900 + devtools/client/inspector/markup/views/moz.build | 19 + .../inspector/markup/views/read-only-container.js | 36 + .../inspector/markup/views/read-only-editor.js | 82 + .../inspector/markup/views/root-container.js | 60 + .../markup/views/slotted-node-container.js | 76 + .../inspector/markup/views/slotted-node-editor.js | 63 + .../inspector/markup/views/text-container.js | 44 + .../client/inspector/markup/views/text-editor.js | 143 + devtools/client/inspector/moz.build | 35 + devtools/client/inspector/node-picker.js | 313 + devtools/client/inspector/panel.js | 19 + devtools/client/inspector/rules/constants.js | 19 + .../client/inspector/rules/models/class-list.js | 271 + .../client/inspector/rules/models/element-style.js | 904 + devtools/client/inspector/rules/models/moz.build | 13 + devtools/client/inspector/rules/models/rule.js | 874 + .../client/inspector/rules/models/text-property.js | 400 + .../inspector/rules/models/user-properties.js | 85 + devtools/client/inspector/rules/moz.build | 25 + devtools/client/inspector/rules/rules.js | 2558 +++ .../client/inspector/rules/test/browser_part1.toml | 319 + .../client/inspector/rules/test/browser_part2.toml | 399 + .../browser_rules_add-property-and-reselect.js | 63 + .../test/browser_rules_add-property-cancel_01.js | 54 + .../test/browser_rules_add-property-cancel_02.js | 45 + .../test/browser_rules_add-property-cancel_03.js | 51 + .../test/browser_rules_add-property-commented.js | 50 + ...rowser_rules_add-property-invalid-identifier.js | 33 + .../rules/test/browser_rules_add-property-svg.js | 24 + .../rules/test/browser_rules_add-property_01.js | 31 + .../rules/test/browser_rules_add-property_02.js | 78 + .../test/browser_rules_add-rule-and-property.js | 31 + ...browser_rules_add-rule-and-remove-style-node.js | 43 + .../test/browser_rules_add-rule-button-state.js | 50 + .../rules/test/browser_rules_add-rule-csp.js | 40 + .../test/browser_rules_add-rule-edit-selector.js | 54 + .../rules/test/browser_rules_add-rule-iframes.js | 53 + .../browser_rules_add-rule-namespace-elements.js | 40 + .../test/browser_rules_add-rule-pseudo-class.js | 64 + ...r_rules_add-rule-then-property-edit-selector.js | 82 + .../rules/test/browser_rules_add-rule-with-menu.js | 44 + .../inspector/rules/test/browser_rules_add-rule.js | 46 + .../inspector/rules/test/browser_rules_authored.js | 73 + .../rules/test/browser_rules_authored_color.js | 78 + .../rules/test/browser_rules_authored_override.js | 56 + .../rules/test/browser_rules_blob_stylesheet.js | 26 + .../rules/test/browser_rules_class_panel_add.js | 108 + .../test/browser_rules_class_panel_autocomplete.js | 277 + .../test/browser_rules_class_panel_content.js | 124 + .../rules/test/browser_rules_class_panel_edit.js | 56 + .../browser_rules_class_panel_invalid_nodes.js | 51 + .../test/browser_rules_class_panel_mutation.js | 75 + .../browser_rules_class_panel_state_preserved.js | 41 + .../rules/test/browser_rules_class_panel_toggle.js | 63 + .../rules/test/browser_rules_colorUnit.js | 70 + .../test/browser_rules_color_scheme_simulation.js | 159 + ...rowser_rules_color_scheme_simulation_bfcache.js | 103 + .../browser_rules_color_scheme_simulation_meta.js | 82 + .../browser_rules_color_scheme_simulation_rdm.js | 90 + ...owser_rules_colorpicker-and-image-tooltip_01.js | 73 + ...owser_rules_colorpicker-and-image-tooltip_02.js | 66 + ...pears-on-swatch-click-or-keyboard-activation.js | 72 + .../browser_rules_colorpicker-commit-on-ENTER.js | 76 + .../browser_rules_colorpicker-contrast-ratio.js | 230 + .../browser_rules_colorpicker-edit-gradient.js | 82 + ...wser_rules_colorpicker-element-without-quads.js | 77 + ...owser_rules_colorpicker-hides-element-picker.js | 31 + .../browser_rules_colorpicker-hides-on-tooltip.js | 54 + .../browser_rules_colorpicker-multiple-changes.js | 110 + ...wser_rules_colorpicker-release-outside-frame.js | 79 + .../browser_rules_colorpicker-revert-on-ESC.js | 67 + .../browser_rules_colorpicker-swatch-displayed.js | 99 + ...rowser_rules_colorpicker-works-with-css-vars.js | 76 + .../test/browser_rules_colorpicker-wrap-focus.js | 76 + ...rowser_rules_completion-existing-property_01.js | 148 + ...rowser_rules_completion-existing-property_02.js | 136 + .../browser_rules_completion-new-property_01.js | 112 + .../browser_rules_completion-new-property_02.js | 137 + .../browser_rules_completion-new-property_03.js | 49 + .../browser_rules_completion-new-property_04.js | 77 + ...wser_rules_completion-new-property_multiline.js | 140 + .../test/browser_rules_completion-on-empty.js | 60 + ...les_completion-popup-hidden-after-navigation.js | 41 + .../test/browser_rules_completion-shortcut.js | 70 + .../rules/test/browser_rules_computed-lists_01.js | 60 + .../rules/test/browser_rules_computed-lists_02.js | 86 + .../rules/test/browser_rules_computed-lists_03.js | 42 + .../rules/test/browser_rules_conditional_import.js | 135 + .../rules/test/browser_rules_container-queries.js | 321 + .../rules/test/browser_rules_content_01.js | 147 + .../rules/test/browser_rules_content_02.js | 76 + .../rules/test/browser_rules_copy_styles.js | 359 + ...wser_rules_css-compatibility-add-rename-rule.js | 121 + ...rowser_rules_css-compatibility-check-add-fix.js | 130 + ...wser_rules_css-compatibility-learn-more-link.js | 64 + ...browser_rules_css-compatibility-toggle-rules.js | 146 + ...er_rules_css-compatibility-tooltip-telemetry.js | 54 + .../inspector/rules/test/browser_rules_cssom.js | 32 + ...er_rules_cubicbezier-appears-on-swatch-click.js | 79 + .../browser_rules_cubicbezier-commit-on-ENTER.js | 82 + .../browser_rules_cubicbezier-revert-on-ESC.js | 67 + .../inspector/rules/test/browser_rules_custom.js | 89 + .../rules/test/browser_rules_cycle-angle.js | 122 + .../rules/test/browser_rules_cycle-color.js | 225 + .../browser_rules_edit-display-grid-property.js | 53 + .../test/browser_rules_edit-property-cancel.js | 56 + .../test/browser_rules_edit-property-click.js | 60 + .../test/browser_rules_edit-property-commit.js | 101 + .../test/browser_rules_edit-property-computed.js | 108 + .../test/browser_rules_edit-property-increments.js | 820 + .../browser_rules_edit-property-nested-rules.js | 103 + .../test/browser_rules_edit-property-order.js | 130 + .../test/browser_rules_edit-property-remove_01.js | 66 + .../test/browser_rules_edit-property-remove_02.js | 66 + .../test/browser_rules_edit-property-remove_03.js | 84 + .../test/browser_rules_edit-property-remove_04.js | 45 + .../rules/test/browser_rules_edit-property_01.js | 165 + .../rules/test/browser_rules_edit-property_02.js | 143 + .../rules/test/browser_rules_edit-property_03.js | 49 + .../rules/test/browser_rules_edit-property_04.js | 83 + .../rules/test/browser_rules_edit-property_05.js | 72 + .../rules/test/browser_rules_edit-property_06.js | 68 + .../rules/test/browser_rules_edit-property_07.js | 79 + .../rules/test/browser_rules_edit-property_08.js | 62 + .../rules/test/browser_rules_edit-property_09.js | 80 + .../rules/test/browser_rules_edit-property_10.js | 51 + ...owser_rules_edit-selector-click-on-scrollbar.js | 96 + .../test/browser_rules_edit-selector-click.js | 84 + .../test/browser_rules_edit-selector-commit.js | 145 + .../browser_rules_edit-selector-nested-rules.js | 74 + .../rules/test/browser_rules_edit-selector_01.js | 66 + .../rules/test/browser_rules_edit-selector_02.js | 92 + .../rules/test/browser_rules_edit-selector_03.js | 55 + .../rules/test/browser_rules_edit-selector_04.js | 70 + .../rules/test/browser_rules_edit-selector_05.js | 77 + .../rules/test/browser_rules_edit-selector_06.js | 83 + .../rules/test/browser_rules_edit-selector_07.js | 67 + .../rules/test/browser_rules_edit-selector_08.js | 78 + .../rules/test/browser_rules_edit-selector_09.js | 110 + .../rules/test/browser_rules_edit-selector_10.js | 62 + .../rules/test/browser_rules_edit-selector_11.js | 67 + .../rules/test/browser_rules_edit-selector_12.js | 36 + .../browser_rules_edit-size-property-dragging.js | 481 + .../test/browser_rules_edit-value-after-name_01.js | 119 + .../test/browser_rules_edit-value-after-name_02.js | 80 + .../test/browser_rules_edit-value-after-name_03.js | 86 + .../test/browser_rules_edit-value-after-name_04.js | 76 + .../rules/test/browser_rules_edit-variable-add.js | 40 + .../test/browser_rules_edit-variable-remove.js | 42 + .../rules/test/browser_rules_edit-variable.js | 47 + .../test/browser_rules_editable-field-focus_01.js | 107 + .../test/browser_rules_editable-field-focus_02.js | 111 + .../rules/test/browser_rules_eyedropper.js | 160 + ...r_rules_filtereditor-appears-on-swatch-click.js | 69 + .../browser_rules_filtereditor-commit-on-ENTER.js | 51 + .../browser_rules_filtereditor-revert-on-ESC.js | 74 + ...rowser_rules_flexbox-highlighter-on-mutation.js | 55 + ...rowser_rules_flexbox-highlighter-on-navigate.js | 46 + .../browser_rules_flexbox-highlighter-on-reload.js | 57 + ...es_flexbox-highlighter-restored-after-reload.js | 69 + .../test/browser_rules_flexbox-toggle-telemetry.js | 53 + .../rules/test/browser_rules_flexbox-toggle_01.js | 89 + .../rules/test/browser_rules_flexbox-toggle_01b.js | 89 + .../rules/test/browser_rules_flexbox-toggle_02.js | 114 + .../rules/test/browser_rules_flexbox-toggle_03.js | 134 + .../rules/test/browser_rules_flexbox-toggle_04.js | 89 + .../test/browser_rules_font-family-parsing.js | 60 + .../browser_rules_grid-highlighter-on-mutation.js | 46 + .../browser_rules_grid-highlighter-on-navigate.js | 42 + .../browser_rules_grid-highlighter-on-reload.js | 55 + ...rules_grid-highlighter-restored-after-reload.js | 83 + .../test/browser_rules_grid-template-areas.js | 178 + .../test/browser_rules_grid-toggle-telemetry.js | 42 + .../rules/test/browser_rules_grid-toggle_01.js | 81 + .../rules/test/browser_rules_grid-toggle_01b.js | 81 + .../rules/test/browser_rules_grid-toggle_02.js | 95 + .../rules/test/browser_rules_grid-toggle_03.js | 138 + .../rules/test/browser_rules_grid-toggle_04.js | 81 + .../rules/test/browser_rules_grid-toggle_05.js | 139 + ...ser_rules_gridline-names-are-shown-correctly.js | 148 + .../browser_rules_gridline-names-autocomplete.js | 205 + .../rules/test/browser_rules_guessIndentation.js | 46 + .../test/browser_rules_highlight-element-rule.js | 49 + .../rules/test/browser_rules_highlight-property.js | 94 + .../test/browser_rules_highlight-used-fonts.js | 125 + .../test/browser_rules_imported_stylesheet_edit.js | 46 + .../browser_rules_inactive_css_display-justify.js | 47 + .../test/browser_rules_inactive_css_flexbox.js | 161 + .../rules/test/browser_rules_inactive_css_grid.js | 267 + .../test/browser_rules_inactive_css_inline.js | 75 + .../browser_rules_inactive_css_split-condition.js | 31 + .../test/browser_rules_inactive_css_visited.js | 67 + .../rules/test/browser_rules_inactive_css_xul.js | 43 + .../browser_rules_inherited-custom-properties.js | 88 + .../test/browser_rules_inherited-properties_01.js | 53 + .../test/browser_rules_inherited-properties_02.js | 35 + .../test/browser_rules_inherited-properties_03.js | 49 + .../test/browser_rules_inherited-properties_04.js | 30 + .../rules/test/browser_rules_inline-source-map.js | 25 + .../rules/test/browser_rules_inline-style-order.js | 86 + .../rules/test/browser_rules_invalid-source-map.js | 44 + .../inspector/rules/test/browser_rules_invalid.js | 32 + .../rules/test/browser_rules_keybindings.js | 301 + .../test/browser_rules_keyframeLineNumbers.js | 24 + .../test/browser_rules_keyframes-rule-shadowdom.js | 76 + .../rules/test/browser_rules_keyframes-rule_01.js | 123 + .../rules/test/browser_rules_keyframes-rule_02.js | 95 + .../browser_rules_large_base64_background_image.js | 73 + .../inspector/rules/test/browser_rules_layer.js | 115 + .../rules/test/browser_rules_lineNumbers.js | 31 + .../test/browser_rules_linear-easing-swatch.js | 487 + .../rules/test/browser_rules_livepreview.js | 76 + .../rules/test/browser_rules_mark_overridden_01.js | 57 + .../rules/test/browser_rules_mark_overridden_02.js | 48 + .../rules/test/browser_rules_mark_overridden_03.js | 33 + .../rules/test/browser_rules_mark_overridden_04.js | 34 + .../rules/test/browser_rules_mark_overridden_05.js | 30 + .../rules/test/browser_rules_mark_overridden_06.js | 65 + .../rules/test/browser_rules_mark_overridden_07.js | 93 + .../rules/test/browser_rules_mark_overridden_08.js | 51 + .../test/browser_rules_mark_overridden_layers.js | 166 + .../rules/test/browser_rules_mathml-element.js | 62 + .../rules/test/browser_rules_media-queries.js | 35 + .../test/browser_rules_media-queries_reload.js | 67 + ...browser_rules_multiple-properties-duplicates.js | 117 + .../browser_rules_multiple-properties-priority.js | 72 + ...wser_rules_multiple-properties-unfinished_01.js | 99 + ...wser_rules_multiple-properties-unfinished_02.js | 107 + .../test/browser_rules_multiple_properties_01.js | 84 + .../test/browser_rules_multiple_properties_02.js | 80 + .../rules/test/browser_rules_nested_at_rules.js | 85 + .../rules/test/browser_rules_nested_rules.js | 211 + .../rules/test/browser_rules_non_ascii.js | 37 + .../test/browser_rules_original-source-link.js | 118 + .../test/browser_rules_original-source-link2.js | 89 + .../test/browser_rules_preview-tooltips-sizes.js | 108 + .../test/browser_rules_print_media_simulation.js | 100 + .../rules/test/browser_rules_pseudo-element_01.js | 484 + .../rules/test/browser_rules_pseudo-element_02.js | 54 + .../rules/test/browser_rules_pseudo-visited.js | 31 + .../browser_rules_pseudo-visited_in_media-query.js | 23 + ...er_rules_pseudo-visited_with_style-attribute.js | 26 + .../test/browser_rules_pseudo_lock_options.js | 181 + .../rules/test/browser_rules_refresh-no-flicker.js | 48 + ...browser_rules_refresh-on-attribute-change_01.js | 71 + .../test/browser_rules_refresh-on-style-change.js | 43 + .../browser_rules_refresh-on-stylesheet-change.js | 105 + .../browser_rules_registered-custom-properties.js | 490 + ...browser_rules_search-filter-computed-list_01.js | 180 + ...browser_rules_search-filter-computed-list_02.js | 118 + ...browser_rules_search-filter-computed-list_03.js | 55 + ...browser_rules_search-filter-computed-list_04.js | 75 + ...r_rules_search-filter-computed-list_expander.js | 113 + ...ser_rules_search-filter-media-queries-layers.js | 195 + ...wser_rules_search-filter-overridden-property.js | 119 + .../rules/test/browser_rules_search-filter_01.js | 98 + .../rules/test/browser_rules_search-filter_02.js | 36 + .../rules/test/browser_rules_search-filter_03.js | 39 + .../rules/test/browser_rules_search-filter_04.js | 82 + .../rules/test/browser_rules_search-filter_05.js | 37 + .../rules/test/browser_rules_search-filter_06.js | 29 + .../rules/test/browser_rules_search-filter_07.js | 59 + .../rules/test/browser_rules_search-filter_08.js | 57 + .../rules/test/browser_rules_search-filter_09.js | 79 + .../rules/test/browser_rules_search-filter_10.js | 96 + .../browser_rules_search-filter_context-menu.js | 99 + .../browser_rules_search-filter_escape-keypress.js | 71 + .../test/browser_rules_select-and-copy-styles.js | 255 + ...ser_rules_selector-highlighter-iframe-picker.js | 60 + ...wser_rules_selector-highlighter-nested-rules.js | 130 + ...owser_rules_selector-highlighter-on-navigate.js | 35 + .../test/browser_rules_selector-highlighter_01.js | 86 + .../test/browser_rules_selector-highlighter_02.js | 48 + .../test/browser_rules_selector-highlighter_03.js | 64 + .../test/browser_rules_selector-highlighter_04.js | 35 + .../test/browser_rules_selector-highlighter_05.js | 46 + .../browser_rules_selector-highlighter_order.js | 56 + .../rules/test/browser_rules_selector_highlight.js | 152 + .../rules/test/browser_rules_selector_warnings.js | 154 + .../test/browser_rules_shadowdom_slot_rules.js | 102 + .../rules/test/browser_rules_shapes-toggle_01.js | 82 + .../rules/test/browser_rules_shapes-toggle_02.js | 61 + .../rules/test/browser_rules_shapes-toggle_03.js | 129 + .../rules/test/browser_rules_shapes-toggle_04.js | 48 + .../rules/test/browser_rules_shapes-toggle_05.js | 43 + .../rules/test/browser_rules_shapes-toggle_06.js | 104 + .../rules/test/browser_rules_shapes-toggle_07.js | 99 + ...ser_rules_shapes-toggle_basic-shapes-default.js | 102 + .../browser_rules_shorthand-overridden-lists.js | 85 + .../browser_rules_shorthand-overridden-lists_01.js | 35 + ..._rules_strict-search-filter-computed-list_01.js | 208 + .../test/browser_rules_strict-search-filter_01.js | 150 + .../test/browser_rules_strict-search-filter_02.js | 38 + .../test/browser_rules_strict-search-filter_03.js | 50 + .../rules/test/browser_rules_style-editor-link.js | 251 + .../test/browser_rules_update_mask_image_cors.js | 60 + .../test/browser_rules_url-click-opens-new-tab.js | 35 + .../rules/test/browser_rules_urls-clickable.js | 77 + .../browser_rules_user-agent-styles-uneditable.js | 62 + .../rules/test/browser_rules_user-agent-styles.js | 217 + .../test/browser_rules_user-property-reset.js | 105 + ...browser_rules_variables-in-pseudo-element_01.js | 45 + ...browser_rules_variables-in-pseudo-element_02.js | 44 + .../rules/test/browser_rules_variables_01.js | 72 + .../rules/test/browser_rules_variables_02.js | 321 + .../browser_rules_variables_03-case-sensitive.js | 32 + .../test/browser_rules_variables_04-valid-chars.js | 58 + .../test/browser_rules_variables_autocomplete.js | 131 + .../rules/test/browser_rules_variables_host.js | 76 + .../inspector/rules/test/doc_author-sheet.html | 34 + .../inspector/rules/test/doc_blob_stylesheet.html | 39 + .../rules/test/doc_class_panel_autocomplete.html | 63 + .../doc_class_panel_autocomplete_stylesheet.css | 42 + .../rules/test/doc_conditional_import.css | 3 + .../rules/test/doc_content_stylesheet.html | 35 + .../rules/test/doc_content_stylesheet_imported.css | 5 + .../test/doc_content_stylesheet_imported2.css | 3 + .../rules/test/doc_content_stylesheet_linked.css | 3 + .../rules/test/doc_content_stylesheet_script.css | 5 + .../client/inspector/rules/test/doc_copystyles.css | 11 + .../inspector/rules/test/doc_copystyles.html | 11 + .../client/inspector/rules/test/doc_cssom.html | 25 + .../client/inspector/rules/test/doc_custom.html | 33 + .../rules/test/doc_edit_imported_selector.html | 13 + .../client/inspector/rules/test/doc_filter.html | 13 + .../rules/test/doc_grid_area_gridline_names.html | 59 + .../inspector/rules/test/doc_grid_names.html | 17 + .../rules/test/doc_imported_anonymous_layer.css | 4 + .../rules/test/doc_imported_named_layer.css | 13 + .../rules/test/doc_imported_nested_named_layer.css | 5 + .../inspector/rules/test/doc_imported_no_layer.css | 3 + .../rules/test/doc_inactive_css_xul.xhtml | 15 + .../inspector/rules/test/doc_inline_sourcemap.html | 18 + .../inspector/rules/test/doc_invalid_sourcemap.css | 3 + .../rules/test/doc_invalid_sourcemap.html | 11 + .../rules/test/doc_keyframeLineNumbers.html | 45 + .../inspector/rules/test/doc_keyframeanimation.css | 84 + .../rules/test/doc_keyframeanimation.html | 13 + .../inspector/rules/test/doc_media_queries.html | 42 + .../rules/test/doc_print_media_simulation.html | 27 + .../inspector/rules/test/doc_pseudoelement.html | 188 + .../inspector/rules/test/doc_ruleLineNumbers.html | 19 + .../test/doc_rules_imported_stylesheet_edit.html | 4 + .../client/inspector/rules/test/doc_sourcemaps.css | 7 + .../inspector/rules/test/doc_sourcemaps.css.map | 7 + .../inspector/rules/test/doc_sourcemaps.html | 11 + .../inspector/rules/test/doc_sourcemaps.scss | 10 + .../inspector/rules/test/doc_sourcemaps2.css | 5 + .../rules/test/doc_sourcemaps2.css^headers^ | 1 + .../inspector/rules/test/doc_sourcemaps2.html | 11 + .../inspector/rules/test/doc_style_editor_link.css | 3 + .../client/inspector/rules/test/doc_test_image.png | Bin 0 -> 580 bytes .../inspector/rules/test/doc_urls_clickable.css | 9 + .../inspector/rules/test/doc_urls_clickable.html | 30 + .../inspector/rules/test/doc_variables_1.html | 23 + .../inspector/rules/test/doc_variables_2.html | 45 + .../inspector/rules/test/doc_variables_3.html | 16 + .../inspector/rules/test/doc_variables_4.html | 23 + .../client/inspector/rules/test/doc_visited.html | 27 + .../rules/test/doc_visited_in_media_query.html | 18 + .../test/doc_visited_with_style_attribute.html | 9 + devtools/client/inspector/rules/test/head.js | 1200 ++ .../rules/test/sjs_imported_stylesheet_edit.sjs | 53 + .../client/inspector/rules/test/square_svg.sjs | 13 + devtools/client/inspector/rules/types.js | 165 + devtools/client/inspector/rules/utils/l10n.js | 15 + devtools/client/inspector/rules/utils/moz.build | 10 + devtools/client/inspector/rules/utils/utils.js | 364 + .../inspector/rules/views/class-list-previewer.js | 310 + devtools/client/inspector/rules/views/moz.build | 10 + .../rules/views/registered-property-editor.js | 182 + .../client/inspector/rules/views/rule-editor.js | 1010 + .../inspector/rules/views/text-property-editor.js | 1637 ++ .../inspector/shared/highlighters-overlay.js | 2014 ++ devtools/client/inspector/shared/moz.build | 18 + devtools/client/inspector/shared/node-reps.js | 47 + devtools/client/inspector/shared/node-types.js | 22 + .../inspector/shared/style-change-tracker.js | 100 + .../inspector/shared/style-inspector-menu.js | 502 + devtools/client/inspector/shared/test/browser.toml | 44 + ...er_styleinspector_context-menu-copy-color_01.js | 85 + ...er_styleinspector_context-menu-copy-color_02.js | 115 + ...rowser_styleinspector_context-menu-copy-urls.js | 160 + .../test/browser_styleinspector_output-parser.js | 381 + .../browser_styleinspector_refresh_when_active.js | 50 + ...er_styleinspector_refresh_when_style_changes.js | 99 + ...wser_styleinspector_tooltip-background-image.js | 150 + ...yleinspector_tooltip-closes-on-new-selection.js | 80 + ...r_styleinspector_tooltip-longhand-fontfamily.js | 178 + ...inspector_tooltip-multiple-background-images.js | 68 + ..._styleinspector_tooltip-shorthand-fontfamily.js | 73 + .../test/browser_styleinspector_tooltip-size.js | 90 + ...wser_styleinspector_transform-highlighter-01.js | 53 + ...wser_styleinspector_transform-highlighter-02.js | 63 + ...wser_styleinspector_transform-highlighter-03.js | 115 + ...wser_styleinspector_transform-highlighter-04.js | 63 + .../shared/test/doc_content_style_changes.html | 28 + devtools/client/inspector/shared/test/head.js | 218 + .../client/inspector/shared/tooltips-overlay.js | 570 + devtools/client/inspector/shared/utils.js | 239 + .../inspector/shared/walker-event-listener.js | 86 + devtools/client/inspector/store.js | 56 + devtools/client/inspector/test/browser.toml | 445 + .../inspector/test/browser_inspector_addNode_01.js | 22 + .../inspector/test/browser_inspector_addNode_02.js | 76 + .../inspector/test/browser_inspector_addNode_03.js | 93 + .../test/browser_inspector_addSidebarTab.js | 63 + .../test/browser_inspector_breadcrumbs.js | 191 + ...rowser_inspector_breadcrumbs_highlight_hover.js | 69 + .../browser_inspector_breadcrumbs_keybinding.js | 82 + .../browser_inspector_breadcrumbs_keyboard_trap.js | 92 + .../browser_inspector_breadcrumbs_mutations.js | 282 + .../browser_inspector_breadcrumbs_namespaced.js | 70 + .../browser_inspector_breadcrumbs_shadowdom.js | 107 + .../browser_inspector_breadcrumbs_visibility.js | 114 + .../browser_inspector_delete-selected-node-01.js | 29 + .../browser_inspector_delete-selected-node-02.js | 145 + .../browser_inspector_delete-selected-node-03.js | 24 + .../test/browser_inspector_delete_node_in_frame.js | 33 + .../browser_inspector_destroy-after-navigation.js | 23 + .../test/browser_inspector_destroy-before-ready.js | 26 + .../test/browser_inspector_expand-collapse.js | 68 + .../test/browser_inspector_eyedropper_ruleview.js | 50 + .../test/browser_inspector_fission_frame.js | 38 + .../browser_inspector_fission_frame_navigation.js | 163 + .../browser_inspector_fission_switch_target.js | 31 + .../test/browser_inspector_highlighter-01.js | 43 + .../test/browser_inspector_highlighter-02.js | 49 + .../test/browser_inspector_highlighter-03.js | 125 + .../test/browser_inspector_highlighter-04.js | 55 + .../test/browser_inspector_highlighter-05.js | 72 + .../test/browser_inspector_highlighter-06.js | 39 + .../test/browser_inspector_highlighter-07.js | 90 + .../test/browser_inspector_highlighter-08.js | 67 + ...ser_inspector_highlighter-autohide-config_01.js | 36 + ...ser_inspector_highlighter-autohide-config_02.js | 45 + ...ser_inspector_highlighter-autohide-config_03.js | 79 + .../test/browser_inspector_highlighter-autohide.js | 77 + .../test/browser_inspector_highlighter-by-type.js | 73 + .../test/browser_inspector_highlighter-cancel.js | 94 + .../test/browser_inspector_highlighter-comments.js | 119 + .../browser_inspector_highlighter-cssgrid_01.js | 91 + .../browser_inspector_highlighter-cssgrid_02.js | 51 + .../browser_inspector_highlighter-cssshape_01.js | 95 + .../browser_inspector_highlighter-cssshape_02.js | 157 + .../browser_inspector_highlighter-cssshape_03.js | 116 + .../browser_inspector_highlighter-cssshape_04.js | 539 + .../browser_inspector_highlighter-cssshape_05.js | 158 + ...wser_inspector_highlighter-cssshape_06-scale.js | 187 + ..._inspector_highlighter-cssshape_06-translate.js | 127 + .../browser_inspector_highlighter-cssshape_07.js | 179 + ...ser_inspector_highlighter-cssshape_iframe_01.js | 97 + ...r_inspector_highlighter-cssshape_offset-path.js | 204 + ...owser_inspector_highlighter-cssshape_percent.js | 121 + ...rowser_inspector_highlighter-csstransform_01.js | 229 + ...rowser_inspector_highlighter-csstransform_02.js | 69 + ...browser_inspector_highlighter-custom-element.js | 30 + .../test/browser_inspector_highlighter-embed.js | 32 + ...r_inspector_highlighter-eyedropper-clipboard.js | 63 + ...browser_inspector_highlighter-eyedropper-csp.js | 39 + ...wser_inspector_highlighter-eyedropper-events.js | 184 + ...wser_inspector_highlighter-eyedropper-frames.js | 94 + ...owser_inspector_highlighter-eyedropper-image.js | 18 + ...owser_inspector_highlighter-eyedropper-label.js | 145 + ...r_inspector_highlighter-eyedropper-show-hide.js | 41 + ...browser_inspector_highlighter-eyedropper-xul.js | 74 + ...rowser_inspector_highlighter-eyedropper-zoom.js | 89 + .../browser_inspector_highlighter-geometry_01.js | 96 + .../browser_inspector_highlighter-geometry_02.js | 125 + .../browser_inspector_highlighter-geometry_03.js | 72 + .../browser_inspector_highlighter-geometry_04.js | 103 + .../browser_inspector_highlighter-geometry_05.js | 140 + .../browser_inspector_highlighter-geometry_06.js | 166 + .../browser_inspector_highlighter-geometry_07.js | 146 + ...tor_highlighter-geometry_hide_on_interaction.js | 80 + ...rowser_inspector_highlighter-geometry_iframe.js | 48 + .../test/browser_inspector_highlighter-hover_01.js | 34 + .../test/browser_inspector_highlighter-hover_02.js | 44 + .../test/browser_inspector_highlighter-hover_03.js | 60 + .../browser_inspector_highlighter-iframes_01.js | 90 + .../browser_inspector_highlighter-iframes_02.js | 80 + .../test/browser_inspector_highlighter-inline.js | 95 + .../browser_inspector_highlighter-keybinding_01.js | 71 + .../browser_inspector_highlighter-keybinding_02.js | 64 + .../browser_inspector_highlighter-keybinding_03.js | 69 + .../browser_inspector_highlighter-keybinding_04.js | 39 + ...ector_highlighter-keybinding_separate-window.js | 120 + ...ser_inspector_highlighter-measure-keybinding.js | 194 + .../browser_inspector_highlighter-measure_01.js | 92 + .../browser_inspector_highlighter-measure_02.js | 138 + .../browser_inspector_highlighter-measure_03.js | 114 + .../browser_inspector_highlighter-measure_04.js | 163 + .../test/browser_inspector_highlighter-options.js | 267 + .../test/browser_inspector_highlighter-preview.js | 72 + ...inspector_highlighter-reduced-motion-message.js | 104 + ...browser_inspector_highlighter-reduced-motion.js | 89 + .../test/browser_inspector_highlighter-reload.js | 36 + .../browser_inspector_highlighter-rulers_01.js | 116 + .../browser_inspector_highlighter-rulers_02.js | 201 + .../browser_inspector_highlighter-rulers_03.js | 117 + .../browser_inspector_highlighter-selector_01.js | 80 + .../browser_inspector_highlighter-selector_02.js | 84 + .../test/browser_inspector_highlighter-zoom.js | 83 + .../test/browser_inspector_iframe-navigation.js | 58 + ...r_inspector_iframe-picker-bfcache-navigation.js | 121 + .../test/browser_inspector_iframe-picker.js | 131 + .../inspector/test/browser_inspector_infobar_01.js | 113 + .../inspector/test/browser_inspector_infobar_02.js | 53 + .../inspector/test/browser_inspector_infobar_03.js | 59 + .../inspector/test/browser_inspector_infobar_04.js | 45 + .../inspector/test/browser_inspector_infobar_05.js | 119 + .../test/browser_inspector_infobar_textnode.js | 53 + .../test/browser_inspector_initialization.js | 114 + .../browser_inspector_inspect-object-element.js | 18 + .../browser_inspector_inspect_loading_document.js | 168 + .../test/browser_inspector_inspect_mutated_node.js | 72 + .../browser_inspector_inspect_node_contextmenu.js | 140 + ...er_inspector_inspect_node_contextmenu_nested.js | 152 + ...rowser_inspector_inspect_parent_process_page.js | 29 + .../inspector/test/browser_inspector_invalidate.js | 44 + ..._inspector_keyboard-shortcuts-copy-outerhtml.js | 53 + .../test/browser_inspector_keyboard-shortcuts.js | 54 + .../test/browser_inspector_menu-01-sensitivity.js | 388 + .../browser_inspector_menu-03-paste-items-svg.js | 46 + .../test/browser_inspector_menu-03-paste-items.js | 170 + .../browser_inspector_menu-04-use-in-console.js | 57 + .../browser_inspector_menu-05-attribute-items.js | 124 + .../test/browser_inspector_menu-06-other.js | 160 + .../test/browser_inspector_navigate_to_errors.js | 69 + .../inspector/test/browser_inspector_navigation.js | 88 + .../test/browser_inspector_open_on_neterror.js | 41 + .../test/browser_inspector_pane-toggle-01.js | 36 + .../test/browser_inspector_pane-toggle-02.js | 85 + .../test/browser_inspector_pane-toggle-03.js | 60 + .../test/browser_inspector_pane-toggle-04.js | 55 + .../test/browser_inspector_pane-toggle-05.js | 106 + ...owser_inspector_pane-toggle-layout-invariant.js | 32 + .../test/browser_inspector_pane_state_restore.js | 75 + .../browser_inspector_picker-reset-reference.js | 69 + .../test/browser_inspector_picker-shift-key.js | 94 + .../browser_inspector_picker-stop-on-eyedropper.js | 46 + ...browser_inspector_picker-stop-on-tool-change.js | 27 + .../browser_inspector_picker-useragent-widget.js | 74 + .../test/browser_inspector_portrait_mode.js | 82 + .../test/browser_inspector_pseudoclass-lock.js | 235 + .../test/browser_inspector_pseudoclass-menu.js | 60 + .../inspector/test/browser_inspector_reload-01.js | 30 + .../inspector/test/browser_inspector_reload-02.js | 47 + .../test/browser_inspector_reload_iframe.js | 48 + .../browser_inspector_reload_invalid_iframe.js | 63 + ...browser_inspector_reload_missing-iframe-node.js | 58 + .../test/browser_inspector_reload_nested_iframe.js | 50 + .../test/browser_inspector_reload_shadow_dom.js | 61 + .../inspector/test/browser_inspector_reload_xul.js | 48 + .../browser_inspector_remove-iframe-during-load.js | 83 + .../inspector/test/browser_inspector_search-01.js | 110 + .../inspector/test/browser_inspector_search-02.js | 155 + .../inspector/test/browser_inspector_search-03.js | 228 + .../inspector/test/browser_inspector_search-04.js | 115 + .../inspector/test/browser_inspector_search-05.js | 107 + .../inspector/test/browser_inspector_search-06.js | 103 + .../inspector/test/browser_inspector_search-07.js | 60 + .../inspector/test/browser_inspector_search-08.js | 75 + .../inspector/test/browser_inspector_search-09.js | 112 + .../inspector/test/browser_inspector_search-10.js | 51 + .../test/browser_inspector_search-clear.js | 59 + ...browser_inspector_search-filter_context-menu.js | 109 + .../test/browser_inspector_search-label.js | 33 + .../test/browser_inspector_search-navigation.js | 73 + .../test/browser_inspector_search-reserved.js | 137 + .../test/browser_inspector_search-selection.js | 83 + .../test/browser_inspector_search-sidebar.js | 91 + ...er_inspector_search-suggests-ids-and-classes.js | 154 + ..._inspector_search_keyboard_shortcut_conflict.js | 59 + .../test/browser_inspector_search_keyboard_trap.js | 95 + .../test/browser_inspector_select-last-selected.js | 75 + .../test/browser_inspector_sidebarstate.js | 132 + .../inspector/test/browser_inspector_startup.js | 84 + ...rowser_inspector_switch-to-inspector-on-pick.js | 123 + .../test/browser_inspector_textbox-menu.js | 103 + ...rowser_inspector_textbox-menu_reopen_toolbox.js | 51 + .../browser_inspector_use-in-console-conflict.js | 51 + .../inspector/test/doc_inspector_add_node.html | 22 + .../inspector/test/doc_inspector_breadcrumbs.html | 75 + .../test/doc_inspector_breadcrumbs_visibility.html | 22 + .../client/inspector/test/doc_inspector_csp.html | 9 + .../inspector/test/doc_inspector_csp.html^headers^ | 2 + .../doc_inspector_delete-selected-node-01.html | 4 + .../doc_inspector_delete-selected-node-02.html | 20 + .../client/inspector/test/doc_inspector_embed.html | 6 + .../test/doc_inspector_eyedropper_disabled.xhtml | 3 + .../doc_inspector_fission_frame_navigation.html | 15 + .../doc_inspector_highlight_after_transition.html | 26 + .../test/doc_inspector_highlighter-comments.html | 19 + .../doc_inspector_highlighter-geometry_01.html | 90 + .../doc_inspector_highlighter-geometry_02.html | 120 + .../inspector/test/doc_inspector_highlighter.html | 40 + ...oc_inspector_highlighter_cssshapes-percent.html | 18 + .../test/doc_inspector_highlighter_cssshapes.html | 93 + ...doc_inspector_highlighter_cssshapes_iframe.html | 11 + .../doc_inspector_highlighter_csstransform.html | 25 + .../doc_inspector_highlighter_custom_element.xhtml | 20 + .../test/doc_inspector_highlighter_dom.html | 20 + .../test/doc_inspector_highlighter_inline.html | 36 + .../test/doc_inspector_highlighter_rect.html | 22 + .../doc_inspector_highlighter_rect_iframe.html | 15 + .../test/doc_inspector_highlighter_scroll.html | 15 + .../inspector/test/doc_inspector_infobar.html | 43 + .../inspector/test/doc_inspector_infobar_01.html | 44 + .../inspector/test/doc_inspector_infobar_02.html | 34 + .../inspector/test/doc_inspector_infobar_03.html | 14 + .../inspector/test/doc_inspector_infobar_04.html | 41 + .../test/doc_inspector_infobar_textnode.html | 14 + .../inspector/test/doc_inspector_long-divs.html | 104 + .../client/inspector/test/doc_inspector_menu.html | 38 + .../inspector/test/doc_inspector_outerhtml.html | 11 + ...doc_inspector_pane-toggle-layout-invariant.html | 27 + .../inspector/test/doc_inspector_reload_xul.xhtml | 9 + .../doc_inspector_remove-iframe-during-load.html | 44 + .../test/doc_inspector_search-iframes.html | 13 + .../test/doc_inspector_search-reserved.html | 11 + .../test/doc_inspector_search-suggestions.html | 27 + .../inspector/test/doc_inspector_search-svg.html | 16 + .../inspector/test/doc_inspector_search.html | 26 + .../doc_inspector_select-last-selected-01.html | 21 + .../doc_inspector_select-last-selected-02.html | 10 + .../client/inspector/test/doc_inspector_svg.svg | 3 + devtools/client/inspector/test/head.js | 1499 ++ ...wser_inspector_highlighter-eyedropper-image.png | Bin 0 -> 522 bytes devtools/client/inspector/test/shared-head.js | 1158 ++ .../inspector/test/sjs_slow-loading-image.sjs | 33 + .../client/inspector/test/style_inspector_csp.css | 3 + .../test/style_inspector_eyedropper_ruleview.css | 3 + devtools/client/inspector/toolsidebar.js | 326 + 1435 files changed, 180120 insertions(+) create mode 100644 devtools/client/inspector/animation/actions/animations.js create mode 100644 devtools/client/inspector/animation/actions/index.js create mode 100644 devtools/client/inspector/animation/actions/moz.build create mode 100644 devtools/client/inspector/animation/animation.js create mode 100644 devtools/client/inspector/animation/components/AnimatedPropertyItem.js create mode 100644 devtools/client/inspector/animation/components/AnimatedPropertyList.js create mode 100644 devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js create mode 100644 devtools/client/inspector/animation/components/AnimatedPropertyName.js create mode 100644 devtools/client/inspector/animation/components/AnimationDetailContainer.js create mode 100644 devtools/client/inspector/animation/components/AnimationDetailHeader.js create mode 100644 devtools/client/inspector/animation/components/AnimationItem.js create mode 100644 devtools/client/inspector/animation/components/AnimationList.js create mode 100644 devtools/client/inspector/animation/components/AnimationListContainer.js create mode 100644 devtools/client/inspector/animation/components/AnimationTarget.js create mode 100644 devtools/client/inspector/animation/components/AnimationToolbar.js create mode 100644 devtools/client/inspector/animation/components/App.js create mode 100644 devtools/client/inspector/animation/components/CurrentTimeLabel.js create mode 100644 devtools/client/inspector/animation/components/CurrentTimeScrubber.js create mode 100644 devtools/client/inspector/animation/components/KeyframesProgressBar.js create mode 100644 devtools/client/inspector/animation/components/NoAnimationPanel.js create mode 100644 devtools/client/inspector/animation/components/PauseResumeButton.js create mode 100644 devtools/client/inspector/animation/components/PlaybackRateSelector.js create mode 100644 devtools/client/inspector/animation/components/ProgressInspectionPanel.js create mode 100644 devtools/client/inspector/animation/components/RewindButton.js create mode 100644 devtools/client/inspector/animation/components/TickLabels.js create mode 100644 devtools/client/inspector/animation/components/TickLines.js create mode 100644 devtools/client/inspector/animation/components/graph/AnimationName.js create mode 100644 devtools/client/inspector/animation/components/graph/ComputedTimingPath.js create mode 100644 devtools/client/inspector/animation/components/graph/DelaySign.js create mode 100644 devtools/client/inspector/animation/components/graph/EffectTimingPath.js create mode 100644 devtools/client/inspector/animation/components/graph/EndDelaySign.js create mode 100644 devtools/client/inspector/animation/components/graph/NegativeDelayPath.js create mode 100644 devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js create mode 100644 devtools/client/inspector/animation/components/graph/NegativePath.js create mode 100644 devtools/client/inspector/animation/components/graph/SummaryGraph.js create mode 100644 devtools/client/inspector/animation/components/graph/SummaryGraphPath.js create mode 100644 devtools/client/inspector/animation/components/graph/TimingPath.js create mode 100644 devtools/client/inspector/animation/components/graph/moz.build create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js create mode 100644 devtools/client/inspector/animation/components/keyframes-graph/moz.build create mode 100644 devtools/client/inspector/animation/components/moz.build create mode 100644 devtools/client/inspector/animation/current-time-timer.js create mode 100644 devtools/client/inspector/animation/moz.build create mode 100644 devtools/client/inspector/animation/reducers/animations.js create mode 100644 devtools/client/inspector/animation/reducers/moz.build create mode 100644 devtools/client/inspector/animation/test/browser.toml create mode 100644 devtools/client/inspector/animation/test/browser_animation_animated-property-list.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animated-property-name.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-list.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-list_select.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-target.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-target_select.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_current-time-label.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_indication-bar.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_created-time.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_mutations.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_pseudo-element.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_rewind-button.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_short-duration.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js create mode 100644 devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js create mode 100644 devtools/client/inspector/animation/test/current-time-scrubber_head.js create mode 100644 devtools/client/inspector/animation/test/doc_custom_playback_rate.html create mode 100644 devtools/client/inspector/animation/test/doc_infinity_duration.html create mode 100644 devtools/client/inspector/animation/test/doc_multi_easings.html create mode 100644 devtools/client/inspector/animation/test/doc_multi_keyframes.html create mode 100644 devtools/client/inspector/animation/test/doc_multi_timings.html create mode 100644 devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html create mode 100644 devtools/client/inspector/animation/test/doc_mutations_fast.html create mode 100644 devtools/client/inspector/animation/test/doc_negative_playback_rate.html create mode 100644 devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html create mode 100644 devtools/client/inspector/animation/test/doc_pseudo.html create mode 100644 devtools/client/inspector/animation/test/doc_short_duration.html create mode 100644 devtools/client/inspector/animation/test/doc_simple_animation.html create mode 100644 devtools/client/inspector/animation/test/doc_special_colors.html create mode 100644 devtools/client/inspector/animation/test/head.js create mode 100644 devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js create mode 100644 devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js create mode 100644 devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js create mode 100644 devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js create mode 100644 devtools/client/inspector/animation/utils/graph-helper.js create mode 100644 devtools/client/inspector/animation/utils/l10n.js create mode 100644 devtools/client/inspector/animation/utils/moz.build create mode 100644 devtools/client/inspector/animation/utils/timescale.js create mode 100644 devtools/client/inspector/animation/utils/utils.js create mode 100644 devtools/client/inspector/boxmodel/actions/box-model-highlighter.js create mode 100644 devtools/client/inspector/boxmodel/actions/box-model.js create mode 100644 devtools/client/inspector/boxmodel/actions/index.js create mode 100644 devtools/client/inspector/boxmodel/actions/moz.build create mode 100644 devtools/client/inspector/boxmodel/box-model.js create mode 100644 devtools/client/inspector/boxmodel/components/BoxModel.js create mode 100644 devtools/client/inspector/boxmodel/components/BoxModelEditable.js create mode 100644 devtools/client/inspector/boxmodel/components/BoxModelInfo.js create mode 100644 devtools/client/inspector/boxmodel/components/BoxModelMain.js create mode 100644 devtools/client/inspector/boxmodel/components/BoxModelProperties.js create mode 100644 devtools/client/inspector/boxmodel/components/ComputedProperty.js create mode 100644 devtools/client/inspector/boxmodel/components/moz.build create mode 100644 devtools/client/inspector/boxmodel/moz.build create mode 100644 devtools/client/inspector/boxmodel/reducers/box-model.js create mode 100644 devtools/client/inspector/boxmodel/reducers/moz.build create mode 100644 devtools/client/inspector/boxmodel/test/browser.toml create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js create mode 100644 devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js create mode 100644 devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html create mode 100644 devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html create mode 100644 devtools/client/inspector/boxmodel/test/head.js create mode 100644 devtools/client/inspector/boxmodel/types.js create mode 100644 devtools/client/inspector/boxmodel/utils/editing-session.js create mode 100644 devtools/client/inspector/boxmodel/utils/moz.build create mode 100644 devtools/client/inspector/breadcrumbs.js create mode 100644 devtools/client/inspector/changes/ChangesContextMenu.js create mode 100644 devtools/client/inspector/changes/ChangesView.js create mode 100644 devtools/client/inspector/changes/actions/changes.js create mode 100644 devtools/client/inspector/changes/actions/index.js create mode 100644 devtools/client/inspector/changes/actions/moz.build create mode 100644 devtools/client/inspector/changes/components/CSSDeclaration.js create mode 100644 devtools/client/inspector/changes/components/ChangesApp.js create mode 100644 devtools/client/inspector/changes/components/moz.build create mode 100644 devtools/client/inspector/changes/moz.build create mode 100644 devtools/client/inspector/changes/reducers/changes.js create mode 100644 devtools/client/inspector/changes/reducers/moz.build create mode 100644 devtools/client/inspector/changes/selectors/changes.js create mode 100644 devtools/client/inspector/changes/selectors/moz.build create mode 100644 devtools/client/inspector/changes/test/browser.toml create mode 100644 devtools/client/inspector/changes/test/browser_changes_background_tracking.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_copy_all_changes.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_copy_declaration.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_copy_rule.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_add_special_character.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_disable.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_duplicate.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_edit_value.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_identical_rules.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_remove.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_remove_ahead.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_remove_disabled.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_declaration_rename.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_nested_rules.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_rule_add.js create mode 100644 devtools/client/inspector/changes/test/browser_changes_rule_selector.js create mode 100644 devtools/client/inspector/changes/test/head.js create mode 100644 devtools/client/inspector/changes/test/xpcshell/.eslintrc.js create mode 100644 devtools/client/inspector/changes/test/xpcshell/head.js create mode 100644 devtools/client/inspector/changes/test/xpcshell/mocks.js create mode 100644 devtools/client/inspector/changes/test/xpcshell/test_changes_stylesheet.js create mode 100644 devtools/client/inspector/changes/test/xpcshell/xpcshell.toml create mode 100644 devtools/client/inspector/changes/utils/changes-utils.js create mode 100644 devtools/client/inspector/changes/utils/l10n.js create mode 100644 devtools/client/inspector/changes/utils/moz.build create mode 100644 devtools/client/inspector/compatibility/CompatibilityView.js create mode 100644 devtools/client/inspector/compatibility/README.md create mode 100644 devtools/client/inspector/compatibility/actions/compatibility.js create mode 100644 devtools/client/inspector/compatibility/actions/index.js create mode 100644 devtools/client/inspector/compatibility/actions/moz.build create mode 100644 devtools/client/inspector/compatibility/components/BrowserIcon.js create mode 100644 devtools/client/inspector/compatibility/components/CompatibilityApp.js create mode 100644 devtools/client/inspector/compatibility/components/Footer.js create mode 100644 devtools/client/inspector/compatibility/components/IssueItem.js create mode 100644 devtools/client/inspector/compatibility/components/IssueList.js create mode 100644 devtools/client/inspector/compatibility/components/IssuePane.js create mode 100644 devtools/client/inspector/compatibility/components/NodeItem.js create mode 100644 devtools/client/inspector/compatibility/components/NodeList.js create mode 100644 devtools/client/inspector/compatibility/components/NodePane.js create mode 100644 devtools/client/inspector/compatibility/components/Settings.js create mode 100644 devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js create mode 100644 devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js create mode 100644 devtools/client/inspector/compatibility/components/moz.build create mode 100644 devtools/client/inspector/compatibility/moz.build create mode 100644 devtools/client/inspector/compatibility/reducers/compatibility.js create mode 100644 devtools/client/inspector/compatibility/reducers/moz.build create mode 100644 devtools/client/inspector/compatibility/test/browser/browser.toml create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-attribute-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-dom-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_markup-dom-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_ruleview-attribute-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_document-reload.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_panel-select.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_rule-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_selected-node-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_top-level-target-change.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_issue-node.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_settings.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_throbber.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_all.js create mode 100644 devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_some.js create mode 100644 devtools/client/inspector/compatibility/test/browser/head.js create mode 100644 devtools/client/inspector/compatibility/test/node/.eslintrc.js create mode 100644 devtools/client/inspector/compatibility/test/node/babel.config.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-CompatibilityApp.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Footer.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueItem.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueList.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssuePane.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeItem.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeList.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodePane.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Settings.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserItem.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserList.test.js.snap create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-CompatibilityApp.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-Footer.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueItem.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueList.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssuePane.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeItem.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeList.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodePane.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-Settings.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserItem.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserList.test.js create mode 100644 devtools/client/inspector/compatibility/test/node/jest.config.js create mode 100644 devtools/client/inspector/compatibility/test/node/package.json create mode 100644 devtools/client/inspector/compatibility/test/node/setup.js create mode 100644 devtools/client/inspector/compatibility/test/node/yarn.lock create mode 100644 devtools/client/inspector/compatibility/test/xpcshell/.eslintrc.js create mode 100644 devtools/client/inspector/compatibility/test/xpcshell/head.js create mode 100644 devtools/client/inspector/compatibility/test/xpcshell/test_default-browsers.js create mode 100644 devtools/client/inspector/compatibility/test/xpcshell/xpcshell.toml create mode 100644 devtools/client/inspector/compatibility/types.js create mode 100644 devtools/client/inspector/compatibility/utils/cases.js create mode 100644 devtools/client/inspector/compatibility/utils/moz.build create mode 100644 devtools/client/inspector/components/InspectorTabPanel.css create mode 100644 devtools/client/inspector/components/InspectorTabPanel.js create mode 100644 devtools/client/inspector/components/moz.build create mode 100644 devtools/client/inspector/computed/computed.js create mode 100644 devtools/client/inspector/computed/moz.build create mode 100644 devtools/client/inspector/computed/test/browser.toml create mode 100644 devtools/client/inspector/computed/test/browser_computed_browser-styles.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_custom_properties.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_cycle_color.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_default_tab.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_keybindings_01.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_keybindings_02.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_media-queries.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_original-source-link.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_search-filter.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_shadow_host.js create mode 100644 devtools/client/inspector/computed/test/browser_computed_style-editor-link.js create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors.html create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css create mode 100644 devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css create mode 100644 devtools/client/inspector/computed/test/doc_media_queries.html create mode 100644 devtools/client/inspector/computed/test/doc_pseudoelement.html create mode 100644 devtools/client/inspector/computed/test/doc_sourcemaps.css create mode 100644 devtools/client/inspector/computed/test/doc_sourcemaps.css.map create mode 100644 devtools/client/inspector/computed/test/doc_sourcemaps.html create mode 100644 devtools/client/inspector/computed/test/doc_sourcemaps.scss create mode 100644 devtools/client/inspector/computed/test/head.js create mode 100644 devtools/client/inspector/configs/development.json create mode 100644 devtools/client/inspector/extensions/actions/index.js create mode 100644 devtools/client/inspector/extensions/actions/moz.build create mode 100644 devtools/client/inspector/extensions/actions/sidebar.js create mode 100644 devtools/client/inspector/extensions/components/ExpressionResultView.js create mode 100644 devtools/client/inspector/extensions/components/ExtensionPage.js create mode 100644 devtools/client/inspector/extensions/components/ExtensionSidebar.js create mode 100644 devtools/client/inspector/extensions/components/ObjectTreeView.js create mode 100644 devtools/client/inspector/extensions/components/moz.build create mode 100644 devtools/client/inspector/extensions/extension-sidebar.js create mode 100644 devtools/client/inspector/extensions/moz.build create mode 100644 devtools/client/inspector/extensions/reducers/moz.build create mode 100644 devtools/client/inspector/extensions/reducers/sidebar.js create mode 100644 devtools/client/inspector/extensions/test/browser.toml create mode 100644 devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js create mode 100644 devtools/client/inspector/extensions/test/head.js create mode 100644 devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js create mode 100644 devtools/client/inspector/extensions/types.js create mode 100644 devtools/client/inspector/flexbox/actions/flexbox-highlighter.js create mode 100644 devtools/client/inspector/flexbox/actions/flexbox.js create mode 100644 devtools/client/inspector/flexbox/actions/index.js create mode 100644 devtools/client/inspector/flexbox/actions/moz.build create mode 100644 devtools/client/inspector/flexbox/components/FlexContainer.js create mode 100644 devtools/client/inspector/flexbox/components/FlexItem.js create mode 100644 devtools/client/inspector/flexbox/components/FlexItemList.js create mode 100644 devtools/client/inspector/flexbox/components/FlexItemSelector.js create mode 100644 devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js create mode 100644 devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js create mode 100644 devtools/client/inspector/flexbox/components/Flexbox.js create mode 100644 devtools/client/inspector/flexbox/components/Header.js create mode 100644 devtools/client/inspector/flexbox/components/moz.build create mode 100644 devtools/client/inspector/flexbox/flexbox.js create mode 100644 devtools/client/inspector/flexbox/moz.build create mode 100644 devtools/client/inspector/flexbox/reducers/flexbox.js create mode 100644 devtools/client/inspector/flexbox/reducers/index.js create mode 100644 devtools/client/inspector/flexbox/reducers/moz.build create mode 100644 devtools/client/inspector/flexbox/test/Ahem.ttf create mode 100644 devtools/client/inspector/flexbox/test/browser.toml create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js create mode 100644 devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html create mode 100644 devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html create mode 100644 devtools/client/inspector/flexbox/test/head.js create mode 100644 devtools/client/inspector/flexbox/types.js create mode 100644 devtools/client/inspector/fonts/actions/font-editor.js create mode 100644 devtools/client/inspector/fonts/actions/font-options.js create mode 100644 devtools/client/inspector/fonts/actions/fonts.js create mode 100644 devtools/client/inspector/fonts/actions/index.js create mode 100644 devtools/client/inspector/fonts/actions/moz.build create mode 100644 devtools/client/inspector/fonts/components/Font.js create mode 100644 devtools/client/inspector/fonts/components/FontAxis.js create mode 100644 devtools/client/inspector/fonts/components/FontEditor.js create mode 100644 devtools/client/inspector/fonts/components/FontList.js create mode 100644 devtools/client/inspector/fonts/components/FontName.js create mode 100644 devtools/client/inspector/fonts/components/FontOrigin.js create mode 100644 devtools/client/inspector/fonts/components/FontOverview.js create mode 100644 devtools/client/inspector/fonts/components/FontPreview.js create mode 100644 devtools/client/inspector/fonts/components/FontPreviewInput.js create mode 100644 devtools/client/inspector/fonts/components/FontPropertyValue.js create mode 100644 devtools/client/inspector/fonts/components/FontSize.js create mode 100644 devtools/client/inspector/fonts/components/FontStyle.js create mode 100644 devtools/client/inspector/fonts/components/FontWeight.js create mode 100644 devtools/client/inspector/fonts/components/FontsApp.js create mode 100644 devtools/client/inspector/fonts/components/LetterSpacing.js create mode 100644 devtools/client/inspector/fonts/components/LineHeight.js create mode 100644 devtools/client/inspector/fonts/components/moz.build create mode 100644 devtools/client/inspector/fonts/fonts.js create mode 100644 devtools/client/inspector/fonts/moz.build create mode 100644 devtools/client/inspector/fonts/reducers/font-editor.js create mode 100644 devtools/client/inspector/fonts/reducers/font-options.js create mode 100644 devtools/client/inspector/fonts/reducers/fonts.js create mode 100644 devtools/client/inspector/fonts/reducers/moz.build create mode 100644 devtools/client/inspector/fonts/test/OstrichLicense.txt create mode 100644 devtools/client/inspector/fonts/test/browser.toml create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_all-fonts.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_copy-URL.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_editor-font-size-conversion.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_editor-keywords.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_editor-letter-spacing-conversion.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_editor-values.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_expand-css-code.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_font-type-telemetry.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_input-element-used-font.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_no-fonts.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_reveal-in-page.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_text-node.js create mode 100644 devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js create mode 100644 devtools/client/inspector/fonts/test/doc_browser_fontinspector.html create mode 100644 devtools/client/inspector/fonts/test/doc_browser_fontinspector_iframe.html create mode 100644 devtools/client/inspector/fonts/test/head.js create mode 100644 devtools/client/inspector/fonts/test/ostrich-black.ttf create mode 100644 devtools/client/inspector/fonts/test/ostrich-regular.ttf create mode 100644 devtools/client/inspector/fonts/test/test_iframe.html create mode 100644 devtools/client/inspector/fonts/types.js create mode 100644 devtools/client/inspector/fonts/utils/font-utils.js create mode 100644 devtools/client/inspector/fonts/utils/l10n.js create mode 100644 devtools/client/inspector/fonts/utils/moz.build create mode 100644 devtools/client/inspector/grids/actions/grid-highlighter.js create mode 100644 devtools/client/inspector/grids/actions/grids.js create mode 100644 devtools/client/inspector/grids/actions/highlighter-settings.js create mode 100644 devtools/client/inspector/grids/actions/index.js create mode 100644 devtools/client/inspector/grids/actions/moz.build create mode 100644 devtools/client/inspector/grids/components/Grid.js create mode 100644 devtools/client/inspector/grids/components/GridDisplaySettings.js create mode 100644 devtools/client/inspector/grids/components/GridItem.js create mode 100644 devtools/client/inspector/grids/components/GridList.js create mode 100644 devtools/client/inspector/grids/components/GridOutline.js create mode 100644 devtools/client/inspector/grids/components/moz.build create mode 100644 devtools/client/inspector/grids/grid-inspector.js create mode 100644 devtools/client/inspector/grids/moz.build create mode 100644 devtools/client/inspector/grids/reducers/grids.js create mode 100644 devtools/client/inspector/grids/reducers/highlighter-settings.js create mode 100644 devtools/client/inspector/grids/reducers/moz.build create mode 100644 devtools/client/inspector/grids/test/browser.toml create mode 100644 devtools/client/inspector/grids/test/browser_grids_accordion-state.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_color-in-rules-grid-toggle.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-areas.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-on-iframe-reloaded.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-on-target-added-removed.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids-z-order.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_01.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_02.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_01.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_02.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-cannot-show-outline.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-multiple-grids.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_highlighter-setting-rules-grid-toggle.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_number-of-css-grids-telemetry.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_persist-color-palette.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_restored-after-reload.js create mode 100644 devtools/client/inspector/grids/test/browser_grids_restored-multiple-grids-after-reload.js create mode 100644 devtools/client/inspector/grids/test/doc_iframe_reloaded.html create mode 100644 devtools/client/inspector/grids/test/doc_subgrid.html create mode 100644 devtools/client/inspector/grids/test/head.js create mode 100644 devtools/client/inspector/grids/test/xpcshell/.eslintrc.js create mode 100644 devtools/client/inspector/grids/test/xpcshell/head.js create mode 100644 devtools/client/inspector/grids/test/xpcshell/test_compare_fragments_geometry.js create mode 100644 devtools/client/inspector/grids/test/xpcshell/xpcshell.toml create mode 100644 devtools/client/inspector/grids/types.js create mode 100644 devtools/client/inspector/grids/utils/moz.build create mode 100644 devtools/client/inspector/grids/utils/utils.js create mode 100644 devtools/client/inspector/index.xhtml create mode 100644 devtools/client/inspector/inspector-search.js create mode 100644 devtools/client/inspector/inspector.js create mode 100644 devtools/client/inspector/layout/components/LayoutApp.js create mode 100644 devtools/client/inspector/layout/components/moz.build create mode 100644 devtools/client/inspector/layout/layout.js create mode 100644 devtools/client/inspector/layout/moz.build create mode 100644 devtools/client/inspector/layout/utils/l10n.js create mode 100644 devtools/client/inspector/layout/utils/moz.build create mode 100644 devtools/client/inspector/markup/components/TextNode.js create mode 100644 devtools/client/inspector/markup/components/moz.build create mode 100644 devtools/client/inspector/markup/markup-context-menu.js create mode 100644 devtools/client/inspector/markup/markup.js create mode 100644 devtools/client/inspector/markup/markup.xhtml create mode 100644 devtools/client/inspector/markup/moz.build create mode 100644 devtools/client/inspector/markup/test/browser.toml create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_new_selection.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_container_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_copy_html.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_copy_image_data.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_display_node_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_display_node_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dom_mutation_breakpoints.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_before_marker_pseudo.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-overflow.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_chrome_blocked.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_chrome_not_blocked.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_keyboard_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_object_listener.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_source_map.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_toggle.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_flex_display_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_undo-redo.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_iframe_blocked_by_csp.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_aria_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_load_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_overflow_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pseudo_on_reload.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_about_page.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_iframe.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_shadowdom.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_screenshot_node_warning.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_scrollable_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_scrollable_badge_click.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_search_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_hover.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_marker_and_before_pseudos.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger_pretty_printed.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_shadowroot_mode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_show_nodes_button.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_slotted_keyboard_focus.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_shadowdom_ua_widgets_with_nac.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_subgrid_display_badge.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_delete_whitespace_node.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_avoid_refocus.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_template.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_display.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_view-original-source.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_view-source.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_html.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_whitespace.js create mode 100644 devtools/client/inspector/markup/test/doc_markup_anonymous.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_edit.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events-overflow.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events-source_map.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_03.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_04.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_chrome_listeners.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_jquery.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_object_listener.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_development_15.4.1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_development_15.4.1_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_15.3.1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_15.3.1_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_16.2.0.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_react_production_16.2.0_jsx.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_toggle.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_flashing.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_links.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_links_aria_attributes.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_mutation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_navigation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_not_displayed.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pseudo.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_search.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_shadowdom_open_debugger_pretty_printed.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_subgrid.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_svg_attributes.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_toggle.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_tooltip.png create mode 100644 devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_view-original-source.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml create mode 100644 devtools/client/inspector/markup/test/doc_markup_whitespace.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_xul.xhtml create mode 100644 devtools/client/inspector/markup/test/events_bundle.js create mode 100644 devtools/client/inspector/markup/test/events_bundle.js.map create mode 100644 devtools/client/inspector/markup/test/events_original.js create mode 100644 devtools/client/inspector/markup/test/head.js create mode 100644 devtools/client/inspector/markup/test/helper_attributes_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_diff.js create mode 100644 devtools/client/inspector/markup/test/helper_events_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_style_attr_test_runner.js create mode 100644 devtools/client/inspector/markup/test/lib_babel_6.21.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.2_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.3_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.4_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.6_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.7_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_16.2.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_dom_15.3.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_dom_15.4.1.js create mode 100644 devtools/client/inspector/markup/test/lib_react_dom_16.2.0_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_with_addons_15.3.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_react_with_addons_15.4.1.js create mode 100644 devtools/client/inspector/markup/test/react_external_listeners.js create mode 100644 devtools/client/inspector/markup/test/shadowdom_open_debugger.min.js create mode 100644 devtools/client/inspector/markup/utils.js create mode 100644 devtools/client/inspector/markup/utils/l10n.js create mode 100644 devtools/client/inspector/markup/utils/moz.build create mode 100644 devtools/client/inspector/markup/views/element-container.js create mode 100644 devtools/client/inspector/markup/views/element-editor.js create mode 100644 devtools/client/inspector/markup/views/html-editor.js create mode 100644 devtools/client/inspector/markup/views/markup-container.js create mode 100644 devtools/client/inspector/markup/views/moz.build create mode 100644 devtools/client/inspector/markup/views/read-only-container.js create mode 100644 devtools/client/inspector/markup/views/read-only-editor.js create mode 100644 devtools/client/inspector/markup/views/root-container.js create mode 100644 devtools/client/inspector/markup/views/slotted-node-container.js create mode 100644 devtools/client/inspector/markup/views/slotted-node-editor.js create mode 100644 devtools/client/inspector/markup/views/text-container.js create mode 100644 devtools/client/inspector/markup/views/text-editor.js create mode 100644 devtools/client/inspector/moz.build create mode 100644 devtools/client/inspector/node-picker.js create mode 100644 devtools/client/inspector/panel.js create mode 100644 devtools/client/inspector/rules/constants.js create mode 100644 devtools/client/inspector/rules/models/class-list.js create mode 100644 devtools/client/inspector/rules/models/element-style.js create mode 100644 devtools/client/inspector/rules/models/moz.build create mode 100644 devtools/client/inspector/rules/models/rule.js create mode 100644 devtools/client/inspector/rules/models/text-property.js create mode 100644 devtools/client/inspector/rules/models/user-properties.js create mode 100644 devtools/client/inspector/rules/moz.build create mode 100644 devtools/client/inspector/rules/rules.js create mode 100644 devtools/client/inspector/rules/test/browser_part1.toml create mode 100644 devtools/client/inspector/rules/test/browser_part2.toml create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-commented.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-invalid-identifier.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property-svg.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-property_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-and-remove-style-node.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-csp.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_add-rule.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_authored.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_authored_color.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_authored_override.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_add.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_content.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_edit.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_invalid_nodes.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_mutation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_state_preserved.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_class_panel_toggle.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorUnit.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_bfcache.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_meta.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_color_scheme_simulation_rdm.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click-or-keyboard-activation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-contrast-ratio.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-element-without-quads.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-element-picker.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-works-with-css-vars.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_colorpicker-wrap-focus.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-on-empty.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_completion-shortcut.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_computed-lists_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_conditional_import.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_container-queries.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_content_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_content_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_copy_styles.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cssom.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_custom.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cycle-angle.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_cycle-color.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-click.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-nested-rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-order.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property-remove_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_06.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_07.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_08.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_09.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-property_10.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector-nested-rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-selector_12.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-size-property-dragging.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-variable-add.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-variable-remove.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_edit-variable.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_eyedropper.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-mutation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-navigate.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-on-reload.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-highlighter-restored-after-reload.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_01b.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_flexbox-toggle_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_font-family-parsing.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-mutation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-template-areas.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_gridline-names-are-shown-correctly.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_guessIndentation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_highlight-property.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_highlight-used-fonts.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_imported_stylesheet_edit.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_display-justify.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_inline.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_split-condition.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_visited.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inactive_css_xul.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inherited-custom-properties.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inherited-properties_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inline-source-map.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_inline-style-order.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_invalid.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keybindings.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keyframes-rule-shadowdom.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_large_base64_background_image.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_layer.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_lineNumbers.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_linear-easing-swatch.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_livepreview.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_08.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mark_overridden_layers.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_mathml-element.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_media-queries.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_nested_at_rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_nested_rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_non_ascii.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_original-source-link.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_original-source-link2.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_preview-tooltips-sizes.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_print_media_simulation.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo-visited.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo-visited_in_media-query.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo-visited_with_style-attribute.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_refresh-on-stylesheet-change.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_registered-custom-properties.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-media-queries-layers.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_06.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_07.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_08.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_09.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_10.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter-iframe-picker.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter-nested-rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector-highlighter_order.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector_highlight.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_selector_warnings.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_basic-shapes-default.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shorthand-overridden-lists_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_style-editor-link.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_update_mask_image_cors.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_url-click-opens-new-tab.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_urls-clickable.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_user-property-reset.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables-in-pseudo-element_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_03-case-sensitive.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_04-valid-chars.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_autocomplete.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_variables_host.js create mode 100644 devtools/client/inspector/rules/test/doc_author-sheet.html create mode 100644 devtools/client/inspector/rules/test/doc_blob_stylesheet.html create mode 100644 devtools/client/inspector/rules/test/doc_class_panel_autocomplete.html create mode 100644 devtools/client/inspector/rules/test/doc_class_panel_autocomplete_stylesheet.css create mode 100644 devtools/client/inspector/rules/test/doc_conditional_import.css create mode 100644 devtools/client/inspector/rules/test/doc_content_stylesheet.html create mode 100644 devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css create mode 100644 devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css create mode 100644 devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css create mode 100644 devtools/client/inspector/rules/test/doc_content_stylesheet_script.css create mode 100644 devtools/client/inspector/rules/test/doc_copystyles.css create mode 100644 devtools/client/inspector/rules/test/doc_copystyles.html create mode 100644 devtools/client/inspector/rules/test/doc_cssom.html create mode 100644 devtools/client/inspector/rules/test/doc_custom.html create mode 100644 devtools/client/inspector/rules/test/doc_edit_imported_selector.html create mode 100644 devtools/client/inspector/rules/test/doc_filter.html create mode 100644 devtools/client/inspector/rules/test/doc_grid_area_gridline_names.html create mode 100644 devtools/client/inspector/rules/test/doc_grid_names.html create mode 100644 devtools/client/inspector/rules/test/doc_imported_anonymous_layer.css create mode 100644 devtools/client/inspector/rules/test/doc_imported_named_layer.css create mode 100644 devtools/client/inspector/rules/test/doc_imported_nested_named_layer.css create mode 100644 devtools/client/inspector/rules/test/doc_imported_no_layer.css create mode 100644 devtools/client/inspector/rules/test/doc_inactive_css_xul.xhtml create mode 100644 devtools/client/inspector/rules/test/doc_inline_sourcemap.html create mode 100644 devtools/client/inspector/rules/test/doc_invalid_sourcemap.css create mode 100644 devtools/client/inspector/rules/test/doc_invalid_sourcemap.html create mode 100644 devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html create mode 100644 devtools/client/inspector/rules/test/doc_keyframeanimation.css create mode 100644 devtools/client/inspector/rules/test/doc_keyframeanimation.html create mode 100644 devtools/client/inspector/rules/test/doc_media_queries.html create mode 100644 devtools/client/inspector/rules/test/doc_print_media_simulation.html create mode 100644 devtools/client/inspector/rules/test/doc_pseudoelement.html create mode 100644 devtools/client/inspector/rules/test/doc_ruleLineNumbers.html create mode 100644 devtools/client/inspector/rules/test/doc_rules_imported_stylesheet_edit.html create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps.css create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps.css.map create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps.html create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps.scss create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps2.css create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps2.css^headers^ create mode 100644 devtools/client/inspector/rules/test/doc_sourcemaps2.html create mode 100644 devtools/client/inspector/rules/test/doc_style_editor_link.css create mode 100644 devtools/client/inspector/rules/test/doc_test_image.png create mode 100644 devtools/client/inspector/rules/test/doc_urls_clickable.css create mode 100644 devtools/client/inspector/rules/test/doc_urls_clickable.html create mode 100644 devtools/client/inspector/rules/test/doc_variables_1.html create mode 100644 devtools/client/inspector/rules/test/doc_variables_2.html create mode 100644 devtools/client/inspector/rules/test/doc_variables_3.html create mode 100644 devtools/client/inspector/rules/test/doc_variables_4.html create mode 100644 devtools/client/inspector/rules/test/doc_visited.html create mode 100644 devtools/client/inspector/rules/test/doc_visited_in_media_query.html create mode 100644 devtools/client/inspector/rules/test/doc_visited_with_style_attribute.html create mode 100644 devtools/client/inspector/rules/test/head.js create mode 100644 devtools/client/inspector/rules/test/sjs_imported_stylesheet_edit.sjs create mode 100644 devtools/client/inspector/rules/test/square_svg.sjs create mode 100644 devtools/client/inspector/rules/types.js create mode 100644 devtools/client/inspector/rules/utils/l10n.js create mode 100644 devtools/client/inspector/rules/utils/moz.build create mode 100644 devtools/client/inspector/rules/utils/utils.js create mode 100644 devtools/client/inspector/rules/views/class-list-previewer.js create mode 100644 devtools/client/inspector/rules/views/moz.build create mode 100644 devtools/client/inspector/rules/views/registered-property-editor.js create mode 100644 devtools/client/inspector/rules/views/rule-editor.js create mode 100644 devtools/client/inspector/rules/views/text-property-editor.js create mode 100644 devtools/client/inspector/shared/highlighters-overlay.js create mode 100644 devtools/client/inspector/shared/moz.build create mode 100644 devtools/client/inspector/shared/node-reps.js create mode 100644 devtools/client/inspector/shared/node-types.js create mode 100644 devtools/client/inspector/shared/style-change-tracker.js create mode 100644 devtools/client/inspector/shared/style-inspector-menu.js create mode 100644 devtools/client/inspector/shared/test/browser.toml create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_style_changes.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js create mode 100644 devtools/client/inspector/shared/test/doc_content_style_changes.html create mode 100644 devtools/client/inspector/shared/test/head.js create mode 100644 devtools/client/inspector/shared/tooltips-overlay.js create mode 100644 devtools/client/inspector/shared/utils.js create mode 100644 devtools/client/inspector/shared/walker-event-listener.js create mode 100644 devtools/client/inspector/store.js create mode 100644 devtools/client/inspector/test/browser.toml create mode 100644 devtools/client/inspector/test/browser_inspector_addNode_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_addNode_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_addNode_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_addSidebarTab.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js create mode 100644 devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js create mode 100644 devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js create mode 100644 devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js create mode 100644 devtools/client/inspector/test/browser_inspector_delete_node_in_frame.js create mode 100644 devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_destroy-before-ready.js create mode 100644 devtools/client/inspector/test/browser_inspector_expand-collapse.js create mode 100644 devtools/client/inspector/test/browser_inspector_eyedropper_ruleview.js create mode 100644 devtools/client/inspector/test/browser_inspector_fission_frame.js create mode 100644 devtools/client/inspector/test/browser_inspector_fission_frame_navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_fission_switch_target.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-04.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-05.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-06.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-07.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-08.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-autohide.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-by-type.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cancel.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-comments.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-scale.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-translate.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_offset-path.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_percent.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-custom-element.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-embed.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-frames.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-image.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-zoom.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_07.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_hide_on_interaction.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-geometry_iframe.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-inline.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-keybinding_separate-window.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-measure-keybinding.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-measure_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-measure_04.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-options.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-preview.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion-message.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-reload.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-rulers_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-zoom.js create mode 100644 devtools/client/inspector/test/browser_inspector_iframe-navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_iframe-picker-bfcache-navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_iframe-picker.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_01.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_02.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_03.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_04.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_05.js create mode 100644 devtools/client/inspector/test/browser_inspector_infobar_textnode.js create mode 100644 devtools/client/inspector/test/browser_inspector_initialization.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect-object-element.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect_loading_document.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect_mutated_node.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu_nested.js create mode 100644 devtools/client/inspector/test/browser_inspector_inspect_parent_process_page.js create mode 100644 devtools/client/inspector/test/browser_inspector_invalidate.js create mode 100644 devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js create mode 100644 devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js create mode 100644 devtools/client/inspector/test/browser_inspector_menu-06-other.js create mode 100644 devtools/client/inspector/test/browser_inspector_navigate_to_errors.js create mode 100644 devtools/client/inspector/test/browser_inspector_navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_open_on_neterror.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-01.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-02.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-03.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-04.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-05.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane-toggle-layout-invariant.js create mode 100644 devtools/client/inspector/test/browser_inspector_pane_state_restore.js create mode 100644 devtools/client/inspector/test/browser_inspector_picker-reset-reference.js create mode 100644 devtools/client/inspector/test/browser_inspector_picker-shift-key.js create mode 100644 devtools/client/inspector/test/browser_inspector_picker-stop-on-eyedropper.js create mode 100644 devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js create mode 100644 devtools/client/inspector/test/browser_inspector_picker-useragent-widget.js create mode 100644 devtools/client/inspector/test/browser_inspector_portrait_mode.js create mode 100644 devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js create mode 100644 devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload-01.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload-02.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_iframe.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_invalid_iframe.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_missing-iframe-node.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_nested_iframe.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_shadow_dom.js create mode 100644 devtools/client/inspector/test/browser_inspector_reload_xul.js create mode 100644 devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-01.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-02.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-03.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-04.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-05.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-06.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-07.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-08.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-09.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-10.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-clear.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-label.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-navigation.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-reserved.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-selection.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-sidebar.js create mode 100644 devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js create mode 100644 devtools/client/inspector/test/browser_inspector_search_keyboard_shortcut_conflict.js create mode 100644 devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js create mode 100644 devtools/client/inspector/test/browser_inspector_select-last-selected.js create mode 100644 devtools/client/inspector/test/browser_inspector_sidebarstate.js create mode 100644 devtools/client/inspector/test/browser_inspector_startup.js create mode 100644 devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js create mode 100644 devtools/client/inspector/test/browser_inspector_textbox-menu.js create mode 100644 devtools/client/inspector/test/browser_inspector_textbox-menu_reopen_toolbox.js create mode 100644 devtools/client/inspector/test/browser_inspector_use-in-console-conflict.js create mode 100644 devtools/client/inspector/test/doc_inspector_add_node.html create mode 100644 devtools/client/inspector/test/doc_inspector_breadcrumbs.html create mode 100644 devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html create mode 100644 devtools/client/inspector/test/doc_inspector_csp.html create mode 100644 devtools/client/inspector/test/doc_inspector_csp.html^headers^ create mode 100644 devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html create mode 100644 devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html create mode 100644 devtools/client/inspector/test/doc_inspector_embed.html create mode 100644 devtools/client/inspector/test/doc_inspector_eyedropper_disabled.xhtml create mode 100644 devtools/client/inspector/test/doc_inspector_fission_frame_navigation.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlight_after_transition.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter-comments.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_cssshapes-percent.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_cssshapes.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_cssshapes_iframe.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_custom_element.xhtml create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_dom.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_inline.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_rect.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html create mode 100644 devtools/client/inspector/test/doc_inspector_highlighter_scroll.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar_01.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar_02.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar_03.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar_04.html create mode 100644 devtools/client/inspector/test/doc_inspector_infobar_textnode.html create mode 100644 devtools/client/inspector/test/doc_inspector_long-divs.html create mode 100644 devtools/client/inspector/test/doc_inspector_menu.html create mode 100644 devtools/client/inspector/test/doc_inspector_outerhtml.html create mode 100644 devtools/client/inspector/test/doc_inspector_pane-toggle-layout-invariant.html create mode 100644 devtools/client/inspector/test/doc_inspector_reload_xul.xhtml create mode 100644 devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html create mode 100644 devtools/client/inspector/test/doc_inspector_search-iframes.html create mode 100644 devtools/client/inspector/test/doc_inspector_search-reserved.html create mode 100644 devtools/client/inspector/test/doc_inspector_search-suggestions.html create mode 100644 devtools/client/inspector/test/doc_inspector_search-svg.html create mode 100644 devtools/client/inspector/test/doc_inspector_search.html create mode 100644 devtools/client/inspector/test/doc_inspector_select-last-selected-01.html create mode 100644 devtools/client/inspector/test/doc_inspector_select-last-selected-02.html create mode 100644 devtools/client/inspector/test/doc_inspector_svg.svg create mode 100644 devtools/client/inspector/test/head.js create mode 100644 devtools/client/inspector/test/img_browser_inspector_highlighter-eyedropper-image.png create mode 100644 devtools/client/inspector/test/shared-head.js create mode 100644 devtools/client/inspector/test/sjs_slow-loading-image.sjs create mode 100644 devtools/client/inspector/test/style_inspector_csp.css create mode 100644 devtools/client/inspector/test/style_inspector_eyedropper_ruleview.css create mode 100644 devtools/client/inspector/toolsidebar.js (limited to 'devtools/client/inspector') diff --git a/devtools/client/inspector/animation/actions/animations.js b/devtools/client/inspector/animation/actions/animations.js new file mode 100644 index 0000000000..3604630441 --- /dev/null +++ b/devtools/client/inspector/animation/actions/animations.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_ANIMATIONS, + UPDATE_DETAIL_VISIBILITY, + UPDATE_ELEMENT_PICKER_ENABLED, + UPDATE_HIGHLIGHTED_NODE, + UPDATE_PLAYBACK_RATES, + UPDATE_SELECTED_ANIMATION, + UPDATE_SIDEBAR_SIZE, +} = require("resource://devtools/client/inspector/animation/actions/index.js"); + +module.exports = { + /** + * Update the list of animation in the animation inspector. + */ + updateAnimations(animations) { + return { + type: UPDATE_ANIMATIONS, + animations, + }; + }, + + /** + * Update visibility of detail pane. + */ + updateDetailVisibility(detailVisibility) { + return { + type: UPDATE_DETAIL_VISIBILITY, + detailVisibility, + }; + }, + + /** + * Update the state of element picker in animation inspector. + */ + updateElementPickerEnabled(elementPickerEnabled) { + return { + type: UPDATE_ELEMENT_PICKER_ENABLED, + elementPickerEnabled, + }; + }, + + /** + * Update the highlighted node. + */ + updateHighlightedNode(nodeFront) { + return { + type: UPDATE_HIGHLIGHTED_NODE, + highlightedNode: nodeFront ? nodeFront.actorID : null, + }; + }, + + /** + * Update the playback rates. + */ + updatePlaybackRates() { + return { + type: UPDATE_PLAYBACK_RATES, + }; + }, + + /** + * Update selected animation. + */ + updateSelectedAnimation(selectedAnimation) { + return { + type: UPDATE_SELECTED_ANIMATION, + selectedAnimation, + }; + }, + + /** + * Update the sidebar size. + */ + updateSidebarSize(sidebarSize) { + return { + type: UPDATE_SIDEBAR_SIZE, + sidebarSize, + }; + }, +}; diff --git a/devtools/client/inspector/animation/actions/index.js b/devtools/client/inspector/animation/actions/index.js new file mode 100644 index 0000000000..572071604c --- /dev/null +++ b/devtools/client/inspector/animation/actions/index.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Update the list of animation. + "UPDATE_ANIMATIONS", + + // Update visibility of detail pane. + "UPDATE_DETAIL_VISIBILITY", + + // Update state of the picker enabled. + "UPDATE_ELEMENT_PICKER_ENABLED", + + // Update highlighted node. + "UPDATE_HIGHLIGHTED_NODE", + + // Update playback rate. + "UPDATE_PLAYBACK_RATES", + + // Update selected animation. + "UPDATE_SELECTED_ANIMATION", + + // Update sidebar size. + "UPDATE_SIDEBAR_SIZE", + ], + module.exports +); diff --git a/devtools/client/inspector/animation/actions/moz.build b/devtools/client/inspector/animation/actions/moz.build new file mode 100644 index 0000000000..f43007dd6e --- /dev/null +++ b/devtools/client/inspector/animation/actions/moz.build @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "animations.js", + "index.js", +) diff --git a/devtools/client/inspector/animation/animation.js b/devtools/client/inspector/animation/animation.js new file mode 100644 index 0000000000..c0f5d389d0 --- /dev/null +++ b/devtools/client/inspector/animation/animation.js @@ -0,0 +1,802 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + Provider, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +const App = createFactory( + require("resource://devtools/client/inspector/animation/components/App.js") +); +const CurrentTimeTimer = require("resource://devtools/client/inspector/animation/current-time-timer.js"); + +const animationsReducer = require("resource://devtools/client/inspector/animation/reducers/animations.js"); +const { + updateAnimations, + updateDetailVisibility, + updateElementPickerEnabled, + updateHighlightedNode, + updatePlaybackRates, + updateSelectedAnimation, + updateSidebarSize, +} = require("resource://devtools/client/inspector/animation/actions/animations.js"); +const { + hasAnimationIterationCountInfinite, + hasRunningAnimation, +} = require("resource://devtools/client/inspector/animation/utils/utils.js"); + +class AnimationInspector { + constructor(inspector, win) { + this.inspector = inspector; + this.win = win; + + this.inspector.store.injectReducer("animations", animationsReducer); + + this.addAnimationsCurrentTimeListener = + this.addAnimationsCurrentTimeListener.bind(this); + this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this); + this.getAnimationsCurrentTime = this.getAnimationsCurrentTime.bind(this); + this.getComputedStyle = this.getComputedStyle.bind(this); + this.getNodeFromActor = this.getNodeFromActor.bind(this); + this.removeAnimationsCurrentTimeListener = + this.removeAnimationsCurrentTimeListener.bind(this); + this.rewindAnimationsCurrentTime = + this.rewindAnimationsCurrentTime.bind(this); + this.selectAnimation = this.selectAnimation.bind(this); + this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this); + this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.bind(this); + this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this); + this.setDetailVisibility = this.setDetailVisibility.bind(this); + this.setHighlightedNode = this.setHighlightedNode.bind(this); + this.setSelectedNode = this.setSelectedNode.bind(this); + this.simulateAnimation = this.simulateAnimation.bind(this); + this.simulateAnimationForKeyframesProgressBar = + this.simulateAnimationForKeyframesProgressBar.bind(this); + this.toggleElementPicker = this.toggleElementPicker.bind(this); + this.update = this.update.bind(this); + this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this); + this.onAnimationsCurrentTimeUpdated = + this.onAnimationsCurrentTimeUpdated.bind(this); + this.onAnimationsMutation = this.onAnimationsMutation.bind(this); + this.onCurrentTimeTimerUpdated = this.onCurrentTimeTimerUpdated.bind(this); + this.onElementPickerStarted = this.onElementPickerStarted.bind(this); + this.onElementPickerStopped = this.onElementPickerStopped.bind(this); + this.onNavigate = this.onNavigate.bind(this); + this.onSidebarResized = this.onSidebarResized.bind(this); + this.onSidebarSelectionChanged = this.onSidebarSelectionChanged.bind(this); + this.onTargetAvailable = this.onTargetAvailable.bind(this); + + EventEmitter.decorate(this); + this.emitForTests = this.emitForTests.bind(this); + + this.initComponents(); + this.initListeners(); + } + + initComponents() { + const { + addAnimationsCurrentTimeListener, + emitForTests: emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + getNodeFromActor, + isAnimationsRunning, + removeAnimationsCurrentTimeListener, + rewindAnimationsCurrentTime, + selectAnimation, + setAnimationsCurrentTime, + setAnimationsPlaybackRate, + setAnimationsPlayState, + setDetailVisibility, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + toggleElementPicker, + } = this; + + const direction = this.win.document.dir; + + this.animationsCurrentTimeListeners = []; + this.isCurrentTimeSet = false; + + const provider = createElement( + Provider, + { + id: "animationinspector", + key: "animationinspector", + store: this.inspector.store, + }, + App({ + addAnimationsCurrentTimeListener, + direction, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + getNodeFromActor, + isAnimationsRunning, + removeAnimationsCurrentTimeListener, + rewindAnimationsCurrentTime, + selectAnimation, + setAnimationsCurrentTime, + setAnimationsPlaybackRate, + setAnimationsPlayState, + setDetailVisibility, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + toggleElementPicker, + }) + ); + this.provider = provider; + } + + async initListeners() { + await this.inspector.commands.targetCommand.watchTargets({ + types: [this.inspector.commands.targetCommand.TYPES.FRAME], + onAvailable: this.onTargetAvailable, + }); + + this.inspector.on("new-root", this.onNavigate); + this.inspector.selection.on("new-node-front", this.update); + this.inspector.sidebar.on("select", this.onSidebarSelectionChanged); + this.inspector.toolbox.on("select", this.onSidebarSelectionChanged); + this.inspector.toolbox.on( + "inspector-sidebar-resized", + this.onSidebarResized + ); + this.inspector.toolbox.nodePicker.on( + "picker-started", + this.onElementPickerStarted + ); + this.inspector.toolbox.nodePicker.on( + "picker-stopped", + this.onElementPickerStopped + ); + } + + destroy() { + this.setAnimationStateChangedListenerEnabled(false); + this.inspector.off("new-root", this.onNavigate); + this.inspector.selection.off("new-node-front", this.update); + this.inspector.sidebar.off("select", this.onSidebarSelectionChanged); + this.inspector.toolbox.off( + "inspector-sidebar-resized", + this.onSidebarResized + ); + this.inspector.toolbox.nodePicker.off( + "picker-started", + this.onElementPickerStarted + ); + this.inspector.toolbox.nodePicker.off( + "picker-stopped", + this.onElementPickerStopped + ); + this.inspector.toolbox.off("select", this.onSidebarSelectionChanged); + + if (this.animationsFront) { + this.animationsFront.off("mutations", this.onAnimationsMutation); + } + + if (this.simulatedAnimation) { + this.simulatedAnimation.cancel(); + this.simulatedAnimation = null; + } + + if (this.simulatedElement) { + this.simulatedElement.remove(); + this.simulatedElement = null; + } + + if (this.simulatedAnimationForKeyframesProgressBar) { + this.simulatedAnimationForKeyframesProgressBar.cancel(); + this.simulatedAnimationForKeyframesProgressBar = null; + } + + this.stopAnimationsCurrentTimeTimer(); + + this.inspector = null; + this.win = null; + } + + get state() { + return this.inspector.store.getState().animations; + } + + addAnimationsCurrentTimeListener(listener) { + this.animationsCurrentTimeListeners.push(listener); + } + + /** + * This function calls AnimationsFront.setCurrentTimes with considering the createdTime. + * + * @param {Number} currentTime + */ + async doSetCurrentTimes(currentTime) { + const { animations, timeScale } = this.state; + currentTime = currentTime + timeScale.minStartTime; + await this.animationsFront.setCurrentTimes(animations, currentTime, true, { + relativeToCreatedTime: true, + }); + } + + /** + * Return a map of animated property from given animation actor. + * + * @param {Object} animation + * @return {Map} A map of animated property + * key: {String} Animated property name + * value: {Array} Array of keyframe object + * Also, the keyframe object is consisted as following. + * { + * value: {String} style, + * offset: {Number} offset of keyframe, + * easing: {String} easing from this keyframe to next keyframe, + * distance: {Number} use as y coordinate in graph, + * } + */ + getAnimatedPropertyMap(animation) { + const properties = animation.state.properties; + const animatedPropertyMap = new Map(); + + for (const { name, values } of properties) { + const keyframes = values.map( + ({ value, offset, easing, distance = 0 }) => { + offset = parseFloat(offset.toFixed(3)); + return { value, offset, easing, distance }; + } + ); + + animatedPropertyMap.set(name, keyframes); + } + + return animatedPropertyMap; + } + + getAnimationsCurrentTime() { + return this.currentTime; + } + + /** + * Return the computed style of the specified property after setting the given styles + * to the simulated element. + * + * @param {String} property + * CSS property name (e.g. text-align). + * @param {Object} styles + * Map of CSS property name and value. + * @return {String} + * Computed style of property. + */ + getComputedStyle(property, styles) { + this.simulatedElement.style.cssText = ""; + + for (const propertyName in styles) { + this.simulatedElement.style.setProperty( + propertyName, + styles[propertyName] + ); + } + + return this.win + .getComputedStyle(this.simulatedElement) + .getPropertyValue(property); + } + + getNodeFromActor(actorID) { + if (!this.inspector) { + return Promise.reject("Animation inspector already destroyed"); + } + + return this.inspector.walker.getNodeFromActor(actorID, ["node"]); + } + + isPanelVisible() { + return ( + this.inspector && + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() === "animationinspector" + ); + } + + onAnimationStateChanged() { + // Simply update the animations since the state has already been updated. + this.fireUpdateAction([...this.state.animations]); + } + + /** + * This method should call when the current time is changed. + * Then, dispatches the current time to listeners that are registered + * by addAnimationsCurrentTimeListener. + * + * @param {Number} currentTime + */ + onAnimationsCurrentTimeUpdated(currentTime) { + this.currentTime = currentTime; + + for (const listener of this.animationsCurrentTimeListeners) { + listener(currentTime); + } + } + + /** + * This method is called when the current time proceed by CurrentTimeTimer. + * + * @param {Number} currentTime + * @param {Bool} shouldStop + */ + onCurrentTimeTimerUpdated(currentTime, shouldStop) { + if (shouldStop) { + this.setAnimationsCurrentTime(currentTime, true); + } else { + this.onAnimationsCurrentTimeUpdated(currentTime); + } + } + + async onAnimationsMutation(changes) { + let animations = [...this.state.animations]; + const addedAnimations = []; + + for (const { type, player: animation } of changes) { + if (type === "added") { + if (!animation.state.type) { + // This animation was added but removed immediately. + continue; + } + + addedAnimations.push(animation); + animation.on("changed", this.onAnimationStateChanged); + } else if (type === "removed") { + const index = animations.indexOf(animation); + + if (index < 0) { + // This animation was added but removed immediately. + continue; + } + + animations.splice(index, 1); + animation.off("changed", this.onAnimationStateChanged); + } + } + + // Update existing other animations as well since the currentTime would be proceeded + // sice the scrubber position is related the currentTime. + // Also, don't update the state of removed animations since React components + // may refer to the same instance still. + try { + animations = await this.refreshAnimationsState(animations); + } catch (_) { + console.error(`Updating Animations failed`); + return; + } + + this.fireUpdateAction(animations.concat(addedAnimations)); + } + + onElementPickerStarted() { + this.inspector.store.dispatch(updateElementPickerEnabled(true)); + } + + onElementPickerStopped() { + this.inspector.store.dispatch(updateElementPickerEnabled(false)); + } + + onNavigate() { + if (!this.isPanelVisible()) { + return; + } + + this.inspector.store.dispatch(updatePlaybackRates()); + } + + async onSidebarSelectionChanged() { + const isPanelVisibled = this.isPanelVisible(); + + if (this.wasPanelVisibled === isPanelVisibled) { + // onSidebarSelectionChanged is called some times even same state + // from sidebar and toolbar. + return; + } + + this.wasPanelVisibled = isPanelVisibled; + + if (this.isPanelVisible()) { + await this.update(); + this.onSidebarResized(null, this.inspector.getSidebarSize()); + } else { + this.stopAnimationsCurrentTimeTimer(); + this.setAnimationStateChangedListenerEnabled(false); + } + } + + onSidebarResized(size) { + if (!this.isPanelVisible()) { + return; + } + + this.inspector.store.dispatch(updateSidebarSize(size)); + } + + async onTargetAvailable({ targetFront }) { + if (targetFront.isTopLevel) { + this.animationsFront = await targetFront.getFront("animations"); + this.animationsFront.setWalkerActor(this.inspector.walker); + this.animationsFront.on("mutations", this.onAnimationsMutation); + + await this.update(); + } + } + + removeAnimationsCurrentTimeListener(listener) { + this.animationsCurrentTimeListeners = + this.animationsCurrentTimeListeners.filter(l => l !== listener); + } + + async rewindAnimationsCurrentTime() { + const { timeScale } = this.state; + await this.setAnimationsCurrentTime(timeScale.zeroPositionTime, true); + } + + selectAnimation(animation) { + this.inspector.store.dispatch(updateSelectedAnimation(animation)); + } + + async setSelectedNode(nodeFront) { + if (this.inspector.selection.nodeFront === nodeFront) { + return; + } + + await this.inspector + .getCommonComponentProps() + .setSelectedNode(nodeFront, { reason: "animation-panel" }); + } + + async setAnimationsCurrentTime(currentTime, shouldRefresh) { + this.stopAnimationsCurrentTimeTimer(); + this.onAnimationsCurrentTimeUpdated(currentTime); + + if (!shouldRefresh && this.isCurrentTimeSet) { + return; + } + + let animations = this.state.animations; + this.isCurrentTimeSet = true; + + try { + await this.doSetCurrentTimes(currentTime); + animations = await this.refreshAnimationsState(animations); + } catch (e) { + // Expected if we've already been destroyed or other node have been selected + // in the meantime. + console.error(e); + return; + } + + this.isCurrentTimeSet = false; + + if (shouldRefresh) { + this.fireUpdateAction(animations); + } + } + + async setAnimationsPlaybackRate(playbackRate) { + if (!this.inspector) { + return; // Already destroyed or another node selected. + } + + let animations = this.state.animations; + // "changed" event on each animation will fire respectively when the playback + // rate changed. Since for each occurrence of event, change of UI is urged. + // To avoid this, disable the listeners once in order to not capture the event. + this.setAnimationStateChangedListenerEnabled(false); + try { + await this.animationsFront.setPlaybackRates(animations, playbackRate); + animations = await this.refreshAnimationsState(animations); + } catch (e) { + // Expected if we've already been destroyed or another node has been + // selected in the meantime. + console.error(e); + return; + } finally { + this.setAnimationStateChangedListenerEnabled(true); + } + + if (animations) { + await this.fireUpdateAction(animations); + } + } + + async setAnimationsPlayState(doPlay) { + if (!this.inspector) { + return; // Already destroyed or another node selected. + } + + let { animations, timeScale } = this.state; + + try { + if ( + doPlay && + animations.every( + animation => + timeScale.getEndTime(animation) <= animation.state.currentTime + ) + ) { + await this.doSetCurrentTimes(timeScale.zeroPositionTime); + } + + if (doPlay) { + await this.animationsFront.playSome(animations); + } else { + await this.animationsFront.pauseSome(animations); + } + + animations = await this.refreshAnimationsState(animations); + } catch (e) { + // Expected if we've already been destroyed or other node have been selected + // in the meantime. + console.error(e); + return; + } + + await this.fireUpdateAction(animations); + } + + /** + * Enable/disable the animation state change listener. + * If set true, observe "changed" event on current animations. + * Otherwise, quit observing the "changed" event. + * + * @param {Bool} isEnabled + */ + setAnimationStateChangedListenerEnabled(isEnabled) { + if (!this.inspector) { + return; // Already destroyed. + } + if (isEnabled) { + for (const animation of this.state.animations) { + animation.on("changed", this.onAnimationStateChanged); + } + } else { + for (const animation of this.state.animations) { + animation.off("changed", this.onAnimationStateChanged); + } + } + } + + setDetailVisibility(isVisible) { + this.inspector.store.dispatch(updateDetailVisibility(isVisible)); + } + + /** + * Persistently highlight the given node identified with a unique selector. + * If no node is provided, hide any persistent highlighter. + * + * @param {NodeFront} nodeFront + */ + async setHighlightedNode(nodeFront) { + await this.inspector.highlighters.hideHighlighterType( + this.inspector.highlighters.TYPES.SELECTOR + ); + + if (nodeFront) { + const selector = await nodeFront.getUniqueSelector(); + if (!selector) { + console.warn( + `Couldn't get unique selector for NodeFront: ${nodeFront.actorID}` + ); + return; + } + + /** + * NOTE: Using a Selector Highlighter here because only one Box Model Highlighter + * can be visible at a time. The Box Model Highlighter is shown when hovering nodes + * which would cause this persistent highlighter to be hidden unexpectedly. + * This limitation of one highlighter type a time should be solved by switching + * to a highlighter by role approach (Bug 1663443). + */ + await this.inspector.highlighters.showHighlighterTypeForNode( + this.inspector.highlighters.TYPES.SELECTOR, + nodeFront, + { + hideInfoBar: true, + hideGuides: true, + selector, + } + ); + } + + this.inspector.store.dispatch(updateHighlightedNode(nodeFront)); + } + + /** + * Returns simulatable animation by given parameters. + * The returned animation is implementing Animation interface of Web Animation API. + * https://drafts.csswg.org/web-animations/#the-animation-interface + * + * @param {Array} keyframes + * e.g. [{ opacity: 0 }, { opacity: 1 }] + * @param {Object} effectTiming + * e.g. { duration: 1000, fill: "both" } + * @param {Boolean} isElementNeeded + * true: create animation with an element. + * If want to know computed value of the element, turn on. + * false: create animation without an element, + * If need to know only timing progress. + * @return {Animation} + * https://drafts.csswg.org/web-animations/#the-animation-interface + */ + simulateAnimation(keyframes, effectTiming, isElementNeeded) { + // Don't simulate animation if the animation inspector is already destroyed. + if (!this.win) { + return null; + } + + let targetEl = null; + + if (isElementNeeded) { + if (!this.simulatedElement) { + this.simulatedElement = this.win.document.createElement("div"); + this.win.document.documentElement.appendChild(this.simulatedElement); + } else { + // Reset styles. + this.simulatedElement.style.cssText = ""; + } + + targetEl = this.simulatedElement; + } + + if (!this.simulatedAnimation) { + this.simulatedAnimation = new this.win.Animation(); + } + + this.simulatedAnimation.effect = new this.win.KeyframeEffect( + targetEl, + keyframes, + effectTiming + ); + + return this.simulatedAnimation; + } + + /** + * Returns a simulatable efect timing animation for the keyframes progress bar. + * The returned animation is implementing Animation interface of Web Animation API. + * https://drafts.csswg.org/web-animations/#the-animation-interface + * + * @param {Object} effectTiming + * e.g. { duration: 1000, fill: "both" } + * @return {Animation} + * https://drafts.csswg.org/web-animations/#the-animation-interface + */ + simulateAnimationForKeyframesProgressBar(effectTiming) { + if (!this.simulatedAnimationForKeyframesProgressBar) { + this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation(); + } + + this.simulatedAnimationForKeyframesProgressBar.effect = + new this.win.KeyframeEffect(null, null, effectTiming); + + return this.simulatedAnimationForKeyframesProgressBar; + } + + stopAnimationsCurrentTimeTimer() { + if (this.currentTimeTimer) { + this.currentTimeTimer.destroy(); + this.currentTimeTimer = null; + } + } + + startAnimationsCurrentTimeTimer() { + const timeScale = this.state.timeScale; + const shouldStopAfterEndTime = !hasAnimationIterationCountInfinite( + this.state.animations + ); + + const currentTimeTimer = new CurrentTimeTimer( + timeScale, + shouldStopAfterEndTime, + this.win, + this.onCurrentTimeTimerUpdated + ); + currentTimeTimer.start(); + this.currentTimeTimer = currentTimeTimer; + } + + toggleElementPicker() { + this.inspector.toolbox.nodePicker.togglePicker(); + } + + async update() { + if (!this.isPanelVisible()) { + return; + } + + const done = this.inspector.updating("animationinspector"); + + const selection = this.inspector.selection; + const animations = + selection.isConnected() && selection.isElementNode() + ? await this.animationsFront.getAnimationPlayersForNode( + selection.nodeFront + ) + : []; + this.fireUpdateAction(animations); + this.setAnimationStateChangedListenerEnabled(true); + + done(); + } + + async refreshAnimationsState(animations) { + let error = null; + + const promises = animations.map(animation => { + return new Promise(resolve => { + animation + .refreshState() + .catch(e => { + error = e; + }) + .finally(() => { + resolve(); + }); + }); + }); + await Promise.all(promises); + + if (error) { + throw new Error(error); + } + + // Even when removal animation on inspected document, refreshAnimationsState + // might be called before onAnimationsMutation due to the async timing. + // Return the animations as result of refreshAnimationsState after getting rid of + // the animations since they should not display. + return animations.filter(anim => !!anim.state.type); + } + + fireUpdateAction(animations) { + // Animation inspector already destroyed + if (!this.inspector) { + return; + } + + this.stopAnimationsCurrentTimeTimer(); + + // Although it is not possible to set a delay or end delay of infinity using + // the animation API, if the value passed exceeds the limit of our internal + // representation of times, it will be treated as infinity. Rather than + // adding special case code to represent this very rare case, we simply omit + // such animations from the graph. + animations = animations.filter( + anim => + Math.abs(anim.state.delay) !== Infinity && + Math.abs(anim.state.endDelay) !== Infinity + ); + + this.inspector.store.dispatch(updateAnimations(animations)); + + if (hasRunningAnimation(animations)) { + this.startAnimationsCurrentTimeTimer(); + } else { + // Even no running animations, update the current time once + // so as to show the state. + this.onCurrentTimeTimerUpdated(this.state.timeScale.getCurrentTime()); + } + } +} + +module.exports = AnimationInspector; diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyItem.js b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js new file mode 100644 index 0000000000..4c4ff37254 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimatedPropertyName = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimatedPropertyName.js") +); +const KeyframesGraph = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js") +); + +class AnimatedPropertyItem extends PureComponent { + static get propTypes() { + return { + getComputedStyle: PropTypes.func.isRequired, + isUnchanged: PropTypes.bool.isRequired, + keyframes: PropTypes.array.isRequired, + name: PropTypes.string.isRequired, + simulateAnimation: PropTypes.func.isRequired, + state: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, + }; + } + + render() { + const { + getComputedStyle, + isUnchanged, + keyframes, + name, + simulateAnimation, + state, + type, + } = this.props; + + return dom.li( + { + className: "animated-property-item" + (isUnchanged ? " unchanged" : ""), + }, + AnimatedPropertyName({ + name, + state, + }), + KeyframesGraph({ + getComputedStyle, + keyframes, + name, + simulateAnimation, + type, + }) + ); + } +} + +module.exports = AnimatedPropertyItem; diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyList.js b/devtools/client/inspector/animation/components/AnimatedPropertyList.js new file mode 100644 index 0000000000..54dff15187 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimatedPropertyList.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimatedPropertyItem = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimatedPropertyItem.js") +); + +class AnimatedPropertyList extends Component { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + emitEventForTest: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getComputedStyle: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + // Array of object which has the property name, the keyframes, its aniamtion type + // and unchanged flag. + animatedProperties: null, + // To avoid rendering while the state is updating + // since we call an async function in updateState. + isStateUpdating: false, + }; + } + + componentDidMount() { + // No need to set isStateUpdating state since paint sequence is finish here. + this.updateState(this.props.animation); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({ isStateUpdating: true }); + this.updateState(nextProps.animation); + } + + shouldComponentUpdate(nextProps, nextState) { + return !nextState.isStateUpdating; + } + + getPropertyState(property) { + const { animation } = this.props; + + for (const propState of animation.state.propertyState) { + if (propState.property === property) { + return propState; + } + } + + return null; + } + + async updateState(animation) { + const { getAnimatedPropertyMap, emitEventForTest } = this.props; + + let propertyMap = null; + let propertyNames = null; + let types = null; + + try { + propertyMap = getAnimatedPropertyMap(animation); + propertyNames = [...propertyMap.keys()]; + types = await animation.getAnimationTypes(propertyNames); + } catch (e) { + // Expected if we've already been destroyed or other node have been selected + // in the meantime. + console.error(e); + return; + } + + const animatedProperties = propertyNames.map(name => { + const keyframes = propertyMap.get(name); + const type = types[name]; + const isUnchanged = keyframes.every( + keyframe => keyframe.value === keyframes[0].value + ); + return { isUnchanged, keyframes, name, type }; + }); + + animatedProperties.sort((a, b) => { + if (a.isUnchanged === b.isUnchanged) { + return a.name > b.name ? 1 : -1; + } + + return a.isUnchanged ? 1 : -1; + }); + + this.setState({ + animatedProperties, + isStateUpdating: false, + }); + + emitEventForTest("animation-keyframes-rendered"); + } + + render() { + const { getComputedStyle, simulateAnimation } = this.props; + const { animatedProperties } = this.state; + + if (!animatedProperties) { + return null; + } + + return dom.ul( + { + className: "animated-property-list", + }, + animatedProperties.map(({ isUnchanged, keyframes, name, type }) => { + const state = this.getPropertyState(name); + return AnimatedPropertyItem({ + getComputedStyle, + isUnchanged, + keyframes, + name, + simulateAnimation, + state, + type, + }); + }) + ); + } +} + +module.exports = AnimatedPropertyList; diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js new file mode 100644 index 0000000000..965099b37e --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimatedPropertyList = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimatedPropertyList.js") +); +const KeyframesProgressBar = createFactory( + require("resource://devtools/client/inspector/animation/components/KeyframesProgressBar.js") +); +const ProgressInspectionPanel = createFactory( + require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js") +); + +const { + getFormatStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +class AnimatedPropertyListContainer extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animation: PropTypes.object.isRequired, + emitEventForTest: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getAnimationsCurrentTime: PropTypes.func.isRequired, + getComputedStyle: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { + addAnimationsCurrentTimeListener, + animation, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + removeAnimationsCurrentTimeListener, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + timeScale, + } = this.props; + + return dom.div( + { + className: `animated-property-list-container ${animation.state.type}`, + }, + ProgressInspectionPanel({ + indicator: KeyframesProgressBar({ + addAnimationsCurrentTimeListener, + animation, + getAnimationsCurrentTime, + removeAnimationsCurrentTimeListener, + simulateAnimationForKeyframesProgressBar, + timeScale, + }), + list: AnimatedPropertyList({ + animation, + emitEventForTest, + getAnimatedPropertyMap, + getComputedStyle, + simulateAnimation, + }), + ticks: [0, 50, 100].map(position => { + const label = getFormatStr( + "detail.propertiesHeader.percentage", + position + ); + return { position, label }; + }), + }) + ); + } +} + +module.exports = AnimatedPropertyListContainer; diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyName.js b/devtools/client/inspector/animation/components/AnimatedPropertyName.js new file mode 100644 index 0000000000..e7a777ee12 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimatedPropertyName.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class AnimatedPropertyName extends PureComponent { + static get propTypes() { + return { + name: PropTypes.string.isRequired, + state: PropTypes.oneOfType([null, PropTypes.object]).isRequired, + }; + } + + render() { + const { name, state } = this.props; + + return dom.div( + { + className: + "animated-property-name" + + (state?.runningOnCompositor ? " compositor" : "") + + (state?.warning ? " warning" : ""), + title: state ? state.warning : "", + }, + dom.span({}, name) + ); + } +} + +module.exports = AnimatedPropertyName; diff --git a/devtools/client/inspector/animation/components/AnimationDetailContainer.js b/devtools/client/inspector/animation/components/AnimationDetailContainer.js new file mode 100644 index 0000000000..9a7b4bfd1a --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationDetailHeader = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationDetailHeader.js") +); +const AnimatedPropertyListContainer = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js") +); + +class AnimationDetailContainer extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animation: PropTypes.object.isRequired, + emitEventForTest: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getAnimationsCurrentTime: PropTypes.func.isRequired, + getComputedStyle: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + setDetailVisibility: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { + addAnimationsCurrentTimeListener, + animation, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + removeAnimationsCurrentTimeListener, + setDetailVisibility, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + timeScale, + } = this.props; + + return dom.div( + { + className: "animation-detail-container", + }, + animation + ? AnimationDetailHeader({ + animation, + setDetailVisibility, + }) + : null, + animation + ? AnimatedPropertyListContainer({ + addAnimationsCurrentTimeListener, + animation, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + removeAnimationsCurrentTimeListener, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + timeScale, + }) + : null + ); + } +} + +const mapStateToProps = state => { + return { + animation: state.animations.selectedAnimation, + }; +}; + +module.exports = connect(mapStateToProps)(AnimationDetailContainer); diff --git a/devtools/client/inspector/animation/components/AnimationDetailHeader.js b/devtools/client/inspector/animation/components/AnimationDetailHeader.js new file mode 100644 index 0000000000..5f3da0acba --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationDetailHeader.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + getFormattedTitle, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +class AnimationDetailHeader extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + setDetailVisibility: PropTypes.func.isRequired, + }; + } + + onClick(event) { + event.stopPropagation(); + const { setDetailVisibility } = this.props; + setDetailVisibility(false); + } + + render() { + const { animation } = this.props; + + return dom.div( + { + className: "animation-detail-header devtools-toolbar", + }, + dom.div( + { + className: "animation-detail-title", + }, + getFormattedTitle(animation.state) + ), + dom.button({ + className: "animation-detail-close-button devtools-button", + onClick: this.onClick.bind(this), + }) + ); + } +} + +module.exports = AnimationDetailHeader; diff --git a/devtools/client/inspector/animation/components/AnimationItem.js b/devtools/client/inspector/animation/components/AnimationItem.js new file mode 100644 index 0000000000..45bb09bac6 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationItem.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationTarget = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationTarget.js") +); +const SummaryGraph = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/SummaryGraph.js") +); + +class AnimationItem extends Component { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getNodeFromActor: PropTypes.func.isRequired, + isDisplayable: PropTypes.bool.isRequired, + selectAnimation: PropTypes.func.isRequired, + selectedAnimation: PropTypes.object.isRequired, + setHighlightedNode: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + isSelected: this.isSelected(props), + }; + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({ + isSelected: this.isSelected(nextProps), + }); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.isDisplayable !== nextProps.isDisplayable || + this.state.isSelected !== nextState.isSelected || + this.props.animation !== nextProps.animation || + this.props.timeScale !== nextProps.timeScale + ); + } + + isSelected(props) { + return ( + props.selectedAnimation && + props.animation.actorID === props.selectedAnimation.actorID + ); + } + + render() { + const { + animation, + dispatch, + getAnimatedPropertyMap, + getNodeFromActor, + isDisplayable, + selectAnimation, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + } = this.props; + const { isSelected } = this.state; + + return dom.li( + { + className: + `animation-item ${animation.state.type} ` + + (isSelected ? "selected" : ""), + }, + isDisplayable + ? [ + AnimationTarget({ + animation, + dispatch, + getNodeFromActor, + setHighlightedNode, + setSelectedNode, + }), + SummaryGraph({ + animation, + getAnimatedPropertyMap, + selectAnimation, + simulateAnimation, + timeScale, + }), + ] + : null + ); + } +} + +const mapStateToProps = state => { + return { + selectedAnimation: state.animations.selectedAnimation, + }; +}; + +module.exports = connect(mapStateToProps)(AnimationItem); diff --git a/devtools/client/inspector/animation/components/AnimationList.js b/devtools/client/inspector/animation/components/AnimationList.js new file mode 100644 index 0000000000..7d0f1caf3f --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationList.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationItem = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationItem.js") +); + +class AnimationList extends PureComponent { + static get propTypes() { + return { + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatch: PropTypes.func.isRequired, + displayableRange: PropTypes.object.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getNodeFromActor: PropTypes.func.isRequired, + selectAnimation: PropTypes.func.isRequired, + setHighlightedNode: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { + animations, + dispatch, + displayableRange, + getAnimatedPropertyMap, + getNodeFromActor, + selectAnimation, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + } = this.props; + + const { startIndex, endIndex } = displayableRange; + + return dom.ul( + { + className: "animation-list", + }, + animations.map((animation, index) => + AnimationItem({ + animation, + dispatch, + getAnimatedPropertyMap, + getNodeFromActor, + isDisplayable: startIndex <= index && index <= endIndex, + selectAnimation, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + }) + ) + ); + } +} + +module.exports = AnimationList; diff --git a/devtools/client/inspector/animation/components/AnimationListContainer.js b/devtools/client/inspector/animation/components/AnimationListContainer.js new file mode 100644 index 0000000000..d97b368f70 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationListContainer.js @@ -0,0 +1,224 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationList = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationList.js") +); +const CurrentTimeScrubber = createFactory( + require("resource://devtools/client/inspector/animation/components/CurrentTimeScrubber.js") +); +const ProgressInspectionPanel = createFactory( + require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js") +); + +const { + findOptimalTimeInterval, +} = require("resource://devtools/client/inspector/animation/utils/utils.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); +const { throttle } = require("resource://devtools/shared/throttle.js"); + +// The minimum spacing between 2 time graduation headers in the timeline (px). +const TIME_GRADUATION_MIN_SPACING = 40; + +class AnimationListContainer extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + direction: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getNodeFromActor: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + selectAnimation: PropTypes.func.isRequired, + setAnimationsCurrentTime: PropTypes.func.isRequired, + setHighlightedNode: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + sidebarWidth: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this._ref = createRef(); + + this.updateDisplayableRange = throttle( + this.updateDisplayableRange, + 100, + this + ); + + this.state = { + // tick labels and lines on the progress inspection panel + ticks: [], + // Displayable range. + displayableRange: { startIndex: 0, endIndex: 0 }, + }; + } + + componentDidMount() { + this.updateTicks(this.props); + + const current = this._ref.current; + this._inspectionPanelEl = current.querySelector( + ".progress-inspection-panel" + ); + this._inspectionPanelEl.addEventListener("scroll", () => { + this.updateDisplayableRange(); + }); + + this._animationListEl = current.querySelector(".animation-list"); + const resizeObserver = new current.ownerGlobal.ResizeObserver(() => { + this.updateDisplayableRange(); + }); + resizeObserver.observe(this._animationListEl); + + const animationItemEl = current.querySelector(".animation-item"); + this._itemHeight = animationItemEl.offsetHeight; + + this.updateDisplayableRange(); + } + + componentDidUpdate(prevProps) { + const { timeScale, sidebarWidth } = this.props; + + if ( + timeScale.getDuration() !== prevProps.timeScale.getDuration() || + timeScale.zeroPositionTime !== prevProps.timeScale.zeroPositionTime || + sidebarWidth !== prevProps.sidebarWidth + ) { + this.updateTicks(this.props); + } + } + + /** + * Since it takes too much time if we render all of animation graphs, + * we restrict to render the items that are not in displaying area. + * This function calculates the displayable item range. + */ + updateDisplayableRange() { + const count = + Math.floor(this._animationListEl.offsetHeight / this._itemHeight) + 1; + const index = Math.floor( + this._inspectionPanelEl.scrollTop / this._itemHeight + ); + this.setState({ + displayableRange: { startIndex: index, endIndex: index + count }, + }); + } + + updateTicks(props) { + const { animations, timeScale } = props; + const tickLinesEl = this._ref.current.querySelector(".tick-lines"); + const width = tickLinesEl.offsetWidth; + const animationDuration = timeScale.getDuration(); + const minTimeInterval = + (TIME_GRADUATION_MIN_SPACING * animationDuration) / width; + const intervalLength = findOptimalTimeInterval(minTimeInterval); + const intervalWidth = (intervalLength * width) / animationDuration; + const tickCount = parseInt(width / intervalWidth, 10); + const isAllDurationInfinity = animations.every( + animation => animation.state.duration === Infinity + ); + const zeroBasePosition = + width * (timeScale.zeroPositionTime / animationDuration); + const shiftWidth = zeroBasePosition % intervalWidth; + const needToShift = zeroBasePosition !== 0 && shiftWidth !== 0; + + const ticks = []; + // Need to display first graduation since position will be shifted. + if (needToShift) { + const label = timeScale.formatTime(timeScale.distanceToRelativeTime(0)); + ticks.push({ position: 0, label, width: shiftWidth }); + } + + for (let i = 0; i <= tickCount; i++) { + const position = ((i * intervalWidth + shiftWidth) * 100) / width; + const distance = timeScale.distanceToRelativeTime(position); + const label = + isAllDurationInfinity && i === tickCount + ? getStr("player.infiniteTimeLabel") + : timeScale.formatTime(distance); + ticks.push({ position, label, width: intervalWidth }); + } + + this.setState({ ticks }); + } + + render() { + const { + addAnimationsCurrentTimeListener, + animations, + direction, + dispatch, + getAnimatedPropertyMap, + getNodeFromActor, + removeAnimationsCurrentTimeListener, + selectAnimation, + setAnimationsCurrentTime, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + } = this.props; + const { displayableRange, ticks } = this.state; + + return dom.div( + { + className: "animation-list-container", + ref: this._ref, + }, + ProgressInspectionPanel({ + indicator: CurrentTimeScrubber({ + addAnimationsCurrentTimeListener, + direction, + removeAnimationsCurrentTimeListener, + setAnimationsCurrentTime, + timeScale, + }), + list: AnimationList({ + animations, + dispatch, + displayableRange, + getAnimatedPropertyMap, + getNodeFromActor, + selectAnimation, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + }), + ticks, + }) + ); + } +} + +const mapStateToProps = state => { + return { + sidebarWidth: state.animations.sidebarSize + ? state.animations.sidebarSize.width + : 0, + }; +}; + +module.exports = connect(mapStateToProps)(AnimationListContainer); diff --git a/devtools/client/inspector/animation/components/AnimationTarget.js b/devtools/client/inspector/animation/components/AnimationTarget.js new file mode 100644 index 0000000000..2fa392b3f6 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationTarget.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + translateNodeFrontToGrip, +} = require("resource://devtools/client/inspector/shared/utils.js"); + +const { + REPS, + MODE, +} = require("resource://devtools/client/shared/components/reps/index.js"); +const { Rep } = REPS; +const ElementNode = REPS.ElementNode; + +const { + getInspectorStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class AnimationTarget extends Component { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + getNodeFromActor: PropTypes.func.isRequired, + highlightedNode: PropTypes.string.isRequired, + setHighlightedNode: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + nodeFront: null, + }; + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillMount() { + this.updateNodeFront(this.props.animation); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + if (this.props.animation.actorID !== nextProps.animation.actorID) { + this.updateNodeFront(nextProps.animation); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.state.nodeFront !== nextState.nodeFront || + this.props.highlightedNode !== nextState.highlightedNode + ); + } + + async updateNodeFront(animation) { + const { getNodeFromActor } = this.props; + + // Try and get it from the playerFront directly. + let nodeFront = animation.animationTargetNodeFront; + + // Next, get it from the walkerActor if it wasn't found. + if (!nodeFront) { + try { + nodeFront = await getNodeFromActor(animation.actorID); + } catch (e) { + // If an error occured while getting the nodeFront and if it can't be + // attributed to the panel having been destroyed in the meantime, this + // error needs to be logged and render needs to stop. + console.error(e); + this.setState({ nodeFront: null }); + return; + } + } + + this.setState({ nodeFront }); + } + + async ensureNodeFront() { + if (!this.state.nodeFront.actorID) { + // In case of no actorID, the node front had been destroyed. + // This will occur when the pseudo element was re-generated. + await this.updateNodeFront(this.props.animation); + } + } + + async highlight() { + await this.ensureNodeFront(); + + if (this.state.nodeFront) { + this.props.dispatch( + highlightNode(this.state.nodeFront, { + hideInfoBar: true, + hideGuides: true, + }) + ); + } + } + + async select() { + await this.ensureNodeFront(); + + if (this.state.nodeFront) { + this.props.setSelectedNode(this.state.nodeFront); + } + } + + render() { + const { dispatch, highlightedNode, setHighlightedNode } = this.props; + const { nodeFront } = this.state; + + if (!nodeFront) { + return dom.div({ + className: "animation-target", + }); + } + + const isHighlighted = nodeFront.actorID === highlightedNode; + + return dom.div( + { + className: "animation-target" + (isHighlighted ? " highlighting" : ""), + }, + Rep({ + defaultRep: ElementNode, + mode: MODE.TINY, + inspectIconTitle: getInspectorStr( + "inspector.nodePreview.highlightNodeLabel" + ), + inspectIconClassName: "highlight-node", + object: translateNodeFrontToGrip(nodeFront), + onDOMNodeClick: () => this.select(), + onDOMNodeMouseOut: () => { + if (!isHighlighted) { + dispatch(unhighlightNode()); + } + }, + onDOMNodeMouseOver: () => { + if (!isHighlighted) { + this.highlight(); + } + }, + onInspectIconClick: (_, e) => { + e.stopPropagation(); + + if (!isHighlighted) { + // At first, hide highlighter which was created by onDOMNodeMouseOver. + dispatch(unhighlightNode()); + } + + setHighlightedNode(isHighlighted ? null : nodeFront); + }, + }) + ); + } +} + +const mapStateToProps = state => { + return { + highlightedNode: state.animations.highlightedNode, + }; +}; + +module.exports = connect(mapStateToProps)(AnimationTarget); diff --git a/devtools/client/inspector/animation/components/AnimationToolbar.js b/devtools/client/inspector/animation/components/AnimationToolbar.js new file mode 100644 index 0000000000..9b6b7aadc8 --- /dev/null +++ b/devtools/client/inspector/animation/components/AnimationToolbar.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const CurrentTimeLabel = createFactory( + require("resource://devtools/client/inspector/animation/components/CurrentTimeLabel.js") +); +const PauseResumeButton = createFactory( + require("resource://devtools/client/inspector/animation/components/PauseResumeButton.js") +); +const PlaybackRateSelector = createFactory( + require("resource://devtools/client/inspector/animation/components/PlaybackRateSelector.js") +); +const RewindButton = createFactory( + require("resource://devtools/client/inspector/animation/components/RewindButton.js") +); + +class AnimationToolbar extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + rewindAnimationsCurrentTime: PropTypes.func.isRequired, + setAnimationsPlaybackRate: PropTypes.func.isRequired, + setAnimationsPlayState: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { + addAnimationsCurrentTimeListener, + animations, + removeAnimationsCurrentTimeListener, + rewindAnimationsCurrentTime, + setAnimationsPlaybackRate, + setAnimationsPlayState, + timeScale, + } = this.props; + + return dom.div( + { + className: "animation-toolbar devtools-toolbar", + }, + RewindButton({ + rewindAnimationsCurrentTime, + }), + PauseResumeButton({ + animations, + setAnimationsPlayState, + }), + PlaybackRateSelector({ + animations, + setAnimationsPlaybackRate, + }), + CurrentTimeLabel({ + addAnimationsCurrentTimeListener, + removeAnimationsCurrentTimeListener, + timeScale, + }) + ); + } +} + +module.exports = AnimationToolbar; diff --git a/devtools/client/inspector/animation/components/App.js b/devtools/client/inspector/animation/components/App.js new file mode 100644 index 0000000000..75a000286c --- /dev/null +++ b/devtools/client/inspector/animation/components/App.js @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const AnimationDetailContainer = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationDetailContainer.js") +); +const AnimationListContainer = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationListContainer.js") +); +const AnimationToolbar = createFactory( + require("resource://devtools/client/inspector/animation/components/AnimationToolbar.js") +); +const NoAnimationPanel = createFactory( + require("resource://devtools/client/inspector/animation/components/NoAnimationPanel.js") +); +const SplitBox = createFactory( + require("resource://devtools/client/shared/components/splitter/SplitBox.js") +); + +class App extends Component { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + detailVisibility: PropTypes.bool.isRequired, + direction: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + emitEventForTest: PropTypes.func.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + getAnimationsCurrentTime: PropTypes.func.isRequired, + getComputedStyle: PropTypes.func.isRequired, + getNodeFromActor: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + rewindAnimationsCurrentTime: PropTypes.func.isRequired, + selectAnimation: PropTypes.func.isRequired, + setAnimationsCurrentTime: PropTypes.func.isRequired, + setAnimationsPlaybackRate: PropTypes.func.isRequired, + setAnimationsPlayState: PropTypes.func.isRequired, + setDetailVisibility: PropTypes.func.isRequired, + setHighlightedNode: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + toggleElementPicker: PropTypes.func.isRequired, + toggleLockingHighlight: PropTypes.func.isRequired, + }; + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.animations.length !== 0 || nextProps.animations.length !== 0 + ); + } + + render() { + const { + addAnimationsCurrentTimeListener, + animations, + detailVisibility, + direction, + dispatch, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + getNodeFromActor, + removeAnimationsCurrentTimeListener, + rewindAnimationsCurrentTime, + selectAnimation, + setAnimationsCurrentTime, + setAnimationsPlaybackRate, + setAnimationsPlayState, + setDetailVisibility, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + timeScale, + toggleElementPicker, + } = this.props; + + return dom.div( + { + id: "animation-container", + className: detailVisibility ? "animation-detail-visible" : "", + tabIndex: -1, + }, + animations.length + ? [ + AnimationToolbar({ + addAnimationsCurrentTimeListener, + animations, + removeAnimationsCurrentTimeListener, + rewindAnimationsCurrentTime, + setAnimationsPlaybackRate, + setAnimationsPlayState, + timeScale, + }), + SplitBox({ + className: "animation-container-splitter", + endPanel: AnimationDetailContainer({ + addAnimationsCurrentTimeListener, + emitEventForTest, + getAnimatedPropertyMap, + getAnimationsCurrentTime, + getComputedStyle, + removeAnimationsCurrentTimeListener, + setDetailVisibility, + simulateAnimation, + simulateAnimationForKeyframesProgressBar, + timeScale, + }), + endPanelControl: true, + initialHeight: "50%", + splitterSize: 1, + minSize: "30px", + startPanel: AnimationListContainer({ + addAnimationsCurrentTimeListener, + animations, + direction, + dispatch, + getAnimatedPropertyMap, + getNodeFromActor, + removeAnimationsCurrentTimeListener, + selectAnimation, + setAnimationsCurrentTime, + setHighlightedNode, + setSelectedNode, + simulateAnimation, + timeScale, + }), + vert: false, + }), + ] + : NoAnimationPanel({ + toggleElementPicker, + }) + ); + } +} + +const mapStateToProps = state => { + return { + animations: state.animations.animations, + detailVisibility: state.animations.detailVisibility, + timeScale: state.animations.timeScale, + }; +}; + +module.exports = connect(mapStateToProps)(App); diff --git a/devtools/client/inspector/animation/components/CurrentTimeLabel.js b/devtools/client/inspector/animation/components/CurrentTimeLabel.js new file mode 100644 index 0000000000..67b2498e8b --- /dev/null +++ b/devtools/client/inspector/animation/components/CurrentTimeLabel.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class CurrentTimeLabel extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this._ref = createRef(); + + const { addAnimationsCurrentTimeListener } = props; + this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this); + + addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + componentWillUnmount() { + const { removeAnimationsCurrentTimeListener } = this.props; + removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + onCurrentTimeUpdated(currentTime) { + const { timeScale } = this.props; + const text = formatStopwatchTime(currentTime - timeScale.zeroPositionTime); + // onCurrentTimeUpdated is bound to requestAnimationFrame. + // As to update the component too frequently has performance issue if React controlled, + // update raw component directly. See Bug 1699039. + this._ref.current.textContent = text; + } + + render() { + return dom.label({ className: "current-time-label", ref: this._ref }); + } +} + +/** + * Format a timestamp (in ms) as a mm:ss.mmm string. + * + * @param {Number} time + * @return {String} + */ +function formatStopwatchTime(time) { + // Format falsy values as 0 + if (!time) { + return "00:00.000"; + } + + const sign = time < 0 ? "-" : ""; + let milliseconds = parseInt(Math.abs(time % 1000), 10); + let seconds = parseInt(Math.abs(time / 1000) % 60, 10); + let minutes = parseInt(Math.abs(time / (1000 * 60)), 10); + + minutes = minutes.toString().padStart(2, "0"); + seconds = seconds.toString().padStart(2, "0"); + milliseconds = milliseconds.toString().padStart(3, "0"); + return `${sign}${minutes}:${seconds}.${milliseconds}`; +} + +module.exports = CurrentTimeLabel; diff --git a/devtools/client/inspector/animation/components/CurrentTimeScrubber.js b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js new file mode 100644 index 0000000000..8b87e32eff --- /dev/null +++ b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const { + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class CurrentTimeScrubber extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + direction: PropTypes.string.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + setAnimationsCurrentTime: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this._ref = createRef(); + + const { addAnimationsCurrentTimeListener } = props; + this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + + addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + componentDidMount() { + const current = this._ref.current; + current.addEventListener("mousedown", this.onMouseDown); + this._scrubber = current.querySelector(".current-time-scrubber"); + } + + componentWillUnmount() { + const { removeAnimationsCurrentTimeListener } = this.props; + removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + onCurrentTimeUpdated(currentTime) { + const { timeScale } = this.props; + + const position = currentTime / timeScale.getDuration(); + // onCurrentTimeUpdated is bound to requestAnimationFrame. + // As to update the component too frequently has performance issue if React controlled, + // update raw component directly. See Bug 1699039. + this._scrubber.style.marginInlineStart = `${position * 100}%`; + } + + onMouseDown(event) { + event.stopPropagation(); + const current = this._ref.current; + this.controllerArea = current.getBoundingClientRect(); + this.listenerTarget = DevToolsUtils.getTopWindow(current.ownerGlobal); + this.listenerTarget.addEventListener("mousemove", this.onMouseMove); + this.listenerTarget.addEventListener("mouseup", this.onMouseUp); + this.decorationTarget = current.closest(".animation-list-container"); + this.decorationTarget.classList.add("active-scrubber"); + + this.updateAnimationsCurrentTime(event.pageX, true); + } + + onMouseMove(event) { + event.stopPropagation(); + this.isMouseMoved = true; + this.updateAnimationsCurrentTime(event.pageX); + } + + onMouseUp(event) { + event.stopPropagation(); + + if (this.isMouseMoved) { + this.updateAnimationsCurrentTime(event.pageX, true); + this.isMouseMoved = null; + } + + this.uninstallListeners(); + } + + uninstallListeners() { + this.listenerTarget.removeEventListener("mousemove", this.onMouseMove); + this.listenerTarget.removeEventListener("mouseup", this.onMouseUp); + this.listenerTarget = null; + this.decorationTarget.classList.remove("active-scrubber"); + this.decorationTarget = null; + this.controllerArea = null; + } + + updateAnimationsCurrentTime(pageX, needRefresh) { + const { direction, setAnimationsCurrentTime, timeScale } = this.props; + + let progressRate = + (pageX - this.controllerArea.x) / this.controllerArea.width; + + if (progressRate < 0.0) { + progressRate = 0.0; + } else if (progressRate > 1.0) { + progressRate = 1.0; + } + + const time = + direction === "ltr" + ? progressRate * timeScale.getDuration() + : (1 - progressRate) * timeScale.getDuration(); + + setAnimationsCurrentTime(time, needRefresh); + } + + render() { + return dom.div( + { + className: "current-time-scrubber-area", + ref: this._ref, + }, + dom.div({ className: "indication-bar current-time-scrubber" }) + ); + } +} + +module.exports = CurrentTimeScrubber; diff --git a/devtools/client/inspector/animation/components/KeyframesProgressBar.js b/devtools/client/inspector/animation/components/KeyframesProgressBar.js new file mode 100644 index 0000000000..b4e9e526de --- /dev/null +++ b/devtools/client/inspector/animation/components/KeyframesProgressBar.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class KeyframesProgressBar extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + animation: PropTypes.object.isRequired, + getAnimationsCurrentTime: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this._progressBarRef = createRef(); + + this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this); + } + + componentDidMount() { + const { addAnimationsCurrentTimeListener } = this.props; + + this.setupAnimation(this.props); + addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { animation, getAnimationsCurrentTime, timeScale } = nextProps; + + this.setupAnimation(nextProps); + this.updateOffset(getAnimationsCurrentTime(), animation, timeScale); + } + + componentWillUnmount() { + const { removeAnimationsCurrentTimeListener } = this.props; + + removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + this.simulatedAnimation = null; + } + + onCurrentTimeUpdated(currentTime) { + const { animation, timeScale } = this.props; + this.updateOffset(currentTime, animation, timeScale); + } + + updateOffset(currentTime, animation, timeScale) { + const { createdTime, playbackRate } = animation.state; + + const time = + (timeScale.minStartTime + currentTime - createdTime) * playbackRate; + + if (isNaN(time)) { + // Setting an invalid currentTime will throw so bail out if time is not a number for + // any reason. + return; + } + + this.simulatedAnimation.currentTime = time; + const position = + this.simulatedAnimation.effect.getComputedTiming().progress; + + // onCurrentTimeUpdated is bound to requestAnimationFrame. + // As to update the component too frequently has performance issue if React controlled, + // update raw component directly. See Bug 1699039. + this._progressBarRef.current.style.marginInlineStart = `${position * 100}%`; + } + + setupAnimation(props) { + const { animation, simulateAnimationForKeyframesProgressBar } = props; + + if (this.simulatedAnimation) { + this.simulatedAnimation.cancel(); + } + + const timing = Object.assign({}, animation.state, { + iterations: animation.state.iterationCount || Infinity, + }); + + this.simulatedAnimation = simulateAnimationForKeyframesProgressBar(timing); + } + + render() { + return dom.div( + { className: "keyframes-progress-bar-area" }, + dom.div({ + className: "indication-bar keyframes-progress-bar", + ref: this._progressBarRef, + }) + ); + } +} + +module.exports = KeyframesProgressBar; diff --git a/devtools/client/inspector/animation/components/NoAnimationPanel.js b/devtools/client/inspector/animation/components/NoAnimationPanel.js new file mode 100644 index 0000000000..ea034e413d --- /dev/null +++ b/devtools/client/inspector/animation/components/NoAnimationPanel.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +const L10N = new LocalizationHelper( + "devtools/client/locales/animationinspector.properties" +); + +class NoAnimationPanel extends Component { + static get propTypes() { + return { + elementPickerEnabled: PropTypes.bool.isRequired, + toggleElementPicker: PropTypes.func.isRequired, + }; + } + + shouldComponentUpdate(nextProps, nextState) { + return this.props.elementPickerEnabled != nextProps.elementPickerEnabled; + } + + render() { + const { elementPickerEnabled, toggleElementPicker } = this.props; + + return dom.div( + { + className: "animation-error-message devtools-sidepanel-no-result", + }, + dom.p(null, L10N.getStr("panel.noAnimation")), + dom.button({ + className: + "animation-element-picker devtools-button" + + (elementPickerEnabled ? " checked" : ""), + "data-standalone": true, + onClick: event => { + event.stopPropagation(); + toggleElementPicker(); + }, + }) + ); + } +} + +const mapStateToProps = state => { + return { + elementPickerEnabled: state.animations.elementPickerEnabled, + }; +}; + +module.exports = connect(mapStateToProps)(NoAnimationPanel); diff --git a/devtools/client/inspector/animation/components/PauseResumeButton.js b/devtools/client/inspector/animation/components/PauseResumeButton.js new file mode 100644 index 0000000000..bcbb597a39 --- /dev/null +++ b/devtools/client/inspector/animation/components/PauseResumeButton.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); + +const { + getStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); +const { + hasRunningAnimation, +} = require("resource://devtools/client/inspector/animation/utils/utils.js"); + +class PauseResumeButton extends PureComponent { + static get propTypes() { + return { + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + setAnimationsPlayState: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.onKeyDown = this.onKeyDown.bind(this); + this.pauseResumeButtonRef = createRef(); + + this.state = { + isRunning: false, + }; + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillMount() { + this.updateState(this.props); + } + + componentDidMount() { + const targetEl = this.getKeyEventTarget(); + targetEl.addEventListener("keydown", this.onKeyDown); + } + + componentDidUpdate() { + this.updateState(); + } + + componentWillUnount() { + const targetEl = this.getKeyEventTarget(); + targetEl.removeEventListener("keydown", this.onKeyDown); + } + + getKeyEventTarget() { + return this.pauseResumeButtonRef.current.closest("#animation-container"); + } + + onToggleAnimationsPlayState(event) { + event.stopPropagation(); + const { setAnimationsPlayState } = this.props; + const { isRunning } = this.state; + + setAnimationsPlayState(!isRunning); + } + + onKeyDown(event) { + // Prevent to the duplicated call from the key listener and click listener. + if ( + event.keyCode === KeyCodes.DOM_VK_SPACE && + event.target !== this.pauseResumeButtonRef.current + ) { + this.onToggleAnimationsPlayState(event); + } + } + + updateState() { + const { animations } = this.props; + const isRunning = hasRunningAnimation(animations); + this.setState({ isRunning }); + } + + render() { + const { isRunning } = this.state; + + return dom.button({ + className: + "pause-resume-button devtools-button" + (isRunning ? "" : " paused"), + onClick: this.onToggleAnimationsPlayState.bind(this), + title: isRunning + ? getStr("timeline.resumedButtonTooltip") + : getStr("timeline.pausedButtonTooltip"), + ref: this.pauseResumeButtonRef, + }); + } +} + +module.exports = PauseResumeButton; diff --git a/devtools/client/inspector/animation/components/PlaybackRateSelector.js b/devtools/client/inspector/animation/components/PlaybackRateSelector.js new file mode 100644 index 0000000000..2d0de53a0c --- /dev/null +++ b/devtools/client/inspector/animation/components/PlaybackRateSelector.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const { + getFormatStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +const PLAYBACK_RATES = [0.1, 0.25, 0.5, 1, 2, 5, 10]; + +class PlaybackRateSelector extends PureComponent { + static get propTypes() { + return { + animations: PropTypes.arrayOf(PropTypes.object).isRequired, + playbackRates: PropTypes.arrayOf(PropTypes.number).isRequired, + setAnimationsPlaybackRate: PropTypes.func.isRequired, + }; + } + + static getDerivedStateFromProps(props, state) { + const { animations, playbackRates } = props; + + const currentPlaybackRates = sortAndUnique( + animations.map(a => a.state.playbackRate) + ); + const options = sortAndUnique([ + ...PLAYBACK_RATES, + ...playbackRates, + ...currentPlaybackRates, + ]); + + if (currentPlaybackRates.length === 1) { + return { + options, + selected: currentPlaybackRates[0], + }; + } + + // When the animations displayed have mixed playback rates, we can't + // select any of the predefined ones. + return { + options: ["", ...options], + selected: "", + }; + } + + constructor(props) { + super(props); + + this.state = { + options: [], + selected: 1, + }; + } + + onChange(e) { + const { setAnimationsPlaybackRate } = this.props; + + if (!e.target.value) { + return; + } + + setAnimationsPlaybackRate(e.target.value); + } + + render() { + const { options, selected } = this.state; + + return dom.select( + { + className: "playback-rate-selector devtools-button", + onChange: this.onChange.bind(this), + }, + options.map(rate => { + return dom.option( + { + selected: rate === selected ? "true" : null, + value: rate, + }, + rate ? getFormatStr("player.playbackRateLabel", rate) : "-" + ); + }) + ); + } +} + +function sortAndUnique(array) { + return [...new Set(array)].sort((a, b) => a > b); +} + +const mapStateToProps = state => { + return { + playbackRates: state.animations.playbackRates, + }; +}; + +module.exports = connect(mapStateToProps)(PlaybackRateSelector); diff --git a/devtools/client/inspector/animation/components/ProgressInspectionPanel.js b/devtools/client/inspector/animation/components/ProgressInspectionPanel.js new file mode 100644 index 0000000000..71c82dd17b --- /dev/null +++ b/devtools/client/inspector/animation/components/ProgressInspectionPanel.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const TickLabels = createFactory( + require("resource://devtools/client/inspector/animation/components/TickLabels.js") +); +const TickLines = createFactory( + require("resource://devtools/client/inspector/animation/components/TickLines.js") +); + +/** + * This component is a panel for the progress of animations or keyframes, supports + * displaying the ticks, take the areas of indicator and the list. + */ +class ProgressInspectionPanel extends PureComponent { + static get propTypes() { + return { + indicator: PropTypes.any.isRequired, + list: PropTypes.any.isRequired, + ticks: PropTypes.arrayOf(PropTypes.object).isRequired, + }; + } + + render() { + const { indicator, list, ticks } = this.props; + + return dom.div( + { + className: "progress-inspection-panel", + }, + dom.div({ className: "background" }, TickLines({ ticks })), + dom.div({ className: "indicator" }, indicator), + dom.div({ className: "header devtools-toolbar" }, TickLabels({ ticks })), + list + ); + } +} + +module.exports = ProgressInspectionPanel; diff --git a/devtools/client/inspector/animation/components/RewindButton.js b/devtools/client/inspector/animation/components/RewindButton.js new file mode 100644 index 0000000000..f069b42368 --- /dev/null +++ b/devtools/client/inspector/animation/components/RewindButton.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + getStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +class RewindButton extends PureComponent { + static get propTypes() { + return { + rewindAnimationsCurrentTime: PropTypes.func.isRequired, + }; + } + + render() { + const { rewindAnimationsCurrentTime } = this.props; + + return dom.button({ + className: "rewind-button devtools-button", + onClick: event => { + event.stopPropagation(); + rewindAnimationsCurrentTime(); + }, + title: getStr("timeline.rewindButtonTooltip"), + }); + } +} + +module.exports = RewindButton; diff --git a/devtools/client/inspector/animation/components/TickLabels.js b/devtools/client/inspector/animation/components/TickLabels.js new file mode 100644 index 0000000000..019c7b177b --- /dev/null +++ b/devtools/client/inspector/animation/components/TickLabels.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +/** + * This component is intended to show tick labels on the header. + */ +class TickLabels extends PureComponent { + static get propTypes() { + return { + ticks: PropTypes.array.isRequired, + }; + } + + render() { + const { ticks } = this.props; + + return dom.div( + { + className: "tick-labels", + }, + ticks.map(tick => + dom.div( + { + className: "tick-label", + style: { + marginInlineStart: `${tick.position}%`, + maxWidth: `${tick.width}px`, + }, + }, + tick.label + ) + ) + ); + } +} + +module.exports = TickLabels; diff --git a/devtools/client/inspector/animation/components/TickLines.js b/devtools/client/inspector/animation/components/TickLines.js new file mode 100644 index 0000000000..d3cbdead98 --- /dev/null +++ b/devtools/client/inspector/animation/components/TickLines.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +/** + * This component is intended to show the tick line as the background. + */ +class TickLines extends PureComponent { + static get propTypes() { + return { + ticks: PropTypes.array.isRequired, + }; + } + + render() { + const { ticks } = this.props; + + return dom.div( + { + className: "tick-lines", + }, + ticks.map(tick => + dom.div({ + className: "tick-line", + style: { marginInlineStart: `${tick.position}%` }, + }) + ) + ); + } +} + +module.exports = TickLines; diff --git a/devtools/client/inspector/animation/components/graph/AnimationName.js b/devtools/client/inspector/animation/components/graph/AnimationName.js new file mode 100644 index 0000000000..1ef2ebd829 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/AnimationName.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class AnimationName extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + }; + } + + render() { + const { animation } = this.props; + + return dom.svg( + { + className: "animation-name", + }, + dom.text( + { + y: "50%", + x: "100%", + }, + animation.state.name + ) + ); + } +} + +module.exports = AnimationName; diff --git a/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js new file mode 100644 index 0000000000..fed25e161e --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); +const TimingPath = require("resource://devtools/client/inspector/animation/components/graph/TimingPath.js"); + +class ComputedTimingPath extends TimingPath { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + durationPerPixel: PropTypes.number.isRequired, + keyframes: PropTypes.object.isRequired, + offset: PropTypes.number.isRequired, + opacity: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + keyframes, + offset, + opacity, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + // Create new keyframes for opacity as computed style. + // The reason why we use computed value instead of computed timing progress is to + // include the easing in keyframes as well. Although the computed timing progress + // is not affected by the easing in keyframes at all, computed value reflects that. + const frames = keyframes.map(keyframe => { + return { + opacity: keyframe.offset, + offset: keyframe.offset, + easing: keyframe.easing, + }; + }); + + const simulatedAnimation = simulateAnimation(frames, effectTiming, true); + + if (!simulatedAnimation) { + return null; + } + + const simulatedElement = simulatedAnimation.effect.target; + const win = simulatedElement.ownerGlobal; + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + // Set the underlying opacity to zero so that if we sample the animation's output + // during the delay phase and it is not filling backwards, we get zero. + simulatedElement.style.opacity = 0; + + const getValueFunc = time => { + if (time < 0) { + return 0; + } + + simulatedAnimation.currentTime = time < endTime ? time : endTime; + return win.getComputedStyle(simulatedElement).opacity; + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + keyframes, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: "animation-computed-timing-path", + style: { opacity }, + transform: `translate(${offset})`, + }, + super.renderGraph(state, helper) + ); + } +} + +module.exports = ComputedTimingPath; diff --git a/devtools/client/inspector/animation/components/graph/DelaySign.js b/devtools/client/inspector/animation/components/graph/DelaySign.js new file mode 100644 index 0000000000..4e817a9d61 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/DelaySign.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class DelaySign extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { animation, timeScale } = this.props; + const { delay, isDelayFilled, startTime } = animation.state.absoluteValues; + + const toPercentage = v => (v / timeScale.getDuration()) * 100; + const offset = toPercentage(startTime - timeScale.minStartTime); + const width = toPercentage(Math.abs(delay)); + + return dom.div({ + className: + "animation-delay-sign" + + (delay < 0 ? " negative" : "") + + (isDelayFilled ? " fill" : ""), + style: { + width: `${width}%`, + marginInlineStart: `${offset}%`, + }, + }); + } +} + +module.exports = DelaySign; diff --git a/devtools/client/inspector/animation/components/graph/EffectTimingPath.js b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js new file mode 100644 index 0000000000..4a984bc61c --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); +const TimingPath = require("resource://devtools/client/inspector/animation/components/graph/TimingPath.js"); + +class EffectTimingPath extends TimingPath { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + durationPerPixel: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + offset, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + const simulatedAnimation = simulateAnimation(null, effectTiming, false); + + if (!simulatedAnimation) { + return null; + } + + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + const getValueFunc = time => { + if (time < 0) { + return 0; + } + + simulatedAnimation.currentTime = time < endTime ? time : endTime; + return Math.max( + simulatedAnimation.effect.getComputedTiming().progress, + 0 + ); + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + null, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: "animation-effect-timing-path", + transform: `translate(${offset})`, + }, + super.renderGraph(state, helper) + ); + } +} + +module.exports = EffectTimingPath; diff --git a/devtools/client/inspector/animation/components/graph/EndDelaySign.js b/devtools/client/inspector/animation/components/graph/EndDelaySign.js new file mode 100644 index 0000000000..f843123cbf --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/EndDelaySign.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class EndDelaySign extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { animation, timeScale } = this.props; + const { endDelay, endTime, isEndDelayFilled } = + animation.state.absoluteValues; + + const toPercentage = v => (v / timeScale.getDuration()) * 100; + const absEndDelay = Math.abs(endDelay); + const offset = toPercentage(endTime - absEndDelay - timeScale.minStartTime); + const width = toPercentage(absEndDelay); + + return dom.div({ + className: + "animation-end-delay-sign" + + (endDelay < 0 ? " negative" : "") + + (isEndDelayFilled ? " fill" : ""), + style: { + width: `${width}%`, + marginInlineStart: `${offset}%`, + }, + }); + } +} + +module.exports = EndDelaySign; diff --git a/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js b/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js new file mode 100644 index 0000000000..0cd87dd320 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const NegativePath = require("resource://devtools/client/inspector/animation/components/graph/NegativePath.js"); + +class NegativeDelayPath extends NegativePath { + getClassName() { + return "animation-negative-delay-path"; + } + + renderGraph(state, helper) { + const startTime = state.delay; + const endTime = 0; + const segments = helper.createPathSegments(startTime, endTime); + + return dom.path({ + d: helper.toPathString(segments), + }); + } +} + +module.exports = NegativeDelayPath; diff --git a/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js b/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js new file mode 100644 index 0000000000..88f5f65037 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const NegativePath = require("resource://devtools/client/inspector/animation/components/graph/NegativePath.js"); + +class NegativeEndDelayPath extends NegativePath { + getClassName() { + return "animation-negative-end-delay-path"; + } + + renderGraph(state, helper) { + const endTime = state.delay + state.iterationCount * state.duration; + const startTime = endTime + state.endDelay; + const segments = helper.createPathSegments(startTime, endTime); + + return dom.path({ + d: helper.toPathString(segments), + }); + } +} + +module.exports = NegativeEndDelayPath; diff --git a/devtools/client/inspector/animation/components/graph/NegativePath.js b/devtools/client/inspector/animation/components/graph/NegativePath.js new file mode 100644 index 0000000000..4f1d0af9e6 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativePath.js @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +class NegativePath extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + className: PropTypes.string.isRequired, + durationPerPixel: PropTypes.number.isRequired, + keyframes: PropTypes.object.isRequired, + offset: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + fill: "both", + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + // Create new keyframes for opacity as computed style. + // The reason why we use computed value instead of computed timing progress is to + // include the easing in keyframes as well. Although the computed timing progress + // is not affected by the easing in keyframes at all, computed value reflects that. + const frames = keyframes.map(keyframe => { + return { + opacity: keyframe.offset, + offset: keyframe.offset, + easing: keyframe.easing, + }; + }); + + const simulatedAnimation = simulateAnimation(frames, effectTiming, true); + + if (!simulatedAnimation) { + return null; + } + + const simulatedElement = simulatedAnimation.effect.target; + const win = simulatedElement.ownerGlobal; + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + // Set the underlying opacity to zero so that if we sample the animation's output + // during the delay phase and it is not filling backwards, we get zero. + simulatedElement.style.opacity = 0; + + const getValueFunc = time => { + simulatedAnimation.currentTime = time; + return win.getComputedStyle(simulatedElement).opacity; + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + keyframes, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: this.getClassName(), + transform: `translate(${offset})`, + }, + this.renderGraph(state, helper) + ); + } +} + +module.exports = NegativePath; diff --git a/devtools/client/inspector/animation/components/graph/SummaryGraph.js b/devtools/client/inspector/animation/components/graph/SummaryGraph.js new file mode 100644 index 0000000000..bac54ea26f --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/SummaryGraph.js @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationName = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/AnimationName.js") +); +const DelaySign = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/DelaySign.js") +); +const EndDelaySign = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/EndDelaySign.js") +); +const SummaryGraphPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/SummaryGraphPath.js") +); + +const { + getFormattedTitle, + getFormatStr, + getStr, + numberWithDecimals, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +class SummaryGraph extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + selectAnimation: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this.onClick = this.onClick.bind(this); + } + + onClick(event) { + event.stopPropagation(); + this.props.selectAnimation(this.props.animation); + } + + getTitleText(state) { + const getTime = time => + getFormatStr("player.timeLabel", numberWithDecimals(time / 1000, 2)); + const getTimeOrInfinity = time => + time === Infinity ? getStr("player.infiniteDurationText") : getTime(time); + + let text = ""; + + // Adding the name. + text += getFormattedTitle(state); + text += "\n"; + + // Adding the delay. + if (state.delay) { + text += getStr("player.animationDelayLabel") + " "; + text += getTime(state.delay); + text += "\n"; + } + + // Adding the duration. + text += getStr("player.animationDurationLabel") + " "; + text += getTimeOrInfinity(state.duration); + text += "\n"; + + // Adding the endDelay. + if (state.endDelay) { + text += getStr("player.animationEndDelayLabel") + " "; + text += getTime(state.endDelay); + text += "\n"; + } + + // Adding the iteration count (the infinite symbol, or an integer). + if (state.iterationCount !== 1) { + text += getStr("player.animationIterationCountLabel") + " "; + text += + state.iterationCount || getStr("player.infiniteIterationCountText"); + text += "\n"; + } + + // Adding the iteration start. + if (state.iterationStart !== 0) { + text += getFormatStr( + "player.animationIterationStartLabel2", + state.iterationStart, + getTimeOrInfinity(state.iterationStart * state.duration) + ); + text += "\n"; + } + + // Adding the easing if it is not "linear". + if (state.easing && state.easing !== "linear") { + text += getStr("player.animationOverallEasingLabel") + " "; + text += state.easing; + text += "\n"; + } + + // Adding the fill mode. + if (state.fill && state.fill !== "none") { + text += getStr("player.animationFillLabel") + " "; + text += state.fill; + text += "\n"; + } + + // Adding the direction mode if it is not "normal". + if (state.direction && state.direction !== "normal") { + text += getStr("player.animationDirectionLabel") + " "; + text += state.direction; + text += "\n"; + } + + // Adding the playback rate if it's different than 1. + if (state.playbackRate !== 1) { + text += getStr("player.animationRateLabel") + " "; + text += state.playbackRate; + text += "\n"; + } + + // Adding the animation-timing-function + // if it is not "ease" which is default value for CSS Animations. + if ( + state.animationTimingFunction && + state.animationTimingFunction !== "ease" + ) { + text += getStr("player.animationTimingFunctionLabel") + " "; + text += state.animationTimingFunction; + text += "\n"; + } + + // Adding a note that the animation is running on the compositor thread if + // needed. + if (state.propertyState) { + if ( + state.propertyState.every(propState => propState.runningOnCompositor) + ) { + text += getStr("player.allPropertiesOnCompositorTooltip"); + } else if ( + state.propertyState.some(propState => propState.runningOnCompositor) + ) { + text += getStr("player.somePropertiesOnCompositorTooltip"); + } + } else if (state.isRunningOnCompositor) { + text += getStr("player.runningOnCompositorTooltip"); + } + + return text; + } + + render() { + const { animation, getAnimatedPropertyMap, simulateAnimation, timeScale } = + this.props; + + const { iterationCount } = animation.state; + const { delay, endDelay } = animation.state.absoluteValues; + + return dom.div( + { + className: + "animation-summary-graph" + + (animation.state.isRunningOnCompositor ? " compositor" : ""), + onClick: this.onClick, + title: this.getTitleText(animation.state), + }, + SummaryGraphPath({ + animation, + getAnimatedPropertyMap, + simulateAnimation, + timeScale, + }), + delay + ? DelaySign({ + animation, + timeScale, + }) + : null, + iterationCount && endDelay + ? EndDelaySign({ + animation, + timeScale, + }) + : null, + animation.state.name + ? AnimationName({ + animation, + }) + : null + ); + } +} + +module.exports = SummaryGraph; diff --git a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js new file mode 100644 index 0000000000..0183c7413e --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.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 { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js"); + +const ComputedTimingPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/ComputedTimingPath.js") +); +const EffectTimingPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/EffectTimingPath.js") +); +const NegativeDelayPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/NegativeDelayPath.js") +); +const NegativeEndDelayPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js") +); +const { + DEFAULT_GRAPH_HEIGHT, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +// Minimum opacity for semitransparent fill color for keyframes's easing graph. +const MIN_KEYFRAMES_EASING_OPACITY = 0.5; + +class SummaryGraphPath extends Component { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + getAnimatedPropertyMap: PropTypes.object.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + // Duration which can display in one pixel. + durationPerPixel: 0, + // To avoid rendering while the state is updating + // since we call an async function in updateState. + isStateUpdating: false, + // List of keyframe which consists by only offset and easing. + keyframesList: [], + }; + } + + componentDidMount() { + // No need to set isStateUpdating state since paint sequence is finish here. + this.updateState(this.props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({ isStateUpdating: true }); + this.updateState(nextProps); + } + + shouldComponentUpdate(nextProps, nextState) { + return !nextState.isStateUpdating; + } + + /** + * Return animatable keyframes list which has only offset and easing. + * Also, this method remove duplicate keyframes. + * For example, if the given animatedPropertyMap is, + * [ + * { + * key: "color", + * values: [ + * { + * offset: 0, + * easing: "ease", + * value: "rgb(255, 0, 0)", + * }, + * { + * offset: 1, + * value: "rgb(0, 255, 0)", + * }, + * ], + * }, + * { + * key: "opacity", + * values: [ + * { + * offset: 0, + * easing: "ease", + * value: 0, + * }, + * { + * offset: 1, + * value: 1, + * }, + * ], + * }, + * ] + * + * then this method returns, + * [ + * [ + * { + * offset: 0, + * easing: "ease", + * }, + * { + * offset: 1, + * }, + * ], + * ] + * + * @param {Map} animated property map + * which can get form getAnimatedPropertyMap in animation.js + * @return {Array} list of keyframes which has only easing and offset. + */ + getOffsetAndEasingOnlyKeyframes(animatedPropertyMap) { + return [...animatedPropertyMap.values()] + .filter((keyframes1, i, self) => { + return ( + i !== + self.findIndex((keyframes2, j) => { + return this.isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) + ? j + : -1; + }) + ); + }) + .map(keyframes => { + return keyframes.map(keyframe => { + return { easing: keyframe.easing, offset: keyframe.offset }; + }); + }); + } + + /** + * Return true if given keyframes have same length, offset and easing. + * + * @param {Array} keyframes1 + * @param {Array} keyframes2 + * @return {Boolean} true: equals + */ + isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) { + if (keyframes1.length !== keyframes2.length) { + return false; + } + + for (let i = 0; i < keyframes1.length; i++) { + const keyframe1 = keyframes1[i]; + const keyframe2 = keyframes2[i]; + + if ( + keyframe1.offset !== keyframe2.offset || + keyframe1.easing !== keyframe2.easing + ) { + return false; + } + } + + return true; + } + + updateState(props) { + const { animation, getAnimatedPropertyMap, timeScale } = props; + + let animatedPropertyMap = null; + let thisEl = null; + + try { + animatedPropertyMap = getAnimatedPropertyMap(animation); + thisEl = ReactDOM.findDOMNode(this); + } catch (e) { + // Expected if we've already been destroyed or other node have been selected + // in the meantime. + console.error(e); + return; + } + + const keyframesList = + this.getOffsetAndEasingOnlyKeyframes(animatedPropertyMap); + const totalDuration = + timeScale.getDuration() * Math.abs(animation.state.playbackRate); + const durationPerPixel = totalDuration / thisEl.parentNode.clientWidth; + + this.setState({ + durationPerPixel, + isStateUpdating: false, + keyframesList, + }); + } + + render() { + const { durationPerPixel, keyframesList } = this.state; + const { animation, simulateAnimation, timeScale } = this.props; + + if (!durationPerPixel || !animation.state.type) { + // Undefined animation.state.type means that the animation had been removed already. + // Even if the animation was removed, we still need the empty svg since the + // component might be re-used. + return dom.svg(); + } + + const { playbackRate } = animation.state; + const { createdTime } = animation.state.absoluteValues; + const absPlaybackRate = Math.abs(playbackRate); + + // Absorb the playbackRate in viewBox of SVG and offset of child path elements + // in order to each graph path components can draw without considering to the + // playbackRate. + const offset = createdTime * absPlaybackRate; + const startTime = timeScale.minStartTime * absPlaybackRate; + const totalDuration = timeScale.getDuration() * absPlaybackRate; + const opacity = Math.max( + 1 / keyframesList.length, + MIN_KEYFRAMES_EASING_OPACITY + ); + + return dom.svg( + { + className: "animation-summary-graph-path", + preserveAspectRatio: "none", + viewBox: + `${startTime} -${DEFAULT_GRAPH_HEIGHT} ` + + `${totalDuration} ${DEFAULT_GRAPH_HEIGHT}`, + }, + keyframesList.map(keyframes => + ComputedTimingPath({ + animation, + durationPerPixel, + keyframes, + offset, + opacity, + simulateAnimation, + totalDuration, + }) + ), + animation.state.easing !== "linear" + ? EffectTimingPath({ + animation, + durationPerPixel, + offset, + simulateAnimation, + totalDuration, + }) + : null, + animation.state.delay < 0 + ? keyframesList.map(keyframes => { + return NegativeDelayPath({ + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + }); + }) + : null, + animation.state.iterationCount && animation.state.endDelay < 0 + ? keyframesList.map(keyframes => { + return NegativeEndDelayPath({ + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + }); + }) + : null + ); + } +} + +module.exports = SummaryGraphPath; diff --git a/devtools/client/inspector/animation/components/graph/TimingPath.js b/devtools/client/inspector/animation/components/graph/TimingPath.js new file mode 100644 index 0000000000..7949527988 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/TimingPath.js @@ -0,0 +1,450 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +// Show max 10 iterations for infinite animations +// to give users a clue that the animation does repeat. +const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10; + +class TimingPath extends PureComponent { + /** + * Render a graph of given parameters and return as element list. + * + * @param {Object} state + * State of animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + * @return {Array} + * list of element. + */ + renderGraph(state, helper) { + // Starting time of main iteration. + let mainIterationStartTime = 0; + let iterationStart = state.iterationStart; + let iterationCount = state.iterationCount ? state.iterationCount : Infinity; + + const pathList = []; + + // Append delay. + if (state.delay > 0) { + this.renderDelay(pathList, state, helper); + mainIterationStartTime = state.delay; + } else { + const negativeDelayCount = -state.delay / state.duration; + // Move to forward the starting point for negative delay. + iterationStart += negativeDelayCount; + // Consume iteration count by negative delay. + if (iterationCount !== Infinity) { + iterationCount -= negativeDelayCount; + } + } + + if (state.duration === Infinity) { + this.renderInfinityDuration( + pathList, + state, + mainIterationStartTime, + helper + ); + return pathList; + } + + // Append 1st section of iterations, + // This section is only useful in cases where iterationStart has decimals. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75. + const firstSectionCount = + iterationStart % 1 === 0 + ? 0 + : Math.min(1 - (iterationStart % 1), iterationCount); + + if (firstSectionCount) { + this.renderFirstIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ); + } + + if (iterationCount === Infinity) { + // If the animation repeats infinitely, + // we fill the remaining area with iteration paths. + this.renderInfinity( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ); + } else { + // Otherwise, we show remaining iterations, endDelay and fill. + + // Append forwards fill-mode. + if (state.fill === "both" || state.fill === "forwards") { + this.renderForwardsFill( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ); + } + + // Append middle section of iterations. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2. + const middleSectionCount = Math.floor(iterationCount - firstSectionCount); + this.renderMiddleIterations( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + helper + ); + + // Append last section of iterations, if there is remaining iteration. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25. + const lastSectionCount = + iterationCount - middleSectionCount - firstSectionCount; + if (lastSectionCount) { + this.renderLastIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + lastSectionCount, + helper + ); + } + + // Append endDelay. + if (state.endDelay > 0) { + this.renderEndDelay( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ); + } + } + return pathList; + } + + /** + * Render 'delay' part in animation and add a element to given pathList. + * + * @param {Array} pathList + * Add rendered element to this array. + * @param {Object} state + * State of animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderDelay(pathList, state, helper) { + const startSegment = helper.getSegment(0); + const endSegment = { x: state.delay, y: startSegment.y }; + const segments = [startSegment, endSegment]; + pathList.push( + dom.path({ + className: "animation-delay-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render 1st section of iterations and add a element to given pathList. + * This section is only useful in cases where iterationStart has decimals. + * + * @param {Array} pathList + * Add rendered element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Start time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderFirstIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ) { + const startTime = mainIterationStartTime; + const endTime = startTime + firstSectionCount * state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render middle iterations and add elements to given pathList. + * + * @param {Array} pathList + * Add rendered elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {Number} middleSectionCount + * Iteration count of middle section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderMiddleIterations( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + helper + ) { + const offset = mainIterationStartTime + firstSectionCount * state.duration; + for (let i = 0; i < middleSectionCount; i++) { + // Get the path segments of each iteration. + const startTime = offset + i * state.duration; + const endTime = startTime + state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + } + + /** + * Render last section of iterations and add a element to given pathList. + * This section is only useful in cases where iterationStart has decimals. + * + * @param {Array} pathList + * Add rendered elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {Number} middleSectionCount + * Iteration count of middle section. + * @param {Number} lastSectionCount + * Iteration count of last section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderLastIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + lastSectionCount, + helper + ) { + const startTime = + mainIterationStartTime + + (firstSectionCount + middleSectionCount) * state.duration; + const endTime = startTime + lastSectionCount * state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render infinity iterations and add elements to given pathList. + * + * @param {Array} pathList + * Add rendered elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderInfinity( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ) { + // Calculate the number of iterations to display, + // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS + let uncappedInfinityIterationCount = + (helper.totalDuration - firstSectionCount * state.duration) / + state.duration; + // If there is a small floating point error resulting in, e.g. 1.0000001 + // ceil will give us 2 so round first. + uncappedInfinityIterationCount = parseFloat( + uncappedInfinityIterationCount.toPrecision(6) + ); + const infinityIterationCount = Math.min( + MAX_INFINITE_ANIMATIONS_ITERATIONS, + Math.ceil(uncappedInfinityIterationCount) + ); + + // Append first full iteration path. + const firstStartTime = + mainIterationStartTime + firstSectionCount * state.duration; + const firstEndTime = firstStartTime + state.duration; + const firstSegments = helper.createPathSegments( + firstStartTime, + firstEndTime + ); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(firstSegments), + }) + ); + + // Append other iterations. We can copy first segments. + const isAlternate = state.direction.match(/alternate/); + for (let i = 1; i < infinityIterationCount; i++) { + const startTime = firstStartTime + i * state.duration; + let segments; + if (isAlternate && i % 2) { + // Copy as reverse. + segments = firstSegments.map(segment => { + return { x: firstEndTime - segment.x + startTime, y: segment.y }; + }); + } else { + // Copy as is. + segments = firstSegments.map(segment => { + return { x: segment.x - firstStartTime + startTime, y: segment.y }; + }); + } + pathList.push( + dom.path({ + className: "animation-iteration-path infinity", + d: helper.toPathString(segments), + }) + ); + } + } + + /** + * Render infinity duration. + * + * @param {Array} pathList + * Add rendered elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderInfinityDuration(pathList, state, mainIterationStartTime, helper) { + const startSegment = helper.getSegment(mainIterationStartTime); + const endSegment = { x: helper.totalDuration, y: startSegment.y }; + const segments = [startSegment, endSegment]; + pathList.push( + dom.path({ + className: "animation-iteration-path infinity-duration", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render 'endDelay' part in animation and add a element to given pathList. + * + * @param {Array} pathList + * Add rendered element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} iterationCount + * Iteration count of whole animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderEndDelay( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ) { + const startTime = mainIterationStartTime + iterationCount * state.duration; + const startSegment = helper.getSegment(startTime); + const endSegment = { x: startTime + state.endDelay, y: startSegment.y }; + pathList.push( + dom.path({ + className: "animation-enddelay-path", + d: helper.toPathString([startSegment, endSegment]), + }) + ); + } + + /** + * Render 'fill' for forwards part in animation and + * add a element to given pathList. + * + * @param {Array} pathList + * Add rendered element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} iterationCount + * Iteration count of whole animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderForwardsFill( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ) { + const startTime = + mainIterationStartTime + + iterationCount * state.duration + + (state.endDelay > 0 ? state.endDelay : 0); + const startSegment = helper.getSegment(startTime); + const endSegment = { x: helper.totalDuration, y: startSegment.y }; + pathList.push( + dom.path({ + className: "animation-fill-forwards-path", + d: helper.toPathString([startSegment, endSegment]), + }) + ); + } +} + +module.exports = TimingPath; diff --git a/devtools/client/inspector/animation/components/graph/moz.build b/devtools/client/inspector/animation/components/graph/moz.build new file mode 100644 index 0000000000..866bdd30ce --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/moz.build @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "AnimationName.js", + "ComputedTimingPath.js", + "DelaySign.js", + "EffectTimingPath.js", + "EndDelaySign.js", + "NegativeDelayPath.js", + "NegativeEndDelayPath.js", + "NegativePath.js", + "SummaryGraph.js", + "SummaryGraphPath.js", + "TimingPath.js", +) diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js new file mode 100644 index 0000000000..120b61c73b --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { colorUtils } = require("resource://devtools/shared/css/color.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 }; + +/* Count for linearGradient ID */ +let LINEAR_GRADIENT_ID_COUNT = 0; + +class ColorPath extends ComputedStylePath { + constructor(props) { + super(props); + + this.state = this.propToState(props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState(this.propToState(nextProps)); + } + + getPropertyName() { + return "color"; + } + + getPropertyValue(keyframe) { + return keyframe.value; + } + + propToState({ keyframes, name }) { + const maxObject = { distance: -Number.MAX_VALUE }; + + for (let i = 0; i < keyframes.length - 1; i++) { + const value1 = getRGBA(name, keyframes[i].value); + for (let j = i + 1; j < keyframes.length; j++) { + const value2 = getRGBA(name, keyframes[j].value); + const distance = getRGBADistance(value1, value2); + + if (maxObject.distance >= distance) { + continue; + } + + maxObject.distance = distance; + maxObject.value1 = value1; + maxObject.value2 = value2; + } + } + + const maxDistance = maxObject.distance; + const baseValue = + maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2; + + return { baseValue, maxDistance, name }; + } + + toSegmentValue(computedStyle) { + const { baseValue, maxDistance, name } = this.state; + const value = getRGBA(name, computedStyle); + return getRGBADistance(baseValue, value) / maxDistance; + } + + /** + * Overide parent's method. + */ + renderEasingHint() { + const { easingHintStrokeWidth, graphHeight, keyframes, totalDuration } = + this.props; + + const hints = []; + + for (let i = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const endKeyframe = keyframes[i + 1]; + const startTime = startKeyframe.offset * totalDuration; + const endTime = endKeyframe.offset * totalDuration; + + const g = dom.g( + { + className: "hint", + }, + dom.title({}, startKeyframe.easing), + dom.rect({ + x: startTime, + y: -graphHeight, + height: graphHeight, + width: endTime - startTime, + }), + dom.line({ + x1: startTime, + y1: -graphHeight, + x2: endTime, + y2: -graphHeight, + style: { + "stroke-width": easingHintStrokeWidth, + }, + }) + ); + hints.push(g); + } + + return hints; + } + + /** + * Overide parent's method. + */ + renderPathSegments(segments) { + for (const segment of segments) { + segment.y = 1; + } + + const lastSegment = segments[segments.length - 1]; + const id = `color-property-${LINEAR_GRADIENT_ID_COUNT++}`; + const path = super.renderPathSegments(segments, { fill: `url(#${id})` }); + const linearGradient = dom.linearGradient( + { id }, + segments.map(segment => { + return dom.stop({ + stopColor: segment.computedStyle, + offset: segment.x / lastSegment.x, + }); + }) + ); + + return [path, linearGradient]; + } + + render() { + return dom.g( + { + className: "color-path", + }, + super.renderGraph() + ); + } +} + +/** + * Parse given RGBA string. + * + * @param {String} propertyName + * @param {String} colorString + * e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on. + * @return {Object} + * RGBA {r: r, g: g, b: b, a: a}. + */ +function getRGBA(propertyName, colorString) { + // Special handling for CSS property which can specify the not normal CSS color value. + switch (propertyName) { + case "caret-color": { + // This property can specify "auto" keyword. + if (colorString === "auto") { + return DEFAULT_COLOR; + } + break; + } + case "scrollbar-color": { + // This property can specify "auto", "dark", "light" keywords and multiple colors. + if ( + ["auto", "dark", "light"].includes(colorString) || + colorString.indexOf(" ") > 0 + ) { + return DEFAULT_COLOR; + } + break; + } + } + + const color = new colorUtils.CssColor(colorString); + return color.getRGBATuple(); +} + +/** + * Return the distance from give two RGBA. + * + * @param {Object} rgba1 + * RGBA (format is same to getRGBA) + * @param {Object} rgba2 + * RGBA (format is same to getRGBA) + * @return {Number} + * The range is 0 - 1.0. + */ +function getRGBADistance(rgba1, rgba2) { + const startA = rgba1.a; + const startR = rgba1.r * startA; + const startG = rgba1.g * startA; + const startB = rgba1.b * startA; + const endA = rgba2.a; + const endR = rgba2.r * endA; + const endG = rgba2.g * endA; + const endB = rgba2.b * endA; + const diffA = startA - endA; + const diffR = startR - endR; + const diffG = startG - endG; + const diffB = startB - endB; + return Math.sqrt( + diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB + ); +} + +module.exports = ColorPath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js new file mode 100644 index 0000000000..1da5c4da96 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.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 { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + createPathSegments, + DEFAULT_DURATION_RESOLUTION, + getPreferredProgressThresholdByKeyframes, + toPathString, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +/* + * This class is an abstraction for computed style path of keyframes. + * Subclass of this should implement the following methods: + * + * getPropertyName() + * Returns property name which will be animated. + * @return {String} + * e.g. opacity + * + * getPropertyValue(keyframe) + * Returns value which uses as animated keyframe value from given parameter. + * @param {Object} keyframe + * @return {String||Number} + * e.g. 0 + * + * toSegmentValue(computedStyle) + * Convert computed style to segment value of graph. + * @param {String||Number} + * e.g. 0 + * @return {Number} + * e.g. 0 (should be 0 - 1.0) + */ +class ComputedStylePath extends PureComponent { + static get propTypes() { + return { + componentWidth: PropTypes.number.isRequired, + easingHintStrokeWidth: PropTypes.number.isRequired, + graphHeight: PropTypes.number.isRequired, + keyframes: PropTypes.array.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + /** + * Return an array containing the path segments between the given start and + * end keyframe values. + * + * @param {Object} startKeyframe + * Starting keyframe. + * @param {Object} endKeyframe + * Ending keyframe. + * @return {Array} + * Array of path segment. + * [{x: {Number} time, y: {Number} segment value}, ...] + */ + getPathSegments(startKeyframe, endKeyframe) { + const { componentWidth, simulateAnimation, totalDuration } = this.props; + + const propertyName = this.getPropertyName(); + const offsetDistance = endKeyframe.offset - startKeyframe.offset; + const duration = offsetDistance * totalDuration; + + const keyframes = [startKeyframe, endKeyframe].map((keyframe, index) => { + return { + offset: index, + easing: keyframe.easing, + [getJsPropertyName(propertyName)]: this.getPropertyValue(keyframe), + }; + }); + const effect = { + duration, + fill: "forwards", + }; + + const simulatedAnimation = simulateAnimation(keyframes, effect, true); + + if (!simulatedAnimation) { + return null; + } + + const simulatedElement = simulatedAnimation.effect.target; + const win = simulatedElement.ownerGlobal; + const threshold = getPreferredProgressThresholdByKeyframes(keyframes); + + const getSegment = time => { + simulatedAnimation.currentTime = time; + const computedStyle = win + .getComputedStyle(simulatedElement) + .getPropertyValue(propertyName); + + return { + computedStyle, + x: time, + y: this.toSegmentValue(computedStyle), + }; + }; + + const segments = createPathSegments( + 0, + duration, + duration / componentWidth, + threshold, + DEFAULT_DURATION_RESOLUTION, + getSegment + ); + const offset = startKeyframe.offset * totalDuration; + + for (const segment of segments) { + segment.x += offset; + } + + return segments; + } + + /** + * Render easing hint from given path segments. + * + * @param {Array} segments + * Path segments. + * @return {Element} + * Element which represents easing hint. + */ + renderEasingHint(segments) { + const { easingHintStrokeWidth, keyframes, totalDuration } = this.props; + + const hints = []; + + for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const endKeyframe = keyframes[i + 1]; + const endTime = endKeyframe.offset * totalDuration; + const hintSegments = []; + + for (; indexOfSegments < segments.length; indexOfSegments++) { + const segment = segments[indexOfSegments]; + hintSegments.push(segment); + + if (startKeyframe.offset === endKeyframe.offset) { + hintSegments.push(segments[++indexOfSegments]); + break; + } else if (segment.x === endTime) { + break; + } + } + + const g = dom.g( + { + className: "hint", + }, + dom.title({}, startKeyframe.easing), + dom.path({ + d: + `M${hintSegments[0].x},${hintSegments[0].y} ` + + toPathString(hintSegments), + style: { + "stroke-width": easingHintStrokeWidth, + }, + }) + ); + + hints.push(g); + } + + return hints; + } + + /** + * Render graph. This method returns React dom. + * + * @return {Element} + */ + renderGraph() { + const { keyframes } = this.props; + + const segments = []; + + for (let i = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const endKeyframe = keyframes[i + 1]; + const keyframesSegments = this.getPathSegments( + startKeyframe, + endKeyframe + ); + + if (!keyframesSegments) { + return null; + } + + segments.push(...keyframesSegments); + } + + return [this.renderPathSegments(segments), this.renderEasingHint(segments)]; + } + + /** + * Return react dom fron given path segments. + * + * @param {Array} segments + * @param {Object} style + * @return {Element} + */ + renderPathSegments(segments, style) { + const { graphHeight } = this.props; + + for (const segment of segments) { + segment.y *= graphHeight; + } + + let d = `M${segments[0].x},0 `; + d += toPathString(segments); + d += `L${segments[segments.length - 1].x},0 Z`; + + return dom.path({ d, style }); + } +} + +/** + * Convert given CSS property name to JavaScript CSS name. + * + * @param {String} cssPropertyName + * CSS property name (e.g. background-color). + * @return {String} + * JavaScript CSS property name (e.g. backgroundColor). + */ +function getJsPropertyName(cssPropertyName) { + if (cssPropertyName == "float") { + return "cssFloat"; + } + // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + return cssPropertyName.replace(/-([a-z])/gi, (str, group) => { + return group.toUpperCase(); + }); +} + +module.exports = ComputedStylePath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js new file mode 100644 index 0000000000..26e5373f7c --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +class DiscretePath extends ComputedStylePath { + static get propTypes() { + return { + name: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = this.propToState(props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState(this.propToState(nextProps)); + } + + getPropertyName() { + return this.props.name; + } + + getPropertyValue(keyframe) { + return keyframe.value; + } + + propToState({ getComputedStyle, keyframes, name }) { + const discreteValues = []; + + for (const keyframe of keyframes) { + const style = getComputedStyle(name, { [name]: keyframe.value }); + + if (!discreteValues.includes(style)) { + discreteValues.push(style); + } + } + + return { discreteValues }; + } + + toSegmentValue(computedStyle) { + const { discreteValues } = this.state; + return discreteValues.indexOf(computedStyle) / (discreteValues.length - 1); + } + + render() { + return dom.g( + { + className: "discrete-path", + }, + super.renderGraph() + ); + } +} + +module.exports = DiscretePath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js new file mode 100644 index 0000000000..3436366c30 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +class DistancePath extends ComputedStylePath { + getPropertyName() { + return "opacity"; + } + + getPropertyValue(keyframe) { + return keyframe.distance; + } + + toSegmentValue(computedStyle) { + return computedStyle; + } + + render() { + return dom.g( + { + className: "distance-path", + }, + super.renderGraph() + ); + } +} + +module.exports = DistancePath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js new file mode 100644 index 0000000000..4212fdb88f --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class KeyframeMarkerItem extends PureComponent { + static get propTypes() { + return { + keyframe: PropTypes.object.isRequired, + }; + } + + render() { + const { keyframe } = this.props; + + return dom.li({ + className: "keyframe-marker-item", + title: keyframe.value, + style: { + marginInlineStart: `${keyframe.offset * 100}%`, + }, + }); + } +} + +module.exports = KeyframeMarkerItem; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js new file mode 100644 index 0000000000..7fd112f641 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const KeyframeMarkerItem = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js") +); + +class KeyframeMarkerList extends PureComponent { + static get propTypes() { + return { + keyframes: PropTypes.array.isRequired, + }; + } + + render() { + const { keyframes } = this.props; + + return dom.ul( + { + className: "keyframe-marker-list", + }, + keyframes.map(keyframe => KeyframeMarkerItem({ keyframe })) + ); + } +} + +module.exports = KeyframeMarkerList; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js new file mode 100644 index 0000000000..cbab6806d2 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const KeyframeMarkerList = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js") +); +const KeyframesGraphPath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js") +); + +class KeyframesGraph extends PureComponent { + static get propTypes() { + return { + getComputedStyle: PropTypes.func.isRequired, + keyframes: PropTypes.array.isRequired, + name: PropTypes.string.isRequired, + simulateAnimation: PropTypes.func.isRequired, + type: PropTypes.string.isRequired, + }; + } + + render() { + const { getComputedStyle, keyframes, name, simulateAnimation, type } = + this.props; + + return dom.div( + { + className: `keyframes-graph ${name}`, + }, + KeyframesGraphPath({ + getComputedStyle, + keyframes, + name, + simulateAnimation, + type, + }), + KeyframeMarkerList({ keyframes }) + ); + } +} + +module.exports = KeyframesGraph; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js new file mode 100644 index 0000000000..70c2720194 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js"); + +const ColorPath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js") +); +const DiscretePath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js") +); +const DistancePath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js") +); + +const { + DEFAULT_EASING_HINT_STROKE_WIDTH, + DEFAULT_GRAPH_HEIGHT, + DEFAULT_KEYFRAMES_GRAPH_DURATION, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +class KeyframesGraphPath extends PureComponent { + static get propTypes() { + return { + getComputedStyle: PropTypes.func.isRequired, + keyframes: PropTypes.array.isRequired, + name: PropTypes.string.isRequired, + simulateAnimation: PropTypes.func.isRequired, + type: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + componentHeight: 0, + componentWidth: 0, + }; + } + + componentDidMount() { + this.updateState(); + } + + getPathComponent(type) { + switch (type) { + case "color": + return ColorPath; + case "discrete": + return DiscretePath; + default: + return DistancePath; + } + } + + updateState() { + const thisEl = ReactDOM.findDOMNode(this); + this.setState({ + componentHeight: thisEl.parentNode.clientHeight, + componentWidth: thisEl.parentNode.clientWidth, + }); + } + + render() { + const { getComputedStyle, keyframes, name, simulateAnimation, type } = + this.props; + const { componentHeight, componentWidth } = this.state; + + if (!componentWidth) { + return dom.svg(); + } + + const pathComponent = this.getPathComponent(type); + const strokeWidthInViewBox = + (DEFAULT_EASING_HINT_STROKE_WIDTH / 2 / componentHeight) * + DEFAULT_GRAPH_HEIGHT; + + return dom.svg( + { + className: "keyframes-graph-path", + preserveAspectRatio: "none", + viewBox: + `0 -${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox} ` + + `${DEFAULT_KEYFRAMES_GRAPH_DURATION} ` + + `${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox * 2}`, + }, + pathComponent({ + componentWidth, + easingHintStrokeWidth: DEFAULT_EASING_HINT_STROKE_WIDTH, + getComputedStyle, + graphHeight: DEFAULT_GRAPH_HEIGHT, + keyframes, + name, + simulateAnimation, + totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION, + }) + ); + } +} + +module.exports = KeyframesGraphPath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/moz.build b/devtools/client/inspector/animation/components/keyframes-graph/moz.build new file mode 100644 index 0000000000..1ff518e21d --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/moz.build @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "ColorPath.js", + "ComputedStylePath.js", + "DiscretePath.js", + "DistancePath.js", + "KeyframeMarkerItem.js", + "KeyframeMarkerList.js", + "KeyframesGraph.js", + "KeyframesGraphPath.js", +) diff --git a/devtools/client/inspector/animation/components/moz.build b/devtools/client/inspector/animation/components/moz.build new file mode 100644 index 0000000000..cd8ecc05c4 --- /dev/null +++ b/devtools/client/inspector/animation/components/moz.build @@ -0,0 +1,30 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["graph", "keyframes-graph"] + +DevToolsModules( + "AnimatedPropertyItem.js", + "AnimatedPropertyList.js", + "AnimatedPropertyListContainer.js", + "AnimatedPropertyName.js", + "AnimationDetailContainer.js", + "AnimationDetailHeader.js", + "AnimationItem.js", + "AnimationList.js", + "AnimationListContainer.js", + "AnimationTarget.js", + "AnimationToolbar.js", + "App.js", + "CurrentTimeLabel.js", + "CurrentTimeScrubber.js", + "KeyframesProgressBar.js", + "NoAnimationPanel.js", + "PauseResumeButton.js", + "PlaybackRateSelector.js", + "ProgressInspectionPanel.js", + "RewindButton.js", + "TickLabels.js", + "TickLines.js", +) diff --git a/devtools/client/inspector/animation/current-time-timer.js b/devtools/client/inspector/animation/current-time-timer.js new file mode 100644 index 0000000000..4c08eb09ad --- /dev/null +++ b/devtools/client/inspector/animation/current-time-timer.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * In animation inspector, the scrubber and the progress bar moves along the current time + * of animation. However, the processing which sync with actual animations is heavy since + * we have to communication by the actor. The role of this class is to make the pseudo + * current time in animation inspector to proceed. + */ +class CurrentTimeTimer { + /** + * Constructor. + * + * @param {Object} timeScale + * @param {Bool} shouldStopAfterEndTime + * If need to stop the timer after animation end time, set true. + * @param {window} win + * Be used for requestAnimationFrame and performance. + * @param {Function} onUpdated + * Listener function to get updating. + * This function is called with 2 parameters. + * 1st: current time + * 2nd: if shouldStopAfterEndTime is true and + * the current time is over the end time, true is given. + */ + constructor(timeScale, shouldStopAfterEndTime, win, onUpdated) { + this.baseCurrentTime = timeScale.getCurrentTime(); + this.endTime = timeScale.getDuration(); + this.timerStartTime = win.performance.now(); + + this.shouldStopAfterEndTime = shouldStopAfterEndTime; + this.onUpdated = onUpdated; + this.win = win; + this.next = this.next.bind(this); + } + + destroy() { + this.stop(); + this.onUpdated = null; + this.win = null; + } + + /** + * Proceed the pseudo current time. + */ + next() { + if (this.doStop) { + return; + } + + const currentTime = + this.baseCurrentTime + this.win.performance.now() - this.timerStartTime; + + if (this.endTime < currentTime && this.shouldStopAfterEndTime) { + this.onUpdated(this.endTime, true); + return; + } + + this.onUpdated(currentTime); + this.win.requestAnimationFrame(this.next); + } + + start() { + this.next(); + } + + stop() { + this.doStop = true; + } +} + +module.exports = CurrentTimeTimer; diff --git a/devtools/client/inspector/animation/moz.build b/devtools/client/inspector/animation/moz.build new file mode 100644 index 0000000000..7620ce2875 --- /dev/null +++ b/devtools/client/inspector/animation/moz.build @@ -0,0 +1,12 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["actions", "components", "reducers", "utils"] + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] + +DevToolsModules("animation.js", "current-time-timer.js") + +with Files("**"): + BUG_COMPONENT = ("DevTools", "Inspector: Animations") diff --git a/devtools/client/inspector/animation/reducers/animations.js b/devtools/client/inspector/animation/reducers/animations.js new file mode 100644 index 0000000000..ead3e84147 --- /dev/null +++ b/devtools/client/inspector/animation/reducers/animations.js @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_ANIMATIONS, + UPDATE_DETAIL_VISIBILITY, + UPDATE_ELEMENT_PICKER_ENABLED, + UPDATE_HIGHLIGHTED_NODE, + UPDATE_PLAYBACK_RATES, + UPDATE_SELECTED_ANIMATION, + UPDATE_SIDEBAR_SIZE, +} = require("resource://devtools/client/inspector/animation/actions/index.js"); + +loader.lazyRequireGetter( + this, + "TimeScale", + "resource://devtools/client/inspector/animation/utils/timescale.js" +); + +const INITIAL_STATE = { + animations: [], + detailVisibility: false, + elementPickerEnabled: false, + highlightedNode: null, + playbackRates: [], + selectedAnimation: null, + sidebarSize: { + height: 0, + width: 0, + }, + timeScale: null, +}; + +const reducers = { + [UPDATE_ANIMATIONS](state, { animations }) { + let detailVisibility = state.detailVisibility; + let selectedAnimation = state.selectedAnimation; + + if ( + !state.selectedAnimation || + !animations.find( + animation => animation.actorID === selectedAnimation.actorID + ) + ) { + selectedAnimation = animations.length === 1 ? animations[0] : null; + detailVisibility = !!selectedAnimation; + } + + const playbackRates = getPlaybackRates(state.playbackRates, animations); + + return Object.assign({}, state, { + animations, + detailVisibility, + playbackRates, + selectedAnimation, + timeScale: new TimeScale(animations), + }); + }, + + [UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) { + const selectedAnimation = detailVisibility ? state.selectedAnimation : null; + + return Object.assign({}, state, { + detailVisibility, + selectedAnimation, + }); + }, + + [UPDATE_ELEMENT_PICKER_ENABLED](state, { elementPickerEnabled }) { + return Object.assign({}, state, { + elementPickerEnabled, + }); + }, + + [UPDATE_HIGHLIGHTED_NODE](state, { highlightedNode }) { + return Object.assign({}, state, { + highlightedNode, + }); + }, + + [UPDATE_PLAYBACK_RATES](state) { + return Object.assign({}, state, { + playbackRates: getPlaybackRates([], state.animations), + }); + }, + + [UPDATE_SELECTED_ANIMATION](state, { selectedAnimation }) { + const detailVisibility = !!selectedAnimation; + + return Object.assign({}, state, { + detailVisibility, + selectedAnimation, + }); + }, + + [UPDATE_SIDEBAR_SIZE](state, { sidebarSize }) { + return Object.assign({}, state, { + sidebarSize, + }); + }, +}; + +function getPlaybackRates(basePlaybackRate, animations) { + return [ + ...new Set( + animations.map(a => a.state.playbackRate).concat(basePlaybackRate) + ), + ]; +} + +module.exports = function (state = INITIAL_STATE, action) { + const reducer = reducers[action.type]; + return reducer ? reducer(state, action) : state; +}; diff --git a/devtools/client/inspector/animation/reducers/moz.build b/devtools/client/inspector/animation/reducers/moz.build new file mode 100644 index 0000000000..8b20a9f6cd --- /dev/null +++ b/devtools/client/inspector/animation/reducers/moz.build @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "animations.js", +) diff --git a/devtools/client/inspector/animation/test/browser.toml b/devtools/client/inspector/animation/test/browser.toml new file mode 100644 index 0000000000..a13e259df3 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser.toml @@ -0,0 +1,221 @@ +[DEFAULT] +prefs = [ + "dom.svg.pathSeg.enabled=true", + "layout.css.properties-and-values.enabled=true" +] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "current-time-scrubber_head.js", + "doc_custom_playback_rate.html", + "doc_infinity_duration.html", + "doc_multi_easings.html", + "doc_multi_keyframes.html", + "doc_multi_timings.html", + "doc_mutations_add_remove_immediately.html", + "doc_mutations_fast.html", + "doc_negative_playback_rate.html", + "doc_overflowed_delay_end_delay.html", + "doc_pseudo.html", + "doc_short_duration.html", + "doc_simple_animation.html", + "doc_special_colors.html", + "head.js", + "keyframes-graph_keyframe-marker_head.js", + "summary-graph_computed-timing-path_head.js", + "summary-graph_delay-sign_head.js", + "summary-graph_end-delay-sign_head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_animation_animated-property-list.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animated-property-list_unchanged-items.js"] + +["browser_animation_animated-property-name.js"] + +["browser_animation_animation-detail_close-button.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-detail_title.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-detail_visibility.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-list.js"] + +["browser_animation_animation-list_one-animation-select.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-list_select.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-target.js"] + +["browser_animation_animation-target_highlight.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = [ + "apple_catalina && !debug", # Disabled in Bug 1713158. Intemittent bug: Bug 1665011 + "os == 'linux' && !debug && !asan && !swgl && !ccov", # Bug 1665011 + "win11_2009", # Bug 1798331 + "a11y_checks && debug", # Bugs 1849028 and 1858041 for causing intermittent test results +] + +["browser_animation_animation-target_select.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_animation-timeline-tick.js"] + +["browser_animation_css-transition-with-playstate-idle.js"] + +["browser_animation_current-time-label.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_current-time-scrubber-rtl.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = ["os == 'linux' && debug"] # Bug 1721716 + +["browser_animation_current-time-scrubber-with-negative-delay.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_current-time-scrubber.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_current-time-scrubber_each-different-creation-time-animations.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_empty_on_invalid_nodes.js"] + +["browser_animation_fission_switch-target.js"] + +["browser_animation_indication-bar.js"] +skip-if = ["a11y_checks"] # Bugs 1858041 and 1849028 for causing intermittent a11y_checks results + +["browser_animation_infinity-duration_current-time-scrubber.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_infinity-duration_summary-graph.js"] + +["browser_animation_infinity-duration_tick-label.js"] + +["browser_animation_keyframes-graph_computed-value-path-01.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_keyframes-graph_computed-value-path-02.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_keyframes-graph_computed-value-path-03.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_keyframes-graph_computed-value-path_easing-hint.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = ["verify && !debug"] + +["browser_animation_keyframes-graph_keyframe-marker-rtl.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_keyframes-graph_keyframe-marker.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_keyframes-graph_special-colors.js"] + +["browser_animation_keyframes-progress-bar.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = ["os == 'win' && ccov"] # Bug 1490981 + +["browser_animation_keyframes-progress-bar_after-resuming.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_logic_adjust-time-with-playback-rate.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_logic_adjust-time.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_logic_auto-stop.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_logic_avoid-updating-during-hiding.js"] + +["browser_animation_logic_created-time.js"] + +["browser_animation_logic_mutations.js"] + +["browser_animation_logic_mutations_add_remove_immediately.js"] + +["browser_animation_logic_mutations_fast.js"] +skip-if = [ + "debug", + "win11_2009' && bits == 32", # Bug 1567800 + "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1567800 +] + +["browser_animation_logic_mutations_properties.js"] + +["browser_animation_logic_overflowed_delay_end-delay.js"] +skip-if = ["debug"] #bug 1480027 + +["browser_animation_logic_scroll-amount.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_pause-resume-button.js"] + +["browser_animation_pause-resume-button_end-time.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled +skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1767699 + +["browser_animation_pause-resume-button_respectively.js"] + +["browser_animation_pause-resume-button_spacebar.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_playback-rate-selector.js"] + +["browser_animation_pseudo-element.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_rewind-button.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_short-duration.js"] + +["browser_animation_summary-graph_animation-name.js"] + +["browser_animation_summary-graph_compositor.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_summary-graph_computed-timing-path_1.js"] + +["browser_animation_summary-graph_computed-timing-path_2.js"] + +["browser_animation_summary-graph_computed-timing-path_different-timescale.js"] + +["browser_animation_summary-graph_delay-sign-rtl.js"] + +["browser_animation_summary-graph_delay-sign.js"] + +["browser_animation_summary-graph_effect-timing-path.js"] + +["browser_animation_summary-graph_end-delay-sign-rtl.js"] + +["browser_animation_summary-graph_end-delay-sign.js"] + +["browser_animation_summary-graph_layout-by-seek.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_summary-graph_negative-delay-path.js"] + +["browser_animation_summary-graph_negative-end-delay-path.js"] + +["browser_animation_summary-graph_tooltip.js"] + +["browser_animation_timing_negative-playback-rate_current-time-scrubber.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_animation_timing_negative-playback-rate_summary-graph.js"] diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js new file mode 100644 index 0000000000..47661d00fc --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test following animated property list test. +// 1. Existence for animated property list. +// 2. Number of animated property item. + +const TEST_DATA = [ + { + targetClass: "animated", + expectedNumber: 1, + }, + { + targetClass: "compositor-notall", + expectedNumber: 4, + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking animated property list and items existence at initial"); + ok( + !panel.querySelector(".animated-property-list"), + "The animated-property-list should not be in the DOM at initial" + ); + + for (const { targetClass, expectedNumber } of TEST_DATA) { + info( + `Checking animated-property-list and items existence at ${targetClass}` + ); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + `.${targetClass}` + ); + + await waitUntil( + () => + panel.querySelectorAll(".animated-property-item").length === + expectedNumber + ); + ok( + true, + `The number of animated-property-list should be ${expectedNumber} at ${targetClass}` + ); + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js new file mode 100644 index 0000000000..22b889297f --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the position and the class of unchanged animated property items. + +const TEST_DATA = [ + { property: "background-color", isUnchanged: false }, + { property: "padding-left", isUnchanged: false }, + { property: "background-attachment", isUnchanged: true }, + { property: "background-clip", isUnchanged: true }, + { property: "background-image", isUnchanged: true }, + { property: "background-origin", isUnchanged: true }, + { property: "background-position-x", isUnchanged: true }, + { property: "background-position-y", isUnchanged: true }, + { property: "background-repeat", isUnchanged: true }, + { property: "background-size", isUnchanged: true }, + { property: "padding-bottom", isUnchanged: true }, + { property: "padding-right", isUnchanged: true }, + { property: "padding-top", isUnchanged: true }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".longhand"]); + const { panel } = await openAnimationInspector(); + + info("Checking unchanged animated property item"); + const itemEls = panel.querySelectorAll(".animated-property-item"); + is( + itemEls.length, + TEST_DATA.length, + `Count of animated property item should be ${TEST_DATA.length}` + ); + + for (let i = 0; i < TEST_DATA.length; i++) { + const { property, isUnchanged } = TEST_DATA[i]; + const itemEl = itemEls[i]; + + ok( + itemEl.querySelector(`.keyframes-graph.${property}`), + `Item of ${property} should display at here` + ); + + if (isUnchanged) { + ok( + itemEl.classList.contains("unchanged"), + "Animated property item should have 'unchanged' class" + ); + } else { + ok( + !itemEl.classList.contains("unchanged"), + "Animated property item should not have 'unchanged' class" + ); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js new file mode 100644 index 0000000000..a1505125f6 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the following animated property name component features: +// * name of property +// * display compositor sign when the property was running on compositor. +// * display warning when the property is runnable on compositor but was not. + +async function test_element(className, data) { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([className]); + const { panel } = await openAnimationInspector(); + + info("Checking animated property name component"); + const animatedPropertyNameEls = panel.querySelectorAll( + ".animated-property-name" + ); + is( + animatedPropertyNameEls.length, + data.length, + `Number of animated property name elements should be ${data.length}` + ); + + for (const [ + index, + animatedPropertyNameEl, + ] of animatedPropertyNameEls.entries()) { + const { property, isOnCompositor, isWarning } = data[index]; + + info(`Checking text content for ${property}`); + + const spanEl = animatedPropertyNameEl.querySelector("span"); + ok( + spanEl, + ` element should be in animated-property-name of ${property}` + ); + is(spanEl.textContent, property, `textContent should be ${property}`); + + info(`Checking compositor sign for ${property}`); + + if (isOnCompositor) { + ok( + animatedPropertyNameEl.classList.contains("compositor"), + "animatedPropertyNameEl should has .compositor class" + ); + isnot( + getComputedStyle(spanEl, "::before").width, + "auto", + "width of ::before pseud should not be auto" + ); + } else { + ok( + !animatedPropertyNameEl.classList.contains("compositor"), + "animatedPropertyNameEl should not have .compositor class" + ); + is( + getComputedStyle(spanEl, "::before").width, + "auto", + "width of ::before pseud should be auto" + ); + } + + info(`Checking warning for ${property}`); + + if (isWarning) { + ok( + animatedPropertyNameEl.classList.contains("warning"), + "animatedPropertyNameEl should has .warning class" + ); + is( + getComputedStyle(spanEl).textDecorationStyle, + "dotted", + "text-decoration-style of spanEl should be 'dotted'" + ); + is( + getComputedStyle(spanEl).textDecorationLine, + "underline", + "text-decoration-line of spanEl should be 'underline'" + ); + } else { + ok( + !animatedPropertyNameEl.classList.contains("warning"), + "animatedPropertyNameEl should not have .warning class" + ); + is( + getComputedStyle(spanEl).textDecorationStyle, + "solid", + "text-decoration-style of spanEl should be 'solid'" + ); + is( + getComputedStyle(spanEl).textDecorationLine, + "none", + "text-decoration-line of spanEl should be 'none'" + ); + } + } +} + +add_task(async function compositor_notall() { + await test_element(".compositor-notall", [ + { + property: "--ball-color", + }, + { + property: "opacity", + isOnCompositor: true, + }, + { + property: "transform", + isOnCompositor: true, + }, + { + property: "width", + }, + ]); +}); + +add_task(async function compositor_warning() { + await test_element(".compositor-warning", [ + { + property: "opacity", + isWarning: true, + }, + ]); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js new file mode 100644 index 0000000000..f7fa4cee70 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that whether close button in header of animation detail works. + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking close button in header of animation detail"); + await clickOnAnimation(animationInspector, panel, 0); + const detailEl = panel.querySelector("#animation-container .controlled"); + const win = panel.ownerGlobal; + isnot( + win.getComputedStyle(detailEl).display, + "none", + "detailEl should be visibled before clicking close button" + ); + clickOnDetailCloseButton(panel); + is( + win.getComputedStyle(detailEl).display, + "none", + "detailEl should be unvisibled after clicking close button" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js new file mode 100644 index 0000000000..91dfd2e50f --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that whether title in header of animations detail. + +const TEST_DATA = [ + { + targetClass: "cssanimation-normal", + expectedTitle: "cssanimation — CSS Animation", + }, + { + targetClass: "delay-positive", + expectedTitle: "test-delay-animation — Script Animation", + }, + { + targetClass: "easing-step", + expectedTitle: "Script Animation", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking title in each header of animation detail"); + + for (const { targetClass, expectedTitle } of TEST_DATA) { + info(`Checking title at ${targetClass}`); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + `.${targetClass}` + ); + const titleEl = panel.querySelector(".animation-detail-title"); + is( + titleEl.textContent, + expectedTitle, + `Title of "${targetClass}" should be "${expectedTitle}"` + ); + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js new file mode 100644 index 0000000000..14f406a1a3 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that whether animations detail could be displayed if there is selected animation. + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking animation detail visibility if animation was unselected"); + const detailEl = panel.querySelector("#animation-container .controlled"); + ok(detailEl, "The detail pane should be in the DOM"); + await assertDisplayStyle(detailEl, true, "detailEl should be unvisibled"); + + info( + "Checking animation detail visibility if animation was selected by click" + ); + await clickOnAnimation(animationInspector, panel, 0); + await assertDisplayStyle(detailEl, false, "detailEl should be visibled"); + + info( + "Checking animation detail visibility when choose node which has animations" + ); + await selectNode("html", inspector); + await assertDisplayStyle( + detailEl, + true, + "detailEl should be unvisibled after choose html node" + ); + + info( + "Checking animation detail visibility when choose node which has an animation" + ); + await selectNode("div", inspector); + await assertDisplayStyle( + detailEl, + false, + "detailEl should be visibled after choose .cssanimation-normal node" + ); +}); + +async function assertDisplayStyle(detailEl, isNoneExpected, description) { + const win = detailEl.ownerGlobal; + await waitUntil(() => { + const isNone = win.getComputedStyle(detailEl).display === "none"; + return isNone === isNoneExpected; + }); + ok(true, description); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list.js b/devtools/client/inspector/animation/test/browser_animation_animation-list.js new file mode 100644 index 0000000000..4f2c4419b3 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-list.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that whether animations ui could be displayed + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".long"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking animation list and items existence"); + ok( + panel.querySelector(".animation-list"), + "The animation-list is in the DOM" + ); + is( + panel.querySelectorAll(".animation-list .animation-item").length, + animationInspector.state.animations.length, + "The number of animations displayed matches the number of animations" + ); + + info( + "Checking list and items existence after select a element which has an animation" + ); + await selectNode(".animated", inspector); + await waitUntil( + () => panel.querySelectorAll(".animation-list .animation-item").length === 1 + ); + ok( + true, + "The number of animations displayed should be 1 for .animated element" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js new file mode 100644 index 0000000000..d84750385c --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the animation item has been selected from first time +// if count of the animations is one. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated"]); + const { panel } = await openAnimationInspector(); + + info("Checking whether an item element has been selected"); + is( + panel.querySelector(".animation-item").classList.contains("selected"), + true, + "The animation item should have 'selected' class" + ); + + info( + "Checking whether the element will be unselected after closing the detail pane" + ); + clickOnDetailCloseButton(panel); + is( + panel.querySelector(".animation-item").classList.contains("selected"), + false, + "The animation item should not have 'selected' class" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js new file mode 100644 index 0000000000..0d8901ef46 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the animation items in the list were selectable. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".long"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking whether 1st element will be selected"); + await clickOnAnimation(animationInspector, panel, 0); + assertSelection(panel, [true, false]); + + info("Checking whether 2nd element will be selected"); + await clickOnAnimation(animationInspector, panel, 1); + assertSelection(panel, [false, true]); + + info( + "Checking whether all elements will be unselected after closing the detail pane" + ); + clickOnDetailCloseButton(panel); + assertSelection(panel, [false, false]); +}); + +function assertSelection(panel, expectedResult) { + panel.querySelectorAll(".animation-item").forEach((item, index) => { + const shouldSelected = expectedResult[index]; + is( + item.classList.contains("selected"), + shouldSelected, + `Animation item[${index}] should ` + + `${shouldSelected ? "" : "not"} have 'selected' class` + ); + }); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target.js b/devtools/client/inspector/animation/test/browser_animation_animation-target.js new file mode 100644 index 0000000000..e38ae18755 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-target.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following AnimationTarget component works. +// * element existance +// * number of elements +// * content of element +// * title of inspect icon + +const TEST_DATA = [ + { expectedTextContent: "div.ball.animated" }, + { expectedTextContent: "div.ball.long" }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".long"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking the animation target elements existance"); + const animationItemEls = panel.querySelectorAll( + ".animation-list .animation-item" + ); + is( + animationItemEls.length, + animationInspector.state.animations.length, + "Number of animation target element should be same to number of animations " + + "that displays" + ); + + for (let i = 0; i < animationItemEls.length; i++) { + const animationItemEl = animationItemEls[i]; + animationItemEl.scrollIntoView(false); + await waitUntil(() => animationItemEl.querySelector(".animation-target")); + + const animationTargetEl = + animationItemEl.querySelector(".animation-target"); + ok( + animationTargetEl, + "The animation target element should be in each animation item element" + ); + + info("Checking the content of animation target"); + const testData = TEST_DATA[i]; + is( + animationTargetEl.textContent, + testData.expectedTextContent, + "The target element's content is correct" + ); + ok( + animationTargetEl.querySelector(".objectBox"), + "objectBox is in the page exists" + ); + ok( + animationTargetEl.querySelector(".highlight-node").title, + INSPECTOR_L10N.getStr("inspector.nodePreview.highlightNodeLabel") + ); + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js new file mode 100644 index 0000000000..0ff5b08018 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following highlighting related. +// * highlight when mouse over on a target node +// * unhighlight when mouse out from the above element +// * lock highlighting when click on the inspect icon in animation target component +// * add 'highlighting' class to animation target component during locking +// * mouseover locked target node +// * unlock highlighting when click on the above icon +// * lock highlighting when click on the other inspect icon +// * if the locked node has multi animations, +// the class will add to those animation target as well + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".multi"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + info("Check highlighting when mouse over on a target node"); + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + mouseOverOnTargetNode(animationInspector, panel, 0); + let data = await onHighlight; + assertNodeFront(data.nodeFront, "DIV", "ball animated"); + + info("Check unhighlighting when mouse out on a target node"); + const onUnhighlight = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + mouseOutOnTargetNode(animationInspector, panel, 0); + await onUnhighlight; + ok(true, "Unhighlighted the targe node"); + + info("Check node is highlighted when the inspect icon is clicked"); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + await clickOnInspectIcon(animationInspector, panel, 0); + data = await onHighlighterShown; + assertNodeFront(data.nodeFront, "DIV", "ball animated"); + await assertHighlight(panel, 0, true); + + info("Check if the animation target is still highlighted on mouse out"); + mouseOutOnTargetNode(animationInspector, panel, 0); + await wait(500); + await assertHighlight(panel, 0, true); + + info("Check no highlight event occur by mouse over locked target"); + let highlightEventCount = 0; + function onHighlighterHidden({ type }) { + if (type === inspector.highlighters.TYPES.BOXMODEL) { + highlightEventCount += 1; + } + } + inspector.highlighters.on("highlighter-hidden", onHighlighterHidden); + mouseOverOnTargetNode(animationInspector, panel, 0); + await wait(500); + is(highlightEventCount, 0, "Highlight event should not occur"); + inspector.highlighters.off("highlighter-hidden", onHighlighterHidden); + + info("Show persistent highlighter on an animation target"); + const onPersistentHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.SELECTOR + ); + await clickOnInspectIcon(animationInspector, panel, 1); + data = await onPersistentHighlighterShown; + assertNodeFront(data.nodeFront, "DIV", "ball multi"); + + info("Check the highlighted state of the animation targets"); + await assertHighlight(panel, 0, false); + await assertHighlight(panel, 1, true); + await assertHighlight(panel, 2, true); + + info("Hide persistent highlighter"); + const onPersistentHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.SELECTOR + ); + await clickOnInspectIcon(animationInspector, panel, 1); + await onPersistentHighlighterHidden; + + info("Check the highlighted state of the animation targets"); + await assertHighlight(panel, 0, false); + await assertHighlight(panel, 1, false); + await assertHighlight(panel, 2, false); +}); + +async function assertHighlight(panel, index, isHighlightExpected) { + const animationItemEl = await findAnimationItemByIndex(panel, index); + const animationTargetEl = animationItemEl.querySelector(".animation-target"); + + await waitUntil( + () => + animationTargetEl.classList.contains("highlighting") === + isHighlightExpected + ); + ok(true, `Highlighting class of animation target[${index}] is correct`); +} + +function assertNodeFront(nodeFront, tagName, classValue) { + is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName"); + is( + nodeFront.attributes[0].name, + "class", + "The highlighted node has the correct attributes" + ); + is( + nodeFront.attributes[0].value, + classValue, + "The highlighted node has the correct class" + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js b/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js new file mode 100644 index 0000000000..970778c4c6 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following selection feature related AnimationTarget component works: +// * select selected node by clicking on target node + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".multi", ".long"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Check initial status"); + is( + panel.querySelectorAll(".animation-item").length, + 3, + "The length of animations should be 3. Two .multi animations and one .long animation" + ); + + info("Check selecting an animated node by clicking on the target node"); + await clickOnTargetNode(animationInspector, panel, 0); + assertNodeFront( + animationInspector.inspector.selection.nodeFront, + "DIV", + "ball multi" + ); + is( + panel.querySelectorAll(".animation-item").length, + 2, + "The length of animations should be 2" + ); + + info("Check if the both target nodes refer to the same node"); + await clickOnTargetNode(animationInspector, panel, 1); + assertNodeFront( + animationInspector.inspector.selection.nodeFront, + "DIV", + "ball multi" + ); + is( + panel.querySelectorAll(".animation-item").length, + 2, + "The length of animations should be 2" + ); +}); + +function assertNodeFront(nodeFront, tagName, classValue) { + is( + nodeFront.tagName, + tagName, + "The highlighted node has the correct tagName" + ); + is( + nodeFront.attributes[0].name, + "class", + "The highlighted node has the correct attributes" + ); + is( + nodeFront.attributes[0].value, + classValue, + "The highlighted node has the correct class" + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js new file mode 100644 index 0000000000..03e9535558 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following timeline tick items. +// * animation list header elements existence +// * tick labels elements existence +// * count and text of tick label elements changing by the sidebar width + +const TimeScale = require("resource://devtools/client/inspector/animation/utils/timescale.js"); +const { + findOptimalTimeInterval, +} = require("resource://devtools/client/inspector/animation/utils/utils.js"); + +// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in +// AnimationTimeTickList component. +const TIME_GRADUATION_MIN_SPACING = 40; + +add_task(async function () { + await pushPref("devtools.inspector.three-pane-enabled", false); + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".end-delay", ".negative-delay"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + const timeScale = new TimeScale(animationInspector.state.animations); + + info("Checking animation list header element existence"); + const listContainerEl = panel.querySelector(".animation-list-container"); + const listHeaderEl = listContainerEl.querySelector(".devtools-toolbar"); + ok( + listHeaderEl, + "The header element should be in animation list container element" + ); + + info("Checking time tick item elements existence"); + await assertTickLabels(timeScale, listContainerEl); + const timelineTickItemLength = + listContainerEl.querySelectorAll(".tick-label").length; + + info("Checking timeline tick item elements after enlarge sidebar width"); + await setSidebarWidth("100%", inspector); + await assertTickLabels(timeScale, listContainerEl); + Assert.less( + timelineTickItemLength, + listContainerEl.querySelectorAll(".tick-label").length, + "The timeline tick item elements should increase" + ); +}); + +/** + * Assert tick label's position and label. + * + * @param {TimeScale} - timeScale + * @param {Element} - listContainerEl + */ +async function assertTickLabels(timeScale, listContainerEl) { + const timelineTickListEl = listContainerEl.querySelector(".tick-labels"); + ok( + timelineTickListEl, + "The animation timeline tick list element should be in header" + ); + + const width = timelineTickListEl.offsetWidth; + const animationDuration = timeScale.getDuration(); + const minTimeInterval = + (TIME_GRADUATION_MIN_SPACING * animationDuration) / width; + const interval = findOptimalTimeInterval(minTimeInterval); + const shiftWidth = timeScale.zeroPositionTime % interval; + const expectedTickItem = + Math.ceil(animationDuration / interval) + (shiftWidth !== 0 ? 1 : 0); + + await waitUntil( + () => + timelineTickListEl.querySelectorAll(".tick-label").length === + expectedTickItem + ); + ok(true, "The expected number of timeline ticks were found"); + + const timelineTickItemEls = + timelineTickListEl.querySelectorAll(".tick-label"); + + info("Make sure graduations are evenly distributed and show the right times"); + for (const [index, tickEl] of timelineTickItemEls.entries()) { + const left = parseFloat(tickEl.style.marginInlineStart); + let expectedPos = + (((index - 1) * interval + shiftWidth) / animationDuration) * 100; + if (shiftWidth !== 0 && index === 0) { + expectedPos = 0; + } + is( + Math.round(left), + Math.round(expectedPos), + `Graduation ${index} is positioned correctly` + ); + + // Note that the distancetoRelativeTime and formatTime functions are tested + // separately in xpcshell test test_timeScale.js, so we assume that they + // work here. + const formattedTime = timeScale.formatTime( + timeScale.distanceToRelativeTime(expectedPos, width) + ); + is( + tickEl.textContent, + formattedTime, + `Graduation ${index} has the right text content` + ); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js b/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js new file mode 100644 index 0000000000..1970884623 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that animation inspector does not fail when rendering an animation that +// transitions from the playState "idle". + +const PAGE_URL = `data:text/html;charset=utf-8, + + + + + + +
test
+ +`; + +add_task(async function () { + const tab = await addTab(PAGE_URL); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Toggle the visible class to start the animation"); + await toggleVisibleClass(tab); + + info("Wait until the scrubber is displayed"); + await waitUntil(() => panel.querySelector(".current-time-scrubber")); + const scrubberEl = panel.querySelector(".current-time-scrubber"); + + info("Wait until animations are paused"); + await waitUntilAnimationsPaused(animationInspector); + + // Check the initial position of the scrubber to detect the animation. + const scrubberX = scrubberEl.getBoundingClientRect().x; + + info("Toggle the visible class to start the animation"); + await toggleVisibleClass(tab); + + info("Wait until the scrubber starts moving"); + await waitUntil(() => scrubberEl.getBoundingClientRect().x != scrubberX); + + info("Wait until animations are paused"); + await waitUntilAnimationsPaused(animationInspector); + + // Query the scrubber element again to check that the UI is still rendered. + ok( + !!panel.querySelector(".current-time-scrubber"), + "The scrubber element is still rendered in the animation inspector panel" + ); +}); + +/** + * Local helper to toggle the "visible" class on the element with a transition defined. + */ +async function toggleVisibleClass(tab) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const win = content.wrappedJSObject; + win.document.querySelector("div").classList.toggle("visible"); + }); +} + +async function waitUntilAnimationsPaused(animationInspector) { + await waitUntil(() => { + const animations = animationInspector.state.animations; + return animations.every(animation => { + const state = animation.state.playState; + return state === "paused" || state === "finished"; + }); + }); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-label.js b/devtools/client/inspector/animation/test/browser_animation_current-time-label.js new file mode 100644 index 0000000000..0cff5b1f53 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_current-time-label.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following CurrentTimeLabel component: +// * element existence +// * label content at plural timing + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept([".keyframes-easing-step"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking current time label existence"); + const labelEl = panel.querySelector(".current-time-label"); + ok(labelEl, "current time label should exist"); + + info("Checking current time label content"); + const duration = animationInspector.state.timeScale.getDuration(); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.5); + const targetAnimation = animationInspector.state.animations[0]; + assertLabelContent(labelEl, targetAnimation.state.currentTime); + + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.2); + await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.2); + assertLabelContent(labelEl, targetAnimation.state.currentTime); + + info("Checking current time label content during running"); + // Resume + clickOnPauseResumeButton(animationInspector, panel); + const previousContent = labelEl.textContent; + + info("Wait until the time label changes"); + await waitFor(() => labelEl.textContent != previousContent); + isnot( + previousContent, + labelEl.textContent, + "Current time label should change" + ); +}); + +function assertLabelContent(labelEl, time) { + const expected = formatStopwatchTime(time); + is(labelEl.textContent, expected, `Content of label should be ${expected}`); +} + +function formatStopwatchTime(time) { + // Format falsy values as 0 + if (!time) { + return "00:00.000"; + } + + let milliseconds = parseInt(time % 1000, 10); + let seconds = parseInt((time / 1000) % 60, 10); + let minutes = parseInt(time / (1000 * 60), 10); + + const pad = (nb, max) => { + if (nb < max) { + return new Array((max + "").length - (nb + "").length + 1).join("0") + nb; + } + + return nb; + }; + + minutes = pad(minutes, 10); + seconds = pad(seconds, 10); + milliseconds = pad(milliseconds, 100); + + return `${minutes}:${seconds}.${milliseconds}`; +} diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js new file mode 100644 index 0000000000..1da1c56e10 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from current-time-scrubber_head.js */ + +// Test for CurrentTimeScrubber on RTL environment. + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "current-time-scrubber_head.js", + this + ); + await pushPref("intl.l10n.pseudo", "bidi"); + // eslint-disable-next-line no-undef + await testCurrentTimeScrubber(true); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js new file mode 100644 index 0000000000..8b2e177079 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the most left position means negative current time. + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept([ + ".cssanimation-normal", + ".delay-negative", + ]); + const { animationInspector, panel, inspector } = + await openAnimationInspector(); + + info("Checking scrubber controller existence"); + const controllerEl = panel.querySelector(".current-time-scrubber-area"); + ok(controllerEl, "scrubber controller should exist"); + + info("Checking the current time of most left scrubber position"); + const timeScale = animationInspector.state.timeScale; + clickOnCurrentTimeScrubberController(animationInspector, panel, 0); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await waitUntilCurrentTimeChangedAt( + animationInspector, + -1 * timeScale.zeroPositionTime + ); + ok(true, "Current time is correct"); + + info("Select negative current time animation"); + await selectNode(".cssanimation-normal", inspector); + await waitUntilCurrentTimeChangedAt( + animationInspector, + -1 * timeScale.zeroPositionTime + ); + ok(true, "Current time is correct"); + + info("Back to 'body' and rewind the animation"); + await selectNode("body", inspector); + await waitUntil( + () => + panel.querySelectorAll(".animation-item").length === + animationInspector.state.animations.length + ); + clickOnRewindButton(animationInspector, panel); + await waitUntilCurrentTimeChangedAt(animationInspector, 0); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js new file mode 100644 index 0000000000..76cd42f282 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from current-time-scrubber_head.js */ + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "current-time-scrubber_head.js", + this + ); + await testCurrentTimeScrubber(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js new file mode 100644 index 0000000000..6761bacb19 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether proper currentTime was set for each animations. + +const WAIT_TIME = 3000; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".still"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info( + "Add an animation to make a situation which has different creation time" + ); + await wait(WAIT_TIME); + await setClassAttribute(animationInspector, ".still", "ball compositor-all"); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2); + + info("Move the scrubber"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + + info("Check existed animations have different currentTime"); + const animations = animationInspector.state.animations; + Assert.greater( + animations[0].state.currentTime + WAIT_TIME, + animations[1].state.currentTime, + `The currentTime of added animation shold be ${WAIT_TIME}ms less than ` + + "at least that currentTime of first animation" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js b/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js new file mode 100644 index 0000000000..d3fa9166a1 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the panel shows no animation data for invalid or not animated nodes + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".long", ".still"]); + const { inspector, panel } = await openAnimationInspector(); + + info("Checking animation list and error message existence for a still node"); + const stillNode = await getNodeFront(".still", inspector); + await selectNode(stillNode, inspector); + + await waitUntil(() => panel.querySelector(".animation-error-message")); + ok( + true, + "Element which has animation-error-message class should exist for a still node" + ); + is( + panel.querySelector(".animation-error-message > p").textContent, + ANIMATION_L10N.getStr("panel.noAnimation"), + "The correct error message is displayed" + ); + ok( + !panel.querySelector(".animation-list"), + "Element which has animations class should not exist for a still node" + ); + + info( + "Show animations once to confirm if there is no animations on the comment node" + ); + await selectNode(".long", inspector); + await waitUntil(() => !panel.querySelector(".animation-error-message")); + + info("Checking animation list and error message existence for a text node"); + const commentNode = await inspector.walker.previousSibling(stillNode); + await selectNode(commentNode, inspector); + await waitUntil(() => panel.querySelector(".animation-error-message")); + ok( + panel.querySelector(".animation-error-message"), + "Element which has animation-error-message class should exist for a text node" + ); + ok( + !panel.querySelector(".animation-list"), + "Element which has animations class should not exist for a text node" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js b/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js new file mode 100644 index 0000000000..5a50e4098f --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the animation inspector works after switching targets. + +const PAGE_ON_CONTENT = `data:text/html;charset=utf-8, + + +
animation
+`; +const PAGE_ON_MAIN = "about:networking"; + +add_task(async function () { + await pushPref("devtools.inspector.three-pane-enabled", false); + + info("Open a page that runs on the content process and has animations"); + const tab = await addTab(PAGE_ON_CONTENT); + const { animationInspector, inspector } = await openAnimationInspector(); + + info("Check the length of the initial animations of the content process"); + is( + animationInspector.state.animations.length, + 0, + "The length of the initial animation is correct" + ); + + info("Check whether the mutation on content process page is worked or not"); + await assertAnimationsMutation(tab, "div", animationInspector, 1); + + info("Load a page that runs on the main process"); + await navigateTo( + PAGE_ON_MAIN, + tab.linkedBrowser, + animationInspector, + inspector + ); + await waitUntil(() => animationInspector.state.animations.length === 0); + ok(true, "The animations are replaced"); + + info("Check whether the mutation on main process page is worked or not"); + await assertAnimationsMutation(tab, "#category-http", animationInspector, 1); + + info("Load a content process page again"); + await navigateTo( + PAGE_ON_CONTENT, + tab.linkedBrowser, + animationInspector, + inspector + ); + await waitUntil(() => animationInspector.state.animations.length === 0); + ok(true, "The animations are replaced again"); + + info("Check the mutation on content process again"); + await assertAnimationsMutation(tab, "div", animationInspector, 1); +}); + +async function assertAnimationsMutation( + tab, + selector, + animationInspector, + expectedAnimationCount +) { + await hover(tab, selector); + await waitUntil( + () => animationInspector.state.animations.length === expectedAnimationCount + ); + ok(true, "Animations mutation is worked"); +} + +async function navigateTo(uri, browser, animationInspector, inspector) { + const previousAnimationsFront = animationInspector.animationsFront; + const onReloaded = inspector.once("reloaded"); + const onUpdated = inspector.once("inspector-updated"); + BrowserTestUtils.startLoadingURIString(browser, uri); + await waitUntil( + () => previousAnimationsFront !== animationInspector.animationsFront + ); + ok(true, "Target is switched correctly"); + await Promise.all([onReloaded, onUpdated]); +} + +async function hover(tab, selector) { + await SpecialPowers.spawn(tab.linkedBrowser, [selector], async s => { + const element = content.wrappedJSObject.document.querySelector(s); + InspectorUtils.addPseudoClassLock(element, ":hover", true); + }); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_indication-bar.js b/devtools/client/inspector/animation/test/browser_animation_indication-bar.js new file mode 100644 index 0000000000..829054178a --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_indication-bar.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the indication bar of both scrubber and progress bar indicates correct +// progress after resizing animation inspector. + +add_task(async function () { + await pushPref("devtools.inspector.three-pane-enabled", false); + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking timeline tick item elements after enlarge sidebar width"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await setSidebarWidth("100%", inspector); + assertPosition(".current-time-scrubber", panel, 0.5); + assertPosition(".keyframes-progress-bar", panel, 0.5); +}); + +/** + * Assert indication bar position. + * + * @param {String} indicationBarSelector + * @param {Element} panel + * @param {Number} expectedPositionRate + */ +function assertPosition(indicationBarSelector, panel, expectedPositionRate) { + const barEl = panel.querySelector(indicationBarSelector); + const parentEl = barEl.parentNode; + const rectBar = barEl.getBoundingClientRect(); + const rectParent = parentEl.getBoundingClientRect(); + const barX = rectBar.x + rectBar.width * 0.5 - rectParent.x; + const expectedPosition = rectParent.width * expectedPositionRate; + ok( + expectedPosition - 1 <= barX && barX <= expectedPosition + 1, + `Indication bar position should be approximately ${expectedPosition}` + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js new file mode 100644 index 0000000000..b4f82a950e --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the scrubber was working for even the animation of infinity duration. + +add_task(async function () { + await addTab(URL_ROOT + "doc_infinity_duration.html"); + await removeAnimatedElementsExcept([".infinity-delay-iteration-start"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Set initial state"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + const initialCurrentTime = + animationInspector.state.animations[0].state.currentTime; + + info("Check whether the animation currentTime was increased"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 1); + await waitUntil( + () => + initialCurrentTime < + animationInspector.state.animations[0].state.currentTime + ); + ok(true, "currentTime should be increased"); + + info("Check whether the progress bar was moved"); + const areaEl = panel.querySelector(".keyframes-progress-bar-area"); + const barEl = areaEl.querySelector(".keyframes-progress-bar"); + const controllerBounds = areaEl.getBoundingClientRect(); + const barBounds = barEl.getBoundingClientRect(); + const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x; + const expectedBarX = controllerBounds.width * 0.5; + Assert.less( + Math.abs(barX - expectedBarX), + 1, + "Progress bar should indicate at progress of 0.5" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js new file mode 100644 index 0000000000..44343c3aa8 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following summary graph with the animation which has infinity duration. +// * Tooltips +// * Graph path +// * Delay sign + +const TEST_DATA = [ + { + targetClass: "infinity", + expectedIterationPath: [ + { x: 0, y: 0 }, + { x: 200000, y: 0 }, + ], + expectedTooltip: { + duration: "\u221E", + }, + }, + { + targetClass: "infinity-delay-iteration-start", + expectedDelayPath: [ + { x: 0, y: 0 }, + { x: 100000, y: 0 }, + ], + expectedDelaySign: { + marginInlineStart: "0%", + width: "50%", + }, + expectedIterationPath: [ + { x: 100000, y: 50 }, + { x: 200000, y: 50 }, + ], + expectedTooltip: { + delay: "100s", + duration: "\u221E", + iterationStart: "0.5 (\u221E)", + }, + }, + { + targetClass: "limited", + expectedIterationPath: [ + { x: 0, y: 0 }, + { x: 100000, y: 100 }, + ], + expectedTooltip: { + duration: "100s", + }, + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_infinity_duration.html"); + const { panel } = await openAnimationInspector(); + + for (const testData of TEST_DATA) { + const { + targetClass, + expectedDelayPath, + expectedDelaySign, + expectedIterationPath, + expectedTooltip, + } = testData; + + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + + info(`Check tooltip for the animation of .${targetClass}`); + assertTooltip(summaryGraphEl, expectedTooltip); + + if (expectedDelayPath) { + info(`Check delay path for the animation of .${targetClass}`); + assertDelayPath(summaryGraphEl, expectedDelayPath); + } + + if (expectedDelaySign) { + info(`Check delay sign for the animation of .${targetClass}`); + assertDelaySign(summaryGraphEl, expectedDelaySign); + } + + info(`Check iteration path for the animation of .${targetClass}`); + assertIterationPath(summaryGraphEl, expectedIterationPath); + } +}); + +function assertDelayPath(summaryGraphEl, expectedPath) { + assertPath( + summaryGraphEl, + ".animation-computed-timing-path .animation-delay-path", + expectedPath + ); +} + +function assertDelaySign(summaryGraphEl, expectedSign) { + const signEl = summaryGraphEl.querySelector(".animation-delay-sign"); + + is( + signEl.style.marginInlineStart, + expectedSign.marginInlineStart, + `marginInlineStart position should be ${expectedSign.marginInlineStart}` + ); + is( + signEl.style.width, + expectedSign.width, + `Width should be ${expectedSign.width}` + ); +} + +function assertIterationPath(summaryGraphEl, expectedPath) { + assertPath( + summaryGraphEl, + ".animation-computed-timing-path .animation-iteration-path", + expectedPath + ); +} + +function assertPath(summaryGraphEl, pathSelector, expectedPath) { + const pathEl = summaryGraphEl.querySelector(pathSelector); + assertPathSegments(pathEl, true, expectedPath); +} + +function assertTooltip(summaryGraphEl, expectedTooltip) { + const tooltip = summaryGraphEl.getAttribute("title"); + const { delay, duration, iterationStart } = expectedTooltip; + + if (delay) { + const expected = `Delay: ${delay}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } + + if (duration) { + const expected = `Duration: ${duration}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } + + if (iterationStart) { + const expected = `Iteration start: ${iterationStart}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js new file mode 100644 index 0000000000..2a554267c4 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test of the content of tick label on timeline header +// with the animation which has infinity duration. + +add_task(async function () { + await addTab(URL_ROOT + "doc_infinity_duration.html"); + const { inspector, panel } = await openAnimationInspector(); + + info("Check the tick label content with limited duration animation"); + isnot( + panel.querySelector(".animation-list-container .tick-label:last-child") + .textContent, + "\u221E", + "The content should not be \u221E" + ); + + info("Check the tick label content with infinity duration animation only"); + await selectNode(".infinity", inspector); + await waitUntil( + () => + panel.querySelector(".animation-list-container .tick-label:last-child") + .textContent === "\u221E" + ); + ok(true, "The content should be \u221E"); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js new file mode 100644 index 0000000000..b893626bda --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js @@ -0,0 +1,164 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for ComputedValuePath of animations that consist by multi types of animated +// properties. + +requestLongerTimeout(2); + +const TEST_DATA = [ + { + targetClass: "multi-types", + properties: [ + { + name: "background-color", + computedValuePathClass: "color-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 1000, y: 100 }, + ], + expectedStopColors: [ + { offset: 0, color: "rgb(255, 0, 0)" }, + { offset: 1, color: "rgb(0, 255, 0)" }, + ], + }, + { + name: "background-repeat", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "font-size", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "margin-left", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "text-align", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "transform", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + { + targetClass: "multi-types-reverse", + properties: [ + { + name: "background-color", + computedValuePathClass: "color-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 1000, y: 100 }, + ], + expectedStopColors: [ + { offset: 0, color: "rgb(0, 255, 0)" }, + { offset: 1, color: "rgb(255, 0, 0)" }, + ], + }, + { + name: "background-repeat", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "font-size", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 100 }, + { x: 500, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "margin-left", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 100 }, + { x: 500, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 100 }, + { x: 500, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "text-align", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "transform", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 100 }, + { x: 500, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, +]; + +add_task(async function () { + await testKeyframesGraphComputedValuePath(TEST_DATA); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js new file mode 100644 index 0000000000..c36bd22628 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for ComputedValuePath of animations that consist by one animated property +// on complexed keyframes. + +requestLongerTimeout(2); + +const TEST_DATA = [ + { + targetClass: "steps-effect", + properties: [ + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 250, y: 25 }, + { x: 500, y: 50 }, + { x: 750, y: 75 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + { + targetClass: "steps-jump-none-keyframe", + properties: [ + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 199, y: 0 }, + { x: 200, y: 25 }, + { x: 399, y: 25 }, + { x: 400, y: 50 }, + { x: 599, y: 50 }, + { x: 600, y: 75 }, + { x: 799, y: 75 }, + { x: 800, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + { + targetClass: "narrow-offsets", + properties: [ + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + { x: 110, y: 100 }, + { x: 114.9, y: 100 }, + { x: 115, y: 50 }, + { x: 129.9, y: 50 }, + { x: 130, y: 0 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + { + targetClass: "duplicate-offsets", + properties: [ + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 100 }, + { x: 250, y: 100 }, + { x: 499, y: 100 }, + { x: 500, y: 100 }, + { x: 500, y: 0 }, + { x: 750, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, +]; + +add_task(async function () { + await testKeyframesGraphComputedValuePath(TEST_DATA); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js new file mode 100644 index 0000000000..957a693a31 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for ComputedValuePath of animations that consist by multi types of animated +// properties on complexed keyframes. + +requestLongerTimeout(2); + +const TEST_DATA = [ + { + targetClass: "middle-keyframe", + properties: [ + { + name: "background-color", + computedValuePathClass: "color-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + expectedStopColors: [ + { offset: 0, color: "rgb(255, 0, 0)" }, + { offset: 0.5, color: "rgb(0, 0, 255)" }, + { offset: 1, color: "rgb(0, 255, 0)" }, + ], + }, + { + name: "background-repeat", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 249.999, y: 0 }, + { x: 250, y: 100 }, + { x: 749.999, y: 100 }, + { x: 750, y: 0 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "font-size", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 250, y: 50 }, + { x: 500, y: 100 }, + { x: 750, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "margin-left", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 250, y: 50 }, + { x: 500, y: 100 }, + { x: 750, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 250, y: 50 }, + { x: 500, y: 100 }, + { x: 750, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "text-align", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 249.999, y: 0 }, + { x: 250, y: 100 }, + { x: 749.999, y: 100 }, + { x: 750, y: 0 }, + { x: 1000, y: 0 }, + ], + }, + { + name: "transform", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 250, y: 50 }, + { x: 500, y: 100 }, + { x: 750, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + { + targetClass: "steps-keyframe", + properties: [ + { + name: "background-color", + computedValuePathClass: "color-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + expectedStopColors: [ + { offset: 0, color: "rgb(255, 0, 0)" }, + { offset: 0.499, color: "rgb(255, 0, 0)" }, + { offset: 0.5, color: "rgb(128, 128, 0)" }, + { offset: 0.999, color: "rgb(128, 128, 0)" }, + { offset: 1, color: "rgb(0, 255, 0)" }, + ], + }, + { + name: "background-repeat", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "font-size", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "margin-left", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 50 }, + { x: 999.999, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "opacity", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 50 }, + { x: 999.999, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "text-align", + computedValuePathClass: "discrete-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 499.999, y: 0 }, + { x: 500, y: 100 }, + { x: 1000, y: 100 }, + ], + }, + { + name: "transform", + computedValuePathClass: "distance-path", + expectedPathSegments: [ + { x: 0, y: 0 }, + { x: 500, y: 0 }, + { x: 500, y: 50 }, + { x: 1000, y: 50 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, +]; + +add_task(async function () { + await testKeyframesGraphComputedValuePath(TEST_DATA); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js new file mode 100644 index 0000000000..b95c8d5fe4 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js @@ -0,0 +1,380 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following easing hint in ComputedValuePath. +// * element existence +// * path segments +// * hint text + +const TEST_DATA = [ + { + targetClass: "no-easing", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "linear", + path: [ + { x: 0, y: 100 }, + { x: 500, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "effect-easing", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "linear", + path: [ + { x: 0, y: 100 }, + { x: 199, y: 81 }, + { x: 200, y: 80 }, + { x: 399, y: 61 }, + { x: 400, y: 60 }, + { x: 599, y: 41 }, + { x: 600, y: 40 }, + { x: 799, y: 21 }, + { x: 800, y: 20 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "keyframe-easing", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "steps(2)", + path: [ + { x: 0, y: 100 }, + { x: 499, y: 100 }, + { x: 500, y: 50 }, + { x: 999, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "both-easing", + properties: [ + { + name: "margin-left", + expectedHints: [ + { + hint: "steps(1)", + path: [ + { x: 0, y: 0 }, + { x: 999, y: 0 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + { + name: "opacity", + expectedHints: [ + { + hint: "steps(2)", + path: [ + { x: 0, y: 100 }, + { x: 499, y: 100 }, + { x: 500, y: 50 }, + { x: 999, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "narrow-keyframes", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "linear", + path: [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ], + }, + { + hint: "steps(1)", + path: [ + { x: 129, y: 100 }, + { x: 130, y: 0 }, + ], + }, + { + hint: "linear", + path: [ + { x: 130, y: 0 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "duplicate-keyframes", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "linear", + path: [ + { x: 0, y: 0 }, + { x: 500, y: 100 }, + ], + }, + { + hint: "", + path: [ + { x: 500, y: 100 }, + { x: 500, y: 0 }, + ], + }, + { + hint: "steps(1)", + path: [ + { x: 500, y: 0 }, + { x: 999, y: 0 }, + { x: 1000, y: 100 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "color-keyframes", + properties: [ + { + name: "color", + expectedHints: [ + { + hint: "ease-in", + rect: { + x: 0, + height: 100, + width: 400, + }, + }, + { + hint: "ease-out", + rect: { + x: 400, + height: 100, + width: 600, + }, + }, + ], + }, + ], + }, + { + targetClass: "jump-start", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "steps(2, jump-start)", + path: [ + { x: 0, y: 50 }, + { x: 499, y: 50 }, + { x: 500, y: 0 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "jump-end", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "steps(2)", + path: [ + { x: 0, y: 100 }, + { x: 499, y: 100 }, + { x: 500, y: 50 }, + { x: 999, y: 50 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, + { + targetClass: "jump-both", + properties: [ + { + name: "opacity", + expectedHints: [ + { + hint: "steps(3, jump-both)", + path: [ + { x: 0, y: 75 }, + { x: 330, y: 75 }, + { x: 340, y: 50 }, + { x: 660, y: 50 }, + { x: 670, y: 25 }, + { x: 999, y: 25 }, + { x: 1000, y: 0 }, + ], + }, + ], + }, + ], + }, +]; + +// Prevent test timeout's on windows code coverage: Bug 1470757 +requestLongerTimeout(2); + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_easings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { animationInspector, panel } = await openAnimationInspector(); + + for (const { properties, targetClass } of TEST_DATA) { + info(`Checking keyframes graph for ${targetClass}`); + const onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + `.${targetClass}` + ); + await onDetailRendered; + + for (const { name, expectedHints } of properties) { + const testTarget = `${name} in ${targetClass}`; + info(`Checking easing hint for ${testTarget}`); + info(`Checking easing hint existence for ${testTarget}`); + const hintEls = panel.querySelectorAll(`.${name} .hint`); + is( + hintEls.length, + expectedHints.length, + `Count of easing hint elements of ${testTarget} ` + + `should be ${expectedHints.length}` + ); + + for (let i = 0; i < expectedHints.length; i++) { + const hintTarget = `hint[${i}] of ${testTarget}`; + + info(`Checking ${hintTarget}`); + const hintEl = hintEls[i]; + const expectedHint = expectedHints[i]; + + info(`Checking in ${hintTarget}`); + const titleEl = hintEl.querySelector("title"); + ok(titleEl, `<title> element in ${hintTarget} should be existence`); + is( + titleEl.textContent, + expectedHint.hint, + `Content of <title> in ${hintTarget} should be ${expectedHint.hint}` + ); + + let interactionEl = null; + let displayedEl = null; + if (expectedHint.path) { + info(`Checking <path> in ${hintTarget}`); + interactionEl = hintEl.querySelector("path"); + displayedEl = interactionEl; + ok( + interactionEl, + `The <path> element in ${hintTarget} should be existence` + ); + assertPathSegments(interactionEl, false, expectedHint.path); + } else { + info(`Checking <rect> in ${hintTarget}`); + interactionEl = hintEl.querySelector("rect"); + displayedEl = hintEl.querySelector("line"); + ok( + interactionEl, + `The <rect> element in ${hintTarget} should be existence` + ); + is( + parseInt(interactionEl.getAttribute("x"), 10), + expectedHint.rect.x, + `x of <rect> in ${hintTarget} should be ${expectedHint.rect.x}` + ); + is( + parseInt(interactionEl.getAttribute("width"), 10), + expectedHint.rect.width, + `width of <rect> in ${hintTarget} should be ${expectedHint.rect.width}` + ); + } + + info(`Checking interaction for ${hintTarget}`); + interactionEl.scrollIntoView(false); + const win = hintEl.ownerGlobal; + // Mouse over the pathEl. + ok( + isStrokeChangedByMouseOver(interactionEl, displayedEl, win), + `stroke-opacity of hintEl for ${hintTarget} should be 1 ` + + "while mouse is over the element" + ); + // Mouse out from pathEl. + EventUtils.synthesizeMouse( + panel.querySelector(".animation-toolbar"), + 0, + 0, + { type: "mouseover" }, + win + ); + is( + parseInt(win.getComputedStyle(displayedEl).strokeOpacity, 10), + 0, + `stroke-opacity of hintEl for ${hintTarget} should be 0 ` + + "while mouse is out from the element" + ); + } + } + } +}); + +function isStrokeChangedByMouseOver(mouseoverEl, displayedEl, win) { + const boundingBox = mouseoverEl.getBoundingClientRect(); + const x = boundingBox.width / 2; + + for (let y = 0; y < boundingBox.height; y++) { + EventUtils.synthesizeMouse(mouseoverEl, x, y, { type: "mouseover" }, win); + + if (win.getComputedStyle(displayedEl).strokeOpacity == 1) { + return true; + } + } + + return false; +} diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js new file mode 100644 index 0000000000..c90b231f0a --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "keyframes-graph_keyframe-marker_head.js", + this + ); + await pushPref("intl.l10n.pseudo", "bidi"); + // eslint-disable-next-line no-undef + await testKeyframesGraphKeyframesMarker(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js new file mode 100644 index 0000000000..a19c2993f2 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "keyframes-graph_keyframe-marker_head.js", + this + ); + // eslint-disable-next-line no-undef + await testKeyframesGraphKeyframesMarker(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js new file mode 100644 index 0000000000..6f46b44332 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DATA = [ + { + propertyName: "caret-color", + expectedMarkers: ["auto", "rgb(0, 255, 0)"], + }, + { + propertyName: "scrollbar-color", + expectedMarkers: ["rgb(0, 255, 0) rgb(255, 0, 0)", "auto"], + }, +]; + +// Test for animatable property which can specify the non standard CSS color value. +add_task(async function () { + await addTab(URL_ROOT + "doc_special_colors.html"); + const { panel } = await openAnimationInspector(); + + for (const { propertyName, expectedMarkers } of TEST_DATA) { + const animatedPropertyEl = panel.querySelector(`.${propertyName}`); + ok(animatedPropertyEl, `Animated property ${propertyName} exists`); + + const markerEls = animatedPropertyEl.querySelectorAll( + ".keyframe-marker-item" + ); + is( + markerEls.length, + expectedMarkers.length, + `The length of keyframe markers should ${expectedMarkers.length}` + ); + for (let i = 0; i < expectedMarkers.length; i++) { + const actualTitle = markerEls[i].title; + const expectedTitle = expectedMarkers[i]; + is(actualTitle, expectedTitle, `Value of keyframes[${i}] is correct`); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js new file mode 100644 index 0000000000..a7051d9a01 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following KeyframesProgressBar: +// * element existence +// * progress bar position in multi effect timings +// * progress bar position after changing playback rate +// * progress bar position when select another animation + +requestLongerTimeout(3); + +const TEST_DATA = [ + { + targetClass: "cssanimation-linear", + scrubberPositions: [0, 0.25, 0.5, 0.75, 1], + expectedPositions: [0, 0.25, 0.5, 0.75, 0], + }, + { + targetClass: "easing-step", + scrubberPositions: [0, 0.49, 0.5, 0.99], + expectedPositions: [0, 0, 0.5, 0.5], + }, + { + targetClass: "delay-positive", + scrubberPositions: [0, 0.33, 0.5], + expectedPositions: [0, 0, 0.25], + }, + { + targetClass: "delay-negative", + scrubberPositions: [0, 0.49, 0.5, 0.75], + expectedPositions: [0, 0, 0.5, 0.75], + }, + { + targetClass: "enddelay-positive", + scrubberPositions: [0, 0.66, 0.67, 0.99], + expectedPositions: [0, 0.99, 0, 0], + }, + { + targetClass: "enddelay-negative", + scrubberPositions: [0, 0.49, 0.5, 0.99], + expectedPositions: [0, 0.49, 0, 0], + }, + { + targetClass: "direction-reverse-with-iterations-infinity", + scrubberPositions: [0, 0.25, 0.5, 0.75, 1], + expectedPositions: [1, 0.75, 0.5, 0.25, 1], + }, + { + targetClass: "fill-both-width-delay-iterationstart", + scrubberPositions: [0, 0.33, 0.66, 0.833, 1], + expectedPositions: [0.5, 0.5, 0.99, 0.25, 0.5], + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking progress bar position in multi effect timings"); + + for (const testdata of TEST_DATA) { + const { targetClass, scrubberPositions, expectedPositions } = testdata; + + info(`Checking progress bar position for ${targetClass}`); + const onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await selectNode(`.${targetClass}`, inspector); + await onDetailRendered; + + info("Checking progress bar existence"); + const areaEl = panel.querySelector(".keyframes-progress-bar-area"); + ok(areaEl, "progress bar area should exist"); + const barEl = areaEl.querySelector(".keyframes-progress-bar"); + ok(barEl, "progress bar should exist"); + + for (let i = 0; i < scrubberPositions.length; i++) { + info(`Scrubber position is ${scrubberPositions[i]}`); + clickOnCurrentTimeScrubberController( + animationInspector, + panel, + scrubberPositions[i] + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + assertPosition(barEl, areaEl, expectedPositions[i], animationInspector); + } + } +}); + +function assertPosition(barEl, areaEl, expectedRate, animationInspector) { + const controllerBounds = areaEl.getBoundingClientRect(); + const barBounds = barEl.getBoundingClientRect(); + const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x; + const expected = controllerBounds.width * expectedRate; + ok( + expected - 1 < barX && barX < expected + 1, + `Position should apploximately be ${expected} (x of bar is ${barX})` + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js new file mode 100644 index 0000000000..8b91026799 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether keyframes progress bar moves correctly after resuming the animation. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated"]); + const { animationInspector, panel } = await openAnimationInspector(); + + const scrubberPositions = [0, 0.25, 0.5, 0.75]; + const expectedPositions = [0, 0.25, 0.5, 0.75]; + + info("Check whether the keyframes progress bar position was correct"); + await assertPosition( + panel, + scrubberPositions, + expectedPositions, + animationInspector + ); + + info( + "Check whether the keyframes progress bar position was correct " + + "after a bit time passed and resuming" + ); + await wait(500); + clickOnPauseResumeButton(animationInspector, panel); + await assertPosition( + panel, + scrubberPositions, + expectedPositions, + animationInspector + ); +}); + +async function assertPosition( + panel, + scrubberPositions, + expectedPositions, + animationInspector +) { + const areaEl = panel.querySelector(".keyframes-progress-bar-area"); + const barEl = areaEl.querySelector(".keyframes-progress-bar"); + const controllerBounds = areaEl.getBoundingClientRect(); + + for (let i = 0; i < scrubberPositions.length; i++) { + info(`Scrubber position is ${scrubberPositions[i]}`); + clickOnCurrentTimeScrubberController( + animationInspector, + panel, + scrubberPositions[i] + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + const barBounds = barEl.getBoundingClientRect(); + const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x; + const expected = controllerBounds.width * expectedPositions[i]; + ok( + expected - 1 < barX && barX < expected + 1, + `Position should apploximately be ${expected} (x of bar is ${barX})` + ); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js new file mode 100644 index 0000000000..ad356cadd2 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adjusting the created time with different playback rate of animation. + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info( + "Pause the all animation and set current time to middle in order to check " + + "the adjusting time" + ); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + + info("Check the created times of all animation are same"); + checkAdjustingTheTime( + animationInspector.state.animations[0].state, + animationInspector.state.animations[1].state + ); + + info("Change the playback rate to x10 after selecting '.div2'"); + await selectNode(".div2", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1); + await changePlaybackRateSelector(animationInspector, panel, 10); + + info("Check each adjusted result of animations after selecting 'body' again"); + await selectNode("body", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2); + + checkAdjustingTheTime( + animationInspector.state.animations[0].state, + animationInspector.state.animations[1].state + ); + + await waitUntil( + () => animationInspector.state.animations[0].state.currentTime === 50000 + ); + ok(true, "The current time of '.div1' animation is 50%"); + + await waitUntil( + () => animationInspector.state.animations[1].state.currentTime === 50000 + ); + ok(true, "The current time of '.div2' animation is 50%"); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js new file mode 100644 index 0000000000..44769ea055 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adjusting the created time with different current times of animation. + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info( + "Pause the all animation and set current time to middle time in order to " + + "check the adjusting time" + ); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + + info("Check the created times of all animation are same"); + checkAdjustingTheTime( + animationInspector.state.animations[0].state, + animationInspector.state.animations[1].state + ); + + info("Change the current time to 75% after selecting '.div2'"); + await selectNode(".div2", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.75); + + info("Check each adjusted result of animations after selecting 'body' again"); + await selectNode("body", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2); + + checkAdjustingTheTime( + animationInspector.state.animations[0].state, + animationInspector.state.animations[1].state + ); + is( + animationInspector.state.animations[0].state.currentTime, + 50000, + "The current time of '.div1' animation is 50%" + ); + is( + animationInspector.state.animations[1].state.currentTime, + 75000, + "The current time of '.div2' animation is 75%" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js b/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js new file mode 100644 index 0000000000..fdf1867ffa --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Animation inspector makes the current time to stop +// after end of animation duration except iterations infinity. +// Test followings: +// * state of animations and UI components after end of animation duration +// * state of animations and UI components after end of animation duration +// but iteration count is infinity + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".compositor-all", ".long"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking state after end of animation duration"); + await selectNode(".long", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1); + const pixelsData = getDurationAndRate(animationInspector, panel, 5); + clickOnCurrentTimeScrubberController( + animationInspector, + panel, + 1 - pixelsData.rate + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + clickOnPauseResumeButton(animationInspector, panel); + await assertStates(animationInspector, panel, false); + + info( + "Checking state after end of animation duration and infinity iterations" + ); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await selectNode(".compositor-all", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1); + clickOnCurrentTimeScrubberController(animationInspector, panel, 1); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + clickOnPauseResumeButton(animationInspector, panel); + await assertStates(animationInspector, panel, true); +}); + +async function assertStates(animationInspector, panel, shouldRunning) { + const buttonEl = panel.querySelector(".pause-resume-button"); + const labelEl = panel.querySelector(".current-time-label"); + const scrubberEl = panel.querySelector(".current-time-scrubber"); + + const previousLabelContent = labelEl.textContent; + const previousScrubberX = scrubberEl.getBoundingClientRect().x; + + await waitUntilAnimationsPlayState( + animationInspector, + shouldRunning ? "running" : "paused" + ); + + const currentLabelContent = labelEl.textContent; + const currentScrubberX = scrubberEl.getBoundingClientRect().x; + + if (shouldRunning) { + isnot( + previousLabelContent, + currentLabelContent, + "Current time label content should change" + ); + isnot( + previousScrubberX, + currentScrubberX, + "Current time scrubber position should change" + ); + ok( + !buttonEl.classList.contains("paused"), + "State of button should be running" + ); + assertAnimationsRunning(animationInspector); + } else { + is( + previousLabelContent, + currentLabelContent, + "Current time label Content should not change" + ); + is( + previousScrubberX, + currentScrubberX, + "Current time scrubber position should not change" + ); + ok( + buttonEl.classList.contains("paused"), + "State of button should be paused" + ); + assertAnimationsPausing(animationInspector); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js b/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js new file mode 100644 index 0000000000..cfdd111ec9 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Animation inspector should not update when hidden. +// Test for followings: +// * whether the UIs update after selecting another inspector +// * whether the UIs update after selecting another tool +// * whether the UIs update after selecting animation inspector again + +add_task(async function () { + info( + "Switch to 2 pane inspector to see if the animation only refreshes when visible" + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking the UIs update after selecting another inspector"); + await selectNode("head", inspector); + inspector.sidebar.select("ruleview"); + await selectNode("div", inspector); + await waitUntil(() => !animationInspector.state.animations.length); + ok(true, "Should not update after selecting another inspector"); + + await selectAnimationInspector(inspector); + await waitUntil(() => animationInspector.state.animations.length); + ok(true, "Should update after selecting animation inspector"); + + await assertCurrentTimeUpdated(animationInspector, panel, true); + inspector.sidebar.select("ruleview"); + is( + animationInspector.state.animations.length, + 1, + "Should not update after selecting another inspector again" + ); + await assertCurrentTimeUpdated(animationInspector, panel, false); + + info("Checking the UIs update after selecting another tool"); + await selectAnimationInspector(inspector); + await selectNode("head", inspector); + await waitUntil(() => !animationInspector.state.animations.length); + await inspector.toolbox.selectTool("webconsole"); + await selectNode("div", inspector); + is( + animationInspector.state.animations.length, + 0, + "Should not update after selecting another tool" + ); + await selectAnimationInspector(inspector); + await waitUntil(() => animationInspector.state.animations.length); + is( + animationInspector.state.animations.length, + 1, + "Should update after selecting animation inspector" + ); + await assertCurrentTimeUpdated(animationInspector, panel, true); + await inspector.toolbox.selectTool("webconsole"); + await waitUntil(() => animationInspector.state.animations.length); + is( + animationInspector.state.animations.length, + 1, + "Should not update after selecting another tool again" + ); + await assertCurrentTimeUpdated(animationInspector, panel, false); +}); + +async function assertCurrentTimeUpdated( + animationInspector, + panel, + shouldRunning +) { + let count = 0; + + const listener = () => { + count++; + }; + + animationInspector.addAnimationsCurrentTimeListener(listener); + await new Promise(resolve => + panel.ownerGlobal.requestAnimationFrame(resolve) + ); + animationInspector.removeAnimationsCurrentTimeListener(listener); + + if (shouldRunning) { + isnot(count, 0, "Should forward current time"); + } else { + is(count, 0, "Should not forward current time"); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js b/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js new file mode 100644 index 0000000000..59e0f4df52 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the created time of animation unchanged even if change node. + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector } = await openAnimationInspector(); + + info("Check both the created time of animation are same"); + const baseCreatedTime = + animationInspector.state.animations[0].state.createdTime; + is( + animationInspector.state.animations[1].state.createdTime, + baseCreatedTime, + "Both created time of animations should be same" + ); + + info("Check created time after selecting '.div1'"); + await selectNode(".div1", inspector); + await waitUntil( + () => + animationInspector.state.animations[0].state.createdTime === + baseCreatedTime + ); + ok( + true, + "The created time of animation on element of .div1 should unchanged" + ); + + info("Check created time after selecting '.div2'"); + await selectNode(".div2", inspector); + await waitUntil( + () => + animationInspector.state.animations[0].state.createdTime === + baseCreatedTime + ); + ok( + true, + "The created time of animation on element of .div2 should unchanged" + ); + + info("Check created time after selecting 'body' again"); + await selectNode("body", inspector); + is( + animationInspector.state.animations[0].state.createdTime, + baseCreatedTime, + "The created time of animation[0] should unchanged" + ); + is( + animationInspector.state.animations[1].state.createdTime, + baseCreatedTime, + "The created time of animation[1] should unchanged" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js new file mode 100644 index 0000000000..56228a60c2 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following mutations: +// * add animation +// * remove animation +// * modify animation + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([ + ".compositor-all", + ".compositor-notall", + ".no-compositor", + ".still", + ]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking the mutation for add an animation"); + const originalAnimationCount = animationInspector.state.animations.length; + await setClassAttribute(animationInspector, ".still", "ball no-compositor"); + await waitUntil( + () => + animationInspector.state.animations.length === originalAnimationCount + 1 + ); + ok(true, "Count of animation should be plus one to original count"); + + info( + "Checking added animation existence even the animation name is duplicated" + ); + is( + getAnimationNameCount(panel, "no-compositor"), + 2, + "Count of animation should be plus one to original count" + ); + + info("Checking the mutation for remove an animation"); + await setClassAttribute( + animationInspector, + ".compositor-notall", + "ball still" + ); + await waitUntil( + () => animationInspector.state.animations.length === originalAnimationCount + ); + ok( + true, + "Count of animation should be same to original count since we remove an animation" + ); + + info("Checking the mutation for modify an animation"); + await selectNode(".compositor-all", inspector); + await setStyle( + animationInspector, + ".compositor-all", + "animationDuration", + "100s" + ); + await setStyle( + animationInspector, + ".compositor-all", + "animationIterationCount", + 1 + ); + const summaryGraphPathEl = getSummaryGraphPathElement( + panel, + "compositor-all" + ); + await waitUntil(() => summaryGraphPathEl.viewBox.baseVal.width === 100000); + ok( + true, + "Width of summary graph path should be 100000 " + + "after modifing the duration and iteration count" + ); + await setStyle( + animationInspector, + ".compositor-all", + "animationDelay", + "100s" + ); + await waitUntil(() => summaryGraphPathEl.viewBox.baseVal.width === 200000); + ok( + true, + "Width of summary graph path should be 200000 after modifing the delay" + ); + ok( + summaryGraphPathEl.parentElement.querySelector(".animation-delay-sign"), + "Delay sign element shoud exist" + ); +}); + +function getAnimationNameCount(panel, animationName) { + return [...panel.querySelectorAll(".animation-name")].reduce( + (count, element) => + element.textContent === animationName ? count + 1 : count, + 0 + ); +} + +function getSummaryGraphPathElement(panel, animationName) { + for (const animationNameEl of panel.querySelectorAll(".animation-name")) { + if (animationNameEl.textContent === animationName) { + return animationNameEl + .closest(".animation-summary-graph") + .querySelector(".animation-summary-graph-path"); + } + } + + return null; +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js new file mode 100644 index 0000000000..c57f3c7b3b --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the animation inspector will not crash when add animation then remove +// immediately. + +add_task(async function () { + const tab = await addTab( + URL_ROOT + "doc_mutations_add_remove_immediately.html" + ); + const { inspector, panel } = await openAnimationInspector(); + + info("Check state of the animation inspector after fast mutations"); + const onDispatch = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS"); + await startMutation(tab); + await onDispatch; + ok( + panel.querySelector(".animation-error-message"), + "No animations message should display" + ); +}); + +async function startMutation(tab) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.startMutation(); + }); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js new file mode 100644 index 0000000000..516a150e42 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the animation inspector will not crash when remove/add animations faster. + +add_task(async function () { + const tab = await addTab(URL_ROOT + "doc_mutations_fast.html"); + const { inspector } = await openAnimationInspector(); + + info("Check state of the animation inspector after fast mutations"); + await startFastMutations(tab); + ok( + inspector.panelWin.document.getElementById("animation-container"), + "Animation inspector should be live" + ); +}); + +async function startFastMutations(tab) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.startFastMutations(); + }); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js new file mode 100644 index 0000000000..9ec3d58be9 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether animation was changed after altering following properties. +// * delay +// * direction +// * duration +// * easing (animationTimingFunction in case of CSS Animationns) +// * fill +// * iterations +// * endDelay (script animation only) +// * iterationStart (script animation only) +// * playbackRate (script animation only) + +const SEC = 1000; +const TEST_EFFECT_TIMING = { + delay: 20 * SEC, + direction: "reverse", + duration: 20 * SEC, + easing: "steps(1)", + endDelay: 20 * SEC, + fill: "backwards", + iterations: 20, + iterationStart: 20 * SEC, +}; +const TEST_PLAYBACK_RATE = 0.1; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".end-delay"]); + const { animationInspector } = await openAnimationInspector(); + await setCSSAnimationProperties(animationInspector); + await assertProperties(animationInspector.state.animations[0], false); + await setScriptAnimationProperties(animationInspector); + await assertProperties(animationInspector.state.animations[1], true); +}); + +async function setCSSAnimationProperties(animationInspector) { + const properties = { + animationDelay: `${TEST_EFFECT_TIMING.delay}ms`, + animationDirection: TEST_EFFECT_TIMING.direction, + animationDuration: `${TEST_EFFECT_TIMING.duration}ms`, + animationFillMode: TEST_EFFECT_TIMING.fill, + animationIterationCount: TEST_EFFECT_TIMING.iterations, + animationTimingFunction: TEST_EFFECT_TIMING.easing, + }; + + await setStyles(animationInspector, ".animated", properties); +} + +async function setScriptAnimationProperties(animationInspector) { + await setEffectTimingAndPlayback( + animationInspector, + ".end-delay", + TEST_EFFECT_TIMING, + TEST_PLAYBACK_RATE + ); +} + +async function assertProperties(animation, isScriptAnimation) { + await waitUntil(() => animation.state.delay === TEST_EFFECT_TIMING.delay); + ok(true, `Delay should be ${TEST_EFFECT_TIMING.delay}`); + + await waitUntil( + () => animation.state.direction === TEST_EFFECT_TIMING.direction + ); + ok(true, `Direction should be ${TEST_EFFECT_TIMING.direction}`); + + await waitUntil( + () => animation.state.duration === TEST_EFFECT_TIMING.duration + ); + ok(true, `Duration should be ${TEST_EFFECT_TIMING.duration}`); + + await waitUntil(() => animation.state.fill === TEST_EFFECT_TIMING.fill); + ok(true, `Fill should be ${TEST_EFFECT_TIMING.fill}`); + + await waitUntil( + () => animation.state.iterationCount === TEST_EFFECT_TIMING.iterations + ); + ok(true, `Iterations should be ${TEST_EFFECT_TIMING.iterations}`); + + if (isScriptAnimation) { + await waitUntil(() => animation.state.easing === TEST_EFFECT_TIMING.easing); + ok(true, `Easing should be ${TEST_EFFECT_TIMING.easing}`); + + await waitUntil( + () => animation.state.iterationStart === TEST_EFFECT_TIMING.iterationStart + ); + ok(true, `IterationStart should be ${TEST_EFFECT_TIMING.iterationStart}`); + + await waitUntil(() => animation.state.playbackRate === TEST_PLAYBACK_RATE); + ok(true, `PlaybackRate should be ${TEST_PLAYBACK_RATE}`); + } else { + await waitUntil( + () => + animation.state.animationTimingFunction === TEST_EFFECT_TIMING.easing + ); + + ok(true, `AnimationTimingFunction should be ${TEST_EFFECT_TIMING.easing}`); + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js b/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js new file mode 100644 index 0000000000..3d1c71b6c3 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that animations with an overflowed delay and end delay are not displayed. + +add_task(async function () { + await addTab(URL_ROOT + "doc_overflowed_delay_end_delay.html"); + const { panel } = await openAnimationInspector(); + + info("Check the number of animation item"); + const animationItemEls = panel.querySelectorAll( + ".animation-list .animation-item" + ); + is( + animationItemEls.length, + 1, + "The number of animations displayed should be 1" + ); + + info("Check the id of animation displayed"); + const animationNameEl = animationItemEls[0].querySelector(".animation-name"); + is( + animationNameEl.textContent, + "big-iteration-start", + "The animation name should be 'big-iteration-start'" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js b/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js new file mode 100644 index 0000000000..11835cd880 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the scroll amount of animation and animated property re-calculate after +// changing selected node. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([ + ".animated", + ".multi", + ".longhand", + ".negative-delay", + ]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info( + "Set the scroll amount of animation and animated property to the bottom" + ); + const onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + ".longhand" + ); + await onDetailRendered; + + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 5); + const bottomAnimationEl = await findAnimationItemByIndex(panel, 4); + const bottomAnimatedPropertyEl = panel.querySelector( + ".animated-property-item:last-child" + ); + bottomAnimationEl.scrollIntoView(false); + bottomAnimatedPropertyEl.scrollIntoView(false); + + info("Hold the scroll amount"); + const animationInspectionPanel = bottomAnimationEl.closest( + ".progress-inspection-panel" + ); + const animatedPropertyInspectionPanel = bottomAnimatedPropertyEl.closest( + ".progress-inspection-panel" + ); + const initialScrollTopOfAnimation = animationInspectionPanel.scrollTop; + const initialScrollTopOfAnimatedProperty = + animatedPropertyInspectionPanel.scrollTop; + + info( + "Check whether the scroll amount re-calculate after changing the count of items" + ); + await selectNode(".negative-delay", inspector); + await waitUntil( + () => + initialScrollTopOfAnimation > animationInspectionPanel.scrollTop && + initialScrollTopOfAnimatedProperty > + animatedPropertyInspectionPanel.scrollTop + ); + ok( + true, + "Scroll amount for animation list should be less than previous state" + ); + ok( + true, + "Scroll amount for animated property list should be less than previous state" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js new file mode 100644 index 0000000000..8a8d9f848b --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following PauseResumeButton component: +// * element existence +// * state during running animations +// * state during pausing animations +// * make animations to pause by push button +// * make animations to resume by push button + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking pause/resume button existence"); + const buttonEl = panel.querySelector(".pause-resume-button"); + ok(buttonEl, "pause/resume button should exist"); + + info("Checking state during running animations"); + ok( + !buttonEl.classList.contains("paused"), + "State of button should be running" + ); + + info("Checking button makes animations to pause"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + ok(true, "All of animtion are paused"); + ok(buttonEl.classList.contains("paused"), "State of button should be paused"); + + info("Checking button makes animations to resume"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "running"); + ok(true, "All of animtion are running"); + ok( + !buttonEl.classList.contains("paused"), + "State of button should be resumed" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js new file mode 100644 index 0000000000..896ae5d2ef --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the animation can rewind if the current time is over end time when +// the resume button clicked. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([ + ".animated", + ".end-delay", + ".long", + ".negative-delay", + ]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Check animations state after resuming with infinite animation"); + info("Make the current time of animation to be over its end time"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 1); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + info("Resume animations"); + clickOnPauseResumeButton(animationInspector, panel); + await wait(1000); + assertPlayState(animationInspector.state.animations, [ + "running", + "finished", + "finished", + "finished", + ]); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + + info("Check animations state after resuming without infinite animation"); + info("Remove infinite animation"); + await setClassAttribute(animationInspector, ".animated", "ball still"); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 3); + + info("Make the current time of animation to be over its end time"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 1.1); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await changePlaybackRateSelector(animationInspector, panel, 0.1); + info("Resume animations"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "running"); + assertCurrentTimeLessThanDuration(animationInspector.state.animations); + assertScrubberPosition(panel); +}); + +function assertPlayState(animations, expectedState) { + animations.forEach((animation, index) => { + is( + animation.state.playState, + expectedState[index], + `The playState of animation [${index}] should be ${expectedState[index]}` + ); + }); +} + +function assertCurrentTimeLessThanDuration(animations) { + animations.forEach((animation, index) => { + Assert.less( + animation.state.currentTime, + animation.state.duration, + `The current time of animation[${index}] should be less than its duration` + ); + }); +} + +function assertScrubberPosition(panel) { + const scrubberEl = panel.querySelector(".current-time-scrubber"); + const marginInlineStart = parseFloat(scrubberEl.style.marginInlineStart); + Assert.greaterOrEqual( + marginInlineStart, + 0, + "The translateX of scrubber position should be zero or more" + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js new file mode 100644 index 0000000000..ad84e4c257 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether pausing/resuming the each animations correctly. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".compositor-all"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + const buttonEl = panel.querySelector(".pause-resume-button"); + + info( + "Check '.compositor-all' animation is still running " + + "after even pausing '.animated' animation" + ); + await selectNode(".animated", inspector); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + ok(buttonEl.classList.contains("paused"), "State of button should be paused"); + await selectNode("body", inspector); + await assertStatus( + animationInspector.state.animations, + buttonEl, + ["paused", "running"], + false + ); + + info( + "Check both animations are paused after clicking pause/resume " + + "while displaying both animations" + ); + clickOnPauseResumeButton(animationInspector, panel); + await assertStatus( + animationInspector.state.animations, + buttonEl, + ["paused", "paused"], + true + ); + + info( + "Check '.animated' animation is still paused " + + "after even resuming '.compositor-all' animation" + ); + await selectNode(".compositor-all", inspector); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntil(() => + animationInspector.state.animations.some( + a => a.state.playState === "running" + ) + ); + ok( + !buttonEl.classList.contains("paused"), + "State of button should be running" + ); + await selectNode("body", inspector); + await assertStatus( + animationInspector.state.animations, + buttonEl, + ["paused", "running"], + false + ); +}); + +async function assertStatus( + animations, + buttonEl, + expectedAnimationStates, + shouldButtonPaused +) { + await waitUntil(() => { + for (let i = 0; i < expectedAnimationStates.length; i++) { + const animation = animations[i]; + const state = expectedAnimationStates[i]; + if (animation.state.playState !== state) { + return false; + } + } + return true; + }); + expectedAnimationStates.forEach((state, index) => { + is( + animations[index].state.playState, + state, + `Animation ${index} should be ${state}` + ); + }); + + is( + buttonEl.classList.contains("paused"), + shouldButtonPaused, + "State of button is correct" + ); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js new file mode 100644 index 0000000000..7a27b1bd07 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following PauseResumeButton component with spacebar: +// * make animations to pause/resume by spacebar +// * combination with other UI components + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking spacebar makes animations to pause"); + await testPauseAndResumeBySpacebar(animationInspector, panel); + + info( + "Checking spacebar makes animations to pause when the button has the focus" + ); + const pauseResumeButton = panel.querySelector(".pause-resume-button"); + await testPauseAndResumeBySpacebar(animationInspector, pauseResumeButton); + + info("Checking spacebar works with other UI components"); + // To pause + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + // To resume + sendSpaceKeyEvent(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "running"); + // To pause + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + // To resume + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "running"); + // To pause + sendSpaceKeyEvent(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + ok(true, "All components that can make animations pause/resume works fine"); +}); + +async function testPauseAndResumeBySpacebar(animationInspector, element) { + await sendSpaceKeyEvent(animationInspector, element); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + ok(true, "Space key can pause animations"); + await sendSpaceKeyEvent(animationInspector, element); + await waitUntilAnimationsPlayState(animationInspector, "running"); + ok(true, "Space key can resume animations"); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js b/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js new file mode 100644 index 0000000000..8552eae138 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following PlaybackRateSelector component: +// * element existence +// * make playback rate of animations by the selector +// * in case of animations have mixed playback rate +// * in case of animations have playback rate which is not default selectable value + +add_task(async function () { + await addTab(URL_ROOT + "doc_custom_playback_rate.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking playback rate selector existence"); + const selectEl = panel.querySelector(".playback-rate-selector"); + ok(selectEl, "scrubber controller should exist"); + + info( + "Checking playback rate existence which includes custom rate of animations" + ); + const expectedPlaybackRates = [0.1, 0.25, 0.5, 1, 1.5, 2, 5, 10]; + await assertPlaybackRateOptions(selectEl, expectedPlaybackRates); + + info("Checking selected playback rate"); + is(Number(selectEl.value), 1.5, "Selected option should be 1.5"); + + info("Checking playback rate of animations"); + await changePlaybackRateSelector(animationInspector, panel, 0.5); + await assertPlaybackRate(animationInspector, 0.5); + + info("Checking mixed playback rate"); + await selectNode("div", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1); + await changePlaybackRateSelector(animationInspector, panel, 2); + await assertPlaybackRate(animationInspector, 2); + await selectNode("body", inspector); + await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2); + await waitUntil(() => selectEl.value === ""); + ok(true, "Selected option should be empty"); + + info("Checking playback rate after re-setting"); + await changePlaybackRateSelector(animationInspector, panel, 1); + await assertPlaybackRate(animationInspector, 1); + + info( + "Checking whether custom playback rate exist " + + "after selecting another playback rate" + ); + await assertPlaybackRateOptions(selectEl, expectedPlaybackRates); +}); + +async function assertPlaybackRate(animationInspector, rate) { + await waitUntil(() => + animationInspector.state?.animations.every( + ({ state }) => state.playbackRate === rate + ) + ); + ok(true, `Playback rate of animations should be ${rate}`); +} + +async function assertPlaybackRateOptions(selectEl, expectedPlaybackRates) { + await waitUntil(() => { + if (selectEl.options.length !== expectedPlaybackRates.length) { + return false; + } + + for (let i = 0; i < selectEl.options.length; i++) { + const optionEl = selectEl.options[i]; + const expectedPlaybackRate = expectedPlaybackRates[i]; + if (Number(optionEl.value) !== expectedPlaybackRate) { + return false; + } + } + + return true; + }); + ok(true, "Content of playback rate options are correct"); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js b/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js new file mode 100644 index 0000000000..00e267f9f8 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for pseudo element. + +const TEST_DATA = [ + { + expectedTargetLabel: "::before", + expectedAnimationNameLabel: "body", + expectedKeyframsGraphPathSegments: [ + { x: 0, y: 0 }, + { x: 1000, y: 100 }, + ], + }, + { + expectedTargetLabel: "::before", + expectedAnimationNameLabel: "div-before", + expectedKeyframsGraphPathSegments: [ + { x: 0, y: 100 }, + { x: 1000, y: 0 }, + ], + }, + { + expectedTargetLabel: "::after", + expectedAnimationNameLabel: "div-after", + }, + { + expectedTargetLabel: "::marker", + expectedAnimationNameLabel: "div-marker", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_pseudo.html"); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking count of animation item for pseudo elements"); + is( + panel.querySelectorAll(".animation-list .animation-item").length, + TEST_DATA.length, + `Count of animation item should be ${TEST_DATA.length}` + ); + + info("Checking content of each animation item"); + for (let i = 0; i < TEST_DATA.length; i++) { + const testData = TEST_DATA[i]; + info(`Checking pseudo element for ${testData.expectedTargetLabel}`); + const animationItemEl = await findAnimationItemByIndex(panel, i); + + info("Checking text content of animation target"); + const animationTargetEl = animationItemEl.querySelector( + ".animation-list .animation-item .animation-target" + ); + is( + animationTargetEl.textContent, + testData.expectedTargetLabel, + `Text content of animation target[${i}] should be ${testData.expectedTarget}` + ); + + info("Checking text content of animation name"); + const animationNameEl = animationItemEl.querySelector(".animation-name"); + is( + animationNameEl.textContent, + testData.expectedAnimationNameLabel, + `The animation name should be ${testData.expectedAnimationNameLabel}` + ); + } + + info( + "Checking whether node is selected correctly " + + "when click on the first inspector icon on Reps component" + ); + let onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await clickOnTargetNode(animationInspector, panel, 0); + await onDetailRendered; + assertAnimationCount(panel, 1); + assertAnimationNameLabel(panel, TEST_DATA[0].expectedAnimationNameLabel); + assertKeyframesGraphPathSegments( + panel, + TEST_DATA[0].expectedKeyframsGraphPathSegments + ); + + info("Select <body> again to reset the animation list"); + await selectNode("body", inspector); + + info( + "Checking whether node is selected correctly " + + "when click on the second inspector icon on Reps component" + ); + onDetailRendered = animationInspector.once("animation-keyframes-rendered"); + await clickOnTargetNode(animationInspector, panel, 1); + await onDetailRendered; + assertAnimationCount(panel, 1); + assertAnimationNameLabel(panel, TEST_DATA[1].expectedAnimationNameLabel); + assertKeyframesGraphPathSegments( + panel, + TEST_DATA[1].expectedKeyframsGraphPathSegments + ); +}); + +function assertAnimationCount(panel, expectedCount) { + info("Checking count of animation item"); + is( + panel.querySelectorAll(".animation-list .animation-item").length, + expectedCount, + `Count of animation item should be ${expectedCount}` + ); +} + +function assertAnimationNameLabel(panel, expectedAnimationNameLabel) { + info("Checking the animation name label"); + is( + panel.querySelector(".animation-list .animation-item .animation-name") + .textContent, + expectedAnimationNameLabel, + `The animation name should be ${expectedAnimationNameLabel}` + ); +} + +function assertKeyframesGraphPathSegments(panel, expectedPathSegments) { + info("Checking the keyframes graph path segments"); + const pathEl = panel.querySelector(".keyframes-graph-path path"); + assertPathSegments(pathEl, true, expectedPathSegments); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_rewind-button.js b/devtools/client/inspector/animation/test/browser_animation_rewind-button.js new file mode 100644 index 0000000000..f74518095b --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_rewind-button.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test for following RewindButton component: +// * element existence +// * make animations to rewind to zero +// * the state should be always paused after rewinding + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept([".delay-negative", ".delay-positive"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking button existence"); + ok(panel.querySelector(".rewind-button"), "Rewind button should exist"); + + info("Checking rewind button makes animations to rewind to zero"); + clickOnRewindButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await waitUntilCurrentTimeChangedAt(animationInspector, 0); + ok(true, "Rewind button make current time 0"); + + info("Checking rewind button makes animations after clicking scrubber"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + clickOnRewindButton(animationInspector, panel); + await waitUntilCurrentTimeChangedAt(animationInspector, 0); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + ok(true, "Rewind button make current time 0 even after clicking scrubber"); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_short-duration.js b/devtools/client/inspector/animation/test/browser_animation_short-duration.js new file mode 100644 index 0000000000..c953d886ff --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_short-duration.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test tooltips and iteration path of summary graph with short duration animation. + +add_task(async function () { + await addTab(URL_ROOT + "doc_short_duration.html"); + const { panel } = await openAnimationInspector(); + + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + ".short" + ); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + + info("Check tooltip"); + assertTooltip(summaryGraphEl); + + info("Check iteration path"); + assertIterationPath(summaryGraphEl); +}); + +function assertTooltip(summaryGraphEl) { + const tooltip = summaryGraphEl.getAttribute("title"); + const expected = "Duration: 0s"; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); +} + +function assertIterationPath(summaryGraphEl) { + const pathEl = summaryGraphEl.querySelector( + ".animation-computed-timing-path .animation-iteration-path" + ); + const expected = [ + { x: 0, y: 0 }, + { x: 0.999, y: 99.9 }, + { x: 1, y: 0 }, + ]; + assertPathSegments(pathEl, true, expected); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js new file mode 100644 index 0000000000..0e9c52449d --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following AnimationName component works. +// * element existance +// * name text + +const TEST_DATA = [ + { + targetClass: "cssanimation-normal", + expectedLabel: "cssanimation", + }, + { + targetClass: "cssanimation-linear", + expectedLabel: "cssanimation", + }, + { + targetClass: "delay-positive", + expectedLabel: "test-delay-animation", + }, + { + targetClass: "delay-negative", + expectedLabel: "test-negative-delay-animation", + }, + { + targetClass: "easing-step", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedLabel } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking animation name element existance for ${targetClass}`); + const animationNameEl = animationItemEl.querySelector(".animation-name"); + + if (expectedLabel) { + ok( + animationNameEl, + "The animation name element should be in animation item element" + ); + is( + animationNameEl.textContent, + expectedLabel, + `The animation name should be ${expectedLabel}` + ); + } else { + ok( + !animationNameEl, + "The animation name element should not be in animation item element" + ); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js new file mode 100644 index 0000000000..186c54cba6 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that when animations displayed in the timeline are running on the +// compositor, they get a special icon and information in the tooltip. + +requestLongerTimeout(2); + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([ + ".compositor-all", + ".compositor-notall", + ".no-compositor", + ]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Check animation whose all properties are running on compositor"); + const summaryGraphAllEl = await findSummaryGraph(".compositor-all", panel); + ok( + summaryGraphAllEl.classList.contains("compositor"), + "The element has the compositor css class" + ); + ok( + hasTooltip( + summaryGraphAllEl, + ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip") + ), + "The element has the right tooltip content" + ); + + info("Check animation is not running on compositor"); + const summaryGraphNoEl = await findSummaryGraph(".no-compositor", panel); + ok( + !summaryGraphNoEl.classList.contains("compositor"), + "The element does not have the compositor css class" + ); + ok( + !hasTooltip( + summaryGraphNoEl, + ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip") + ), + "The element does not have oncompositor tooltip content" + ); + ok( + !hasTooltip( + summaryGraphNoEl, + ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip") + ), + "The element does not have oncompositor tooltip content" + ); + + info( + "Select a node has animation whose some properties are running on compositor" + ); + await selectNode(".compositor-notall", inspector); + const summaryGraphEl = await findSummaryGraph(".compositor-notall", panel); + ok( + summaryGraphEl.classList.contains("compositor"), + "The element has the compositor css class" + ); + ok( + hasTooltip( + summaryGraphEl, + ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip") + ), + "The element has the right tooltip content" + ); + + info("Check compositor sign after pausing"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntil(() => !summaryGraphEl.classList.contains("compositor")); + ok( + true, + "The element should not have the compositor css class after pausing" + ); + + info("Check compositor sign after resuming"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntil(() => summaryGraphEl.classList.contains("compositor")); + ok(true, "The element should have the compositor css class after resuming"); + + info("Check compositor sign after rewind"); + clickOnRewindButton(animationInspector, panel); + await waitUntil(() => !summaryGraphEl.classList.contains("compositor")); + ok( + true, + "The element should not have the compositor css class after rewinding" + ); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntil(() => summaryGraphEl.classList.contains("compositor")); + ok(true, "The element should have the compositor css class after resuming"); + + info("Check compositor sign after setting the current time"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntil(() => !summaryGraphEl.classList.contains("compositor")); + ok( + true, + "The element should not have the compositor css class " + + "after setting the current time" + ); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntil(() => summaryGraphEl.classList.contains("compositor")); + ok(true, "The element should have the compositor css class after resuming"); +}); + +async function findSummaryGraph(selector, panel) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + selector + ); + return animationItemEl.querySelector(".animation-summary-graph"); +} + +function hasTooltip(summaryGraphEl, expected) { + const tooltip = summaryGraphEl.getAttribute("title"); + return tooltip.includes(expected); +} diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js new file mode 100644 index 0000000000..a81b971559 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js @@ -0,0 +1,208 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following ComputedTimingPath component works. +// * element existance +// * iterations: path, count +// * delay: path +// * fill: path +// * endDelay: path + +/* import-globals-from summary-graph_computed-timing-path_head.js */ +Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_computed-timing-path_head.js", + this +); + +const TEST_DATA = [ + { + targetClass: "cssanimation-normal", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 40.851 }, + { x: 500000, y: 80.24 }, + { x: 750000, y: 96.05 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "cssanimation-linear", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "delay-positive", + expectedDelayPath: [ + { x: 0, y: 0 }, + { x: 500000, y: 0 }, + ], + expectedIterationPathList: [ + [ + { x: 500000, y: 0 }, + { x: 750000, y: 25 }, + { x: 1000000, y: 50 }, + { x: 1250000, y: 75 }, + { x: 1500000, y: 100 }, + { x: 1500000, y: 0 }, + ], + ], + }, + { + targetClass: "easing-step", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 499999, y: 0 }, + { x: 500000, y: 50 }, + { x: 999999, y: 50 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "enddelay-positive", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + expectedEndDelayPath: [ + { x: 1000000, y: 0 }, + { x: 1500000, y: 0 }, + ], + }, + { + targetClass: "enddelay-negative", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 500000, y: 0 }, + ], + ], + }, + { + targetClass: "enddelay-with-fill-forwards", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + expectedEndDelayPath: [ + { x: 1000000, y: 0 }, + { x: 1000000, y: 100 }, + { x: 1500000, y: 100 }, + { x: 1500000, y: 0 }, + ], + expectedForwardsPath: [ + { x: 1500000, y: 0 }, + { x: 1500000, y: 100 }, + ], + }, + { + targetClass: "enddelay-with-iterations-infinity", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1250000, y: 25 }, + { x: 1500000, y: 50 }, + ], + ], + isInfinity: true, + }, + { + targetClass: "direction-alternate-with-iterations-infinity", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1000000, y: 100 }, + { x: 1250000, y: 75 }, + { x: 1500000, y: 50 }, + ], + ], + isInfinity: true, + }, + { + targetClass: "direction-alternate-reverse-with-iterations-infinity", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 250000, y: 75 }, + { x: 500000, y: 50 }, + { x: 750000, y: 25 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1250000, y: 25 }, + { x: 1500000, y: 50 }, + ], + ], + isInfinity: true, + }, + { + targetClass: "direction-reverse-with-iterations-infinity", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + { x: 250000, y: 75 }, + { x: 500000, y: 50 }, + { x: 750000, y: 25 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1000000, y: 100 }, + { x: 1250000, y: 75 }, + { x: 1500000, y: 50 }, + ], + ], + isInfinity: true, + }, +]; + +add_task(async function () { + await testComputedTimingPath(TEST_DATA); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js new file mode 100644 index 0000000000..e1e4c52ba6 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js @@ -0,0 +1,192 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following ComputedTimingPath component works. +// * element existance +// * iterations: path, count +// * delay: path +// * fill: path +// * endDelay: path + +/* import-globals-from summary-graph_computed-timing-path_head.js */ +Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_computed-timing-path_head.js", + this +); + +const TEST_DATA = [ + { + targetClass: "fill-backwards", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "fill-backwards-with-delay-iterationstart", + expectedDelayPath: [ + { x: 0, y: 0 }, + { x: 0, y: 50 }, + { x: 500000, y: 50 }, + { x: 500000, y: 0 }, + ], + expectedIterationPathList: [ + [ + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1250000, y: 25 }, + { x: 1500000, y: 50 }, + { x: 1500000, y: 0 }, + ], + ], + }, + { + targetClass: "fill-both", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + expectedForwardsPath: [ + { x: 1000000, y: 0 }, + { x: 1000000, y: 100 }, + { x: 1500000, y: 100 }, + { x: 1500000, y: 0 }, + ], + }, + { + targetClass: "fill-both-width-delay-iterationstart", + expectedDelayPath: [ + { x: 0, y: 0 }, + { x: 0, y: 50 }, + { x: 500000, y: 50 }, + { x: 500000, y: 0 }, + ], + expectedIterationPathList: [ + [ + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + [ + { x: 1000000, y: 0 }, + { x: 1250000, y: 25 }, + { x: 1500000, y: 50 }, + { x: 1500000, y: 0 }, + ], + ], + expectedForwardsPath: [ + { x: 1500000, y: 0 }, + { x: 1500000, y: 50 }, + ], + }, + { + targetClass: "fill-forwards", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + expectedForwardsPath: [ + { x: 1000000, y: 0 }, + { x: 1000000, y: 100 }, + { x: 1500000, y: 100 }, + { x: 1500000, y: 0 }, + ], + }, + { + targetClass: "iterationstart", + expectedIterationPathList: [ + [ + { x: 0, y: 50 }, + { x: 250000, y: 75 }, + { x: 500000, y: 100 }, + { x: 500000, y: 0 }, + ], + [ + { x: 500000, y: 0 }, + { x: 750000, y: 25 }, + { x: 1000000, y: 50 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "no-compositor", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "keyframes-easing-step", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 499999, y: 0 }, + { x: 500000, y: 50 }, + { x: 999999, y: 50 }, + { x: 1000000, y: 0 }, + ], + ], + }, + { + targetClass: "narrow-keyframes", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 100000, y: 10 }, + { x: 110000, y: 10 }, + { x: 115000, y: 10 }, + { x: 129999, y: 10 }, + { x: 130000, y: 13 }, + { x: 135000, y: 13.5 }, + ], + ], + }, + { + targetClass: "duplicate-offsets", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 250000, y: 25 }, + { x: 500000, y: 50 }, + { x: 999999, y: 50 }, + ], + ], + }, +]; + +add_task(async function () { + await testComputedTimingPath(TEST_DATA); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js new file mode 100644 index 0000000000..0b9bc79def --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Computed Timing Path component for different time scales. + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".animated", ".end-delay"]); + const { animationInspector, inspector, panel } = + await openAnimationInspector(); + + info("Checking the path for different time scale"); + let onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await selectNode(".animated", inspector); + await onDetailRendered; + const itemA = await findAnimationItemByTargetSelector(panel, ".animated"); + const pathStringA = itemA + .querySelector(".animation-iteration-path") + .getAttribute("d"); + + info("Select animation which has different time scale from no-compositor"); + onDetailRendered = animationInspector.once("animation-keyframes-rendered"); + await selectNode(".end-delay", inspector); + await onDetailRendered; + + info("Select no-compositor again"); + onDetailRendered = animationInspector.once("animation-keyframes-rendered"); + await selectNode(".animated", inspector); + await onDetailRendered; + const itemB = await findAnimationItemByTargetSelector(panel, ".animated"); + const pathStringB = itemB + .querySelector(".animation-iteration-path") + .getAttribute("d"); + is( + pathStringA, + pathStringB, + "Path string should be same even change the time scale" + ); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js new file mode 100644 index 0000000000..591fc5f3fe --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_delay-sign_head.js", + this + ); + await pushPref("intl.l10n.pseudo", "bidi"); + // eslint-disable-next-line no-undef + await testSummaryGraphDelaySign(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js new file mode 100644 index 0000000000..891d9fd90e --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_delay-sign_head.js", + this + ); + // eslint-disable-next-line no-undef + await testSummaryGraphDelaySign(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js new file mode 100644 index 0000000000..6974eab6c6 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following EffectTimingPath component works. +// * element existance +// * path + +const TEST_DATA = [ + { + targetClass: "cssanimation-linear", + }, + { + targetClass: "delay-negative", + }, + { + targetClass: "easing-step", + expectedPath: [ + { x: 0, y: 0 }, + { x: 499999, y: 0 }, + { x: 500000, y: 50 }, + { x: 999999, y: 50 }, + { x: 1000000, y: 0 }, + ], + }, + { + targetClass: "keyframes-easing-step", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedPath } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking effect timing path existance for ${targetClass}`); + const effectTimingPathEl = animationItemEl.querySelector( + ".animation-effect-timing-path" + ); + + if (expectedPath) { + ok( + effectTimingPathEl, + "The effect timing path element should be in animation item element" + ); + const pathEl = effectTimingPathEl.querySelector( + ".animation-iteration-path" + ); + assertPathSegments(pathEl, false, expectedPath); + } else { + ok( + !effectTimingPathEl, + "The effect timing path element should not be in animation item element" + ); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js new file mode 100644 index 0000000000..084e4acf1d --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from summary-graph_end-delay-sign_head.js */ + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_end-delay-sign_head.js", + this + ); + await pushPref("intl.l10n.pseudo", "bidi"); + await testSummaryGraphEndDelaySign(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js new file mode 100644 index 0000000000..4382ed4c2d --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.scriptloader.loadSubScript( + CHROME_URL_ROOT + "summary-graph_end-delay-sign_head.js", + this + ); + // eslint-disable-next-line no-undef + await testSummaryGraphEndDelaySign(); +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js new file mode 100644 index 0000000000..5f1c808728 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the layout of graphs were broken by seek and resume. + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept([ + ".delay-positive", + ".delay-negative", + ".enddelay-positive", + ".enddelay-negative", + ]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Get initial coordinates result as test data"); + const initialCoordinatesResult = []; + + for (let i = 0; i < animationInspector.state.animations.length; i++) { + const itemEl = await findAnimationItemByIndex(panel, i); + const svgEl = itemEl.querySelector("svg"); + const svgViewBoxX = svgEl.viewBox.baseVal.x; + const svgViewBoxWidth = svgEl.viewBox.baseVal.width; + + const pathEl = svgEl.querySelector(".animation-computed-timing-path"); + const pathX = pathEl.transform.baseVal[0].matrix.e; + + const delayEl = itemEl.querySelector(".animation-delay-sign"); + let delayX = null; + let delayWidth = null; + + if (delayEl) { + const computedStyle = delayEl.ownerGlobal.getComputedStyle(delayEl); + delayX = computedStyle.left; + delayWidth = computedStyle.width; + } + + const endDelayEl = itemEl.querySelector(".animation-end-delay-sign"); + let endDelayX = null; + let endDelayWidth = null; + + if (endDelayEl) { + const computedStyle = endDelayEl.ownerGlobal.getComputedStyle(endDelayEl); + endDelayX = computedStyle.left; + endDelayWidth = computedStyle.width; + } + + const coordinates = { + svgViewBoxX, + svgViewBoxWidth, + pathX, + delayX, + delayWidth, + endDelayX, + endDelayWidth, + }; + initialCoordinatesResult.push(coordinates); + } + + info("Set currentTime to rear of the end of animation of .delay-negative."); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.75); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + info("Resume animations"); + clickOnPauseResumeButton(animationInspector, panel); + // As some animations may be finished, we check if some animations will be running. + await waitUntil(() => + animationInspector.state.animations.some( + a => a.state.playState === "running" + ) + ); + + info("Check the layout"); + const itemEls = panel.querySelectorAll(".animation-item"); + is( + itemEls.length, + initialCoordinatesResult.length, + "Count of animation item should be same to initial items" + ); + + info("Check the coordinates"); + checkExpectedCoordinates(itemEls, initialCoordinatesResult); +}); + +function checkExpectedCoordinates(itemEls, initialCoordinatesResult) { + for (let i = 0; i < itemEls.length; i++) { + const expectedCoordinates = initialCoordinatesResult[i]; + const itemEl = itemEls[i]; + const svgEl = itemEl.querySelector("svg"); + is( + svgEl.viewBox.baseVal.x, + expectedCoordinates.svgViewBoxX, + "X of viewBox of svg should be same" + ); + is( + svgEl.viewBox.baseVal.width, + expectedCoordinates.svgViewBoxWidth, + "Width of viewBox of svg should be same" + ); + + const pathEl = svgEl.querySelector(".animation-computed-timing-path"); + is( + pathEl.transform.baseVal[0].matrix.e, + expectedCoordinates.pathX, + "X of tansform of path element should be same" + ); + + const delayEl = itemEl.querySelector(".animation-delay-sign"); + + if (delayEl) { + const computedStyle = delayEl.ownerGlobal.getComputedStyle(delayEl); + is( + computedStyle.left, + expectedCoordinates.delayX, + "X of delay sign should be same" + ); + is( + computedStyle.width, + expectedCoordinates.delayWidth, + "Width of delay sign should be same" + ); + } else { + ok(!expectedCoordinates.delayX, "X of delay sign should exist"); + ok(!expectedCoordinates.delayWidth, "Width of delay sign should exist"); + } + + const endDelayEl = itemEl.querySelector(".animation-end-delay-sign"); + + if (endDelayEl) { + const computedStyle = endDelayEl.ownerGlobal.getComputedStyle(endDelayEl); + is( + computedStyle.left, + expectedCoordinates.endDelayX, + "X of endDelay sign should be same" + ); + is( + computedStyle.width, + expectedCoordinates.endDelayWidth, + "Width of endDelay sign should be same" + ); + } else { + ok(!expectedCoordinates.endDelayX, "X of endDelay sign should exist"); + ok( + !expectedCoordinates.endDelayWidth, + "Width of endDelay sign should exist" + ); + } + } +} diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js new file mode 100644 index 0000000000..8ed638c443 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following NegativeDelayPath component works. +// * element existance +// * path + +const TEST_DATA = [ + { + targetClass: "delay-positive", + }, + { + targetClass: "delay-negative", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 0, y: 50 }, + { x: 250000, y: 75 }, + { x: 500000, y: 100 }, + { x: 500000, y: 0 }, + ], + ], + expectedNegativePath: [ + { x: -500000, y: 0 }, + { x: -250000, y: 25 }, + { x: 0, y: 50 }, + { x: 0, y: 0 }, + ], + }, + { + targetClass: "delay-negative-25", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 0, y: 25 }, + { x: 750000, y: 100 }, + { x: 750000, y: 0 }, + ], + ], + expectedNegativePath: [ + { x: -250000, y: 0 }, + { x: 0, y: 25 }, + { x: 0, y: 0 }, + ], + }, + { + targetClass: "delay-negative-75", + expectedIterationPathList: [ + [ + { x: 0, y: 0 }, + { x: 0, y: 75 }, + { x: 250000, y: 100 }, + { x: 250000, y: 0 }, + ], + ], + expectedNegativePath: [ + { x: -750000, y: 0 }, + { x: 0, y: 75 }, + { x: 0, y: 0 }, + ], + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { + targetClass, + expectedIterationPathList, + expectedNegativePath, + } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking negative delay path existence for ${targetClass}`); + const negativeDelayPathEl = animationItemEl.querySelector( + ".animation-negative-delay-path" + ); + + if (expectedNegativePath) { + ok( + negativeDelayPathEl, + "The negative delay path element should be in animation item element" + ); + const pathEl = negativeDelayPathEl.querySelector("path"); + assertPathSegments(pathEl, true, expectedNegativePath); + } else { + ok( + !negativeDelayPathEl, + "The negative delay path element should not be in animation item element" + ); + } + + if (!expectedIterationPathList) { + // We don't need to test for iteration path. + continue; + } + + info(`Checking computed timing path existance for ${targetClass}`); + const computedTimingPathEl = animationItemEl.querySelector( + ".animation-computed-timing-path" + ); + ok( + computedTimingPathEl, + "The computed timing path element should be in each animation item element" + ); + + info(`Checking iteration path list for ${targetClass}`); + const iterationPathEls = computedTimingPathEl.querySelectorAll( + ".animation-iteration-path" + ); + is( + iterationPathEls.length, + expectedIterationPathList.length, + `Number of iteration path should be ${expectedIterationPathList.length}` + ); + + for (const [j, iterationPathEl] of iterationPathEls.entries()) { + assertPathSegments(iterationPathEl, true, expectedIterationPathList[j]); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js new file mode 100644 index 0000000000..69ce5007b5 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following NegativeEndDelayPath component works. +// * element existance +// * path + +const TEST_DATA = [ + { + targetClass: "enddelay-positive", + }, + { + targetClass: "enddelay-negative", + expectedPath: [ + { x: 500000, y: 0 }, + { x: 500000, y: 50 }, + { x: 750000, y: 75 }, + { x: 1000000, y: 100 }, + { x: 1000000, y: 0 }, + ], + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedPath } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking negative endDelay path existance for ${targetClass}`); + const negativeEndDelayPathEl = animationItemEl.querySelector( + ".animation-negative-end-delay-path" + ); + + if (expectedPath) { + ok( + negativeEndDelayPathEl, + "The negative endDelay path element should be in animation item element" + ); + const pathEl = negativeEndDelayPathEl.querySelector("path"); + assertPathSegments(pathEl, true, expectedPath); + } else { + ok( + !negativeEndDelayPathEl, + "The negative endDelay path element should not be in animation item element" + ); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js new file mode 100644 index 0000000000..1be3c92f4f --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js @@ -0,0 +1,294 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for existance and content of tooltip on summary graph element. + +const TEST_DATA = [ + { + targetClass: "cssanimation-normal", + expectedResult: { + nameAndType: "cssanimation — CSS Animation", + duration: "1,000s", + }, + }, + { + targetClass: "cssanimation-linear", + expectedResult: { + nameAndType: "cssanimation — CSS Animation", + duration: "1,000s", + animationTimingFunction: "linear", + }, + }, + { + targetClass: "delay-positive", + expectedResult: { + nameAndType: "test-delay-animation — Script Animation", + delay: "500s", + duration: "1,000s", + }, + }, + { + targetClass: "delay-negative", + expectedResult: { + nameAndType: "test-negative-delay-animation — Script Animation", + delay: "-500s", + duration: "1,000s", + }, + }, + { + targetClass: "easing-step", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + easing: "steps(2)", + }, + }, + { + targetClass: "enddelay-positive", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + endDelay: "500s", + }, + }, + { + targetClass: "enddelay-negative", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + endDelay: "-500s", + }, + }, + { + targetClass: "enddelay-with-fill-forwards", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + endDelay: "500s", + fill: "forwards", + }, + }, + { + targetClass: "enddelay-with-iterations-infinity", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + endDelay: "500s", + iterations: "\u221E", + }, + }, + { + targetClass: "direction-alternate-with-iterations-infinity", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + direction: "alternate", + iterations: "\u221E", + }, + }, + { + targetClass: "direction-alternate-reverse-with-iterations-infinity", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + direction: "alternate-reverse", + iterations: "\u221E", + }, + }, + { + targetClass: "direction-reverse-with-iterations-infinity", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + direction: "reverse", + iterations: "\u221E", + }, + }, + { + targetClass: "fill-backwards", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + fill: "backwards", + }, + }, + { + targetClass: "fill-backwards-with-delay-iterationstart", + expectedResult: { + nameAndType: "Script Animation", + delay: "500s", + duration: "1,000s", + fill: "backwards", + iterationStart: "0.5", + }, + }, + { + targetClass: "fill-both", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + fill: "both", + }, + }, + { + targetClass: "fill-both-width-delay-iterationstart", + expectedResult: { + nameAndType: "Script Animation", + delay: "500s", + duration: "1,000s", + fill: "both", + iterationStart: "0.5", + }, + }, + { + targetClass: "fill-forwards", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + fill: "forwards", + }, + }, + { + targetClass: "iterationstart", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + iterationStart: "0.5", + }, + }, + { + targetClass: "no-compositor", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + }, + }, + { + targetClass: "keyframes-easing-step", + expectedResult: { + nameAndType: "Script Animation", + duration: "1,000s", + }, + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedResult } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + + info(`Checking tooltip for ${targetClass}`); + ok( + summaryGraphEl.hasAttribute("title"), + "Summary graph should have 'title' attribute" + ); + + const tooltip = summaryGraphEl.getAttribute("title"); + const { + animationTimingFunction, + delay, + easing, + endDelay, + direction, + duration, + fill, + iterations, + iterationStart, + nameAndType, + } = expectedResult; + + ok( + tooltip.startsWith(nameAndType), + "Tooltip should start with name and type" + ); + + if (animationTimingFunction) { + const expected = `Animation timing function: ${animationTimingFunction}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok( + !tooltip.includes("Animation timing function:"), + "Tooltip should not include animation timing function" + ); + } + + if (delay) { + const expected = `Delay: ${delay}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok(!tooltip.includes("Delay:"), "Tooltip should not include delay"); + } + + if (direction) { + const expected = `Direction: ${direction}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok(!tooltip.includes("Direction:"), "Tooltip should not include delay"); + } + + if (duration) { + const expected = `Duration: ${duration}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok(!tooltip.includes("Duration:"), "Tooltip should not include delay"); + } + + if (easing) { + const expected = `Overall easing: ${easing}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok( + !tooltip.includes("Overall easing:"), + "Tooltip should not include easing" + ); + } + + if (endDelay) { + const expected = `End delay: ${endDelay}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok( + !tooltip.includes("End delay:"), + "Tooltip should not include endDelay" + ); + } + + if (fill) { + const expected = `Fill: ${fill}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok(!tooltip.includes("Fill:"), "Tooltip should not include fill"); + } + + if (iterations) { + const expected = `Repeats: ${iterations}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok( + !tooltip.includes("Repeats:"), + "Tooltip should not include iterations" + ); + } + + if (iterationStart) { + const expected = `Iteration start: ${iterationStart}`; + ok(tooltip.includes(expected), `Tooltip should include '${expected}'`); + } else { + ok( + !tooltip.includes("Iteration start:"), + "Tooltip should not include iterationStart" + ); + } + } +}); diff --git a/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js new file mode 100644 index 0000000000..e5f05d4f18 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the scrubber was working in case of negative playback rate. + +add_task(async function () { + await addTab(URL_ROOT + "doc_negative_playback_rate.html"); + await removeAnimatedElementsExcept([".normal"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Set initial state"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + const initialCurrentTime = + animationInspector.state.animations[0].state.currentTime; + const initialProgressBarX = getProgressBarX(panel); + + info("Check whether the animation currentTime was decreased"); + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilCurrentTimeChangedAt( + animationInspector, + animationInspector.state.timeScale.getDuration() * 0.5 + ); + Assert.greater( + initialCurrentTime, + animationInspector.state.animations[0].state.currentTime, + "currentTime should be decreased" + ); + + info("Check whether the progress bar was moved to left"); + Assert.greater( + initialProgressBarX, + getProgressBarX(panel), + "Progress bar should be moved to left" + ); +}); + +function getProgressBarX(panel) { + const areaEl = panel.querySelector(".keyframes-progress-bar-area"); + const barEl = areaEl.querySelector(".keyframes-progress-bar"); + const controllerBounds = areaEl.getBoundingClientRect(); + const barBounds = barEl.getBoundingClientRect(); + const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x; + return barX; +} diff --git a/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js new file mode 100644 index 0000000000..ef326f5eb2 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for following summary graph with the animation which is negative playback rate. +// * Tooltips +// * Graph path +// * Delay sign +// * End delay sign + +const TEST_DATA = [ + { + targetSelector: ".normal", + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 0, y: 100 }, + { x: 50000, y: 50 }, + { x: 100000, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -1", + expectedViewboxWidth: 200000, + }, + { + targetSelector: ".normal-playbackrate-2", + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 0, y: 100 }, + { x: 50000, y: 50 }, + { x: 100000, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -2", + expectedViewboxWidth: 400000, + }, + { + targetSelector: ".positive-delay", + expectedSignList: [ + { + selector: ".animation-end-delay-sign", + sign: { + marginInlineStart: "75%", + width: "25%", + }, + }, + ], + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 0, y: 100 }, + { x: 50000, y: 50 }, + { x: 100000, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -1", + }, + { + targetSelector: ".negative-delay", + expectedSignList: [ + { + selector: ".animation-end-delay-sign", + sign: { + marginInlineStart: "50%", + width: "25%", + }, + }, + ], + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 0, y: 100 }, + { x: 50000, y: 50 }, + { x: 50000, y: 0 }, + ], + }, + { + selector: ".animation-negative-delay-path path", + path: [ + { x: 50000, y: 50 }, + { x: 100000, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -1", + }, + { + targetSelector: ".positive-end-delay", + expectedSignList: [ + { + selector: ".animation-delay-sign", + sign: { + isFilled: true, + marginInlineStart: "25%", + width: "25%", + }, + }, + ], + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 50000, y: 100 }, + { x: 100000, y: 50 }, + { x: 150000, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -1", + }, + { + targetSelector: ".negative-end-delay", + expectedSignList: [ + { + selector: ".animation-delay-sign", + sign: { + isFilled: true, + marginInlineStart: "0%", + width: "25%", + }, + }, + ], + expectedPathList: [ + { + selector: ".animation-iteration-path", + path: [ + { x: 0, y: 50 }, + { x: 50000, y: 0 }, + ], + }, + { + selector: ".animation-negative-end-delay-path path", + path: [ + { x: -50000, y: 100 }, + { x: 0, y: 0 }, + ], + }, + ], + expectedTooltip: "Playback rate: -1", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_negative_playback_rate.html"); + const { panel } = await openAnimationInspector(); + + for (const testData of TEST_DATA) { + const { + targetSelector, + expectedPathList, + expectedSignList, + expectedTooltip, + expectedViewboxWidth, + } = testData; + + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + targetSelector + ); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + + info(`Check tooltip for the animation of ${targetSelector}`); + assertTooltip(summaryGraphEl, expectedTooltip); + + if (expectedPathList) { + for (const { selector, path } of expectedPathList) { + info(`Check path for ${selector}`); + assertPath(summaryGraphEl, selector, path); + } + } + + if (expectedSignList) { + for (const { selector, sign } of expectedSignList) { + info(`Check sign for ${selector}`); + assertSign(summaryGraphEl, selector, sign); + } + } + + if (expectedViewboxWidth) { + info("Check width of viewbox of SVG"); + const svgEl = summaryGraphEl.querySelector( + ".animation-summary-graph-path" + ); + is( + svgEl.viewBox.baseVal.width, + expectedViewboxWidth, + `width of viewbox should be ${expectedViewboxWidth}` + ); + } + } +}); + +function assertPath(summaryGraphEl, pathSelector, expectedPath) { + const pathEl = summaryGraphEl.querySelector(pathSelector); + assertPathSegments(pathEl, true, expectedPath); +} + +function assertSign(summaryGraphEl, selector, expectedSign) { + const signEl = summaryGraphEl.querySelector(selector); + + is( + signEl.style.marginInlineStart, + expectedSign.marginInlineStart, + `marginInlineStart position should be ${expectedSign.marginInlineStart}` + ); + is( + signEl.style.width, + expectedSign.width, + `Width should be ${expectedSign.width}` + ); + is( + signEl.classList.contains("fill"), + expectedSign.isFilled || false, + "signEl should be correct" + ); +} + +function assertTooltip(summaryGraphEl, expectedTooltip) { + const tooltip = summaryGraphEl.getAttribute("title"); + ok( + tooltip.includes(expectedTooltip), + `Tooltip should include '${expectedTooltip}'` + ); +} diff --git a/devtools/client/inspector/animation/test/current-time-scrubber_head.js b/devtools/client/inspector/animation/test/current-time-scrubber_head.js new file mode 100644 index 0000000000..1e94a7562c --- /dev/null +++ b/devtools/client/inspector/animation/test/current-time-scrubber_head.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +// Test for following CurrentTimeScrubber and CurrentTimeScrubberController components: +// * element existence +// * scrubber position validity +// * make animations currentTime to change by click on the controller +// * mouse drag on the scrubber + +// eslint-disable-next-line no-unused-vars +async function testCurrentTimeScrubber(isRTL) { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".long"]); + const { animationInspector, panel } = await openAnimationInspector(); + + info("Checking scrubber controller existence"); + const controllerEl = panel.querySelector(".current-time-scrubber-area"); + ok(controllerEl, "scrubber controller should exist"); + + info("Checking scrubber existence"); + const scrubberEl = controllerEl.querySelector(".current-time-scrubber"); + ok(scrubberEl, "scrubber should exist"); + + info("Checking scrubber changes current time of animation and the position"); + const duration = animationInspector.state.timeScale.getDuration(); + clickOnCurrentTimeScrubberController( + animationInspector, + panel, + isRTL ? 1 : 0 + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + await waitUntilCurrentTimeChangedAt(animationInspector, 0); + assertPosition( + scrubberEl, + controllerEl, + isRTL ? duration : 0, + animationInspector + ); + + clickOnCurrentTimeScrubberController( + animationInspector, + panel, + isRTL ? 0 : 1 + ); + await waitUntilCurrentTimeChangedAt(animationInspector, duration); + assertPosition( + scrubberEl, + controllerEl, + isRTL ? 0 : duration, + animationInspector + ); + + clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5); + await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.5); + assertPosition(scrubberEl, controllerEl, duration * 0.5, animationInspector); + + info("Checking current time scrubber position during running"); + // Running again + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "running"); + let previousX = scrubberEl.getBoundingClientRect().x; + await wait(1000); + let currentX = scrubberEl.getBoundingClientRect().x; + isnot(previousX, currentX, "Scrubber should be moved"); + + info("Checking draggable on scrubber over animation list"); + clickOnPauseResumeButton(animationInspector, panel); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + previousX = scrubberEl.getBoundingClientRect().x; + await dragOnCurrentTimeScrubber(animationInspector, panel, 5, 30); + currentX = scrubberEl.getBoundingClientRect().x; + isnot(previousX, currentX, "Scrubber should be draggable"); + + info( + "Checking a behavior which mouse out from animation inspector area " + + "during dragging from controller" + ); + await dragOnCurrentTimeScrubberController(animationInspector, panel, 0.5, 2); + ok( + !panel + .querySelector(".animation-list-container") + .classList.contains("active-scrubber"), + "Click and DnD should be inactive" + ); +} + +function assertPosition(scrubberEl, controllerEl, time, animationInspector) { + const controllerBounds = controllerEl.getBoundingClientRect(); + const scrubberBounds = scrubberEl.getBoundingClientRect(); + const scrubberX = + scrubberBounds.x + scrubberBounds.width / 2 - controllerBounds.x; + const timeScale = animationInspector.state.timeScale; + const expected = Math.round( + (time / timeScale.getDuration()) * controllerBounds.width + ); + is(scrubberX, expected, `Position should be ${expected} at ${time}ms`); +} diff --git a/devtools/client/inspector/animation/test/doc_custom_playback_rate.html b/devtools/client/inspector/animation/test/doc_custom_playback_rate.html new file mode 100644 index 0000000000..9adee99884 --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_custom_playback_rate.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 100px; + } + </style> + </head> + <body> + <script> + "use strict"; + + const duration = 100000; + + function createAnimation(cls) { + const div = document.createElement("div"); + div.classList.add(cls); + document.body.appendChild(div); + const animation = div.animate([{ opacity: 0 }], duration); + animation.playbackRate = 1.5; + } + + createAnimation("div1"); + createAnimation("div2"); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_infinity_duration.html b/devtools/client/inspector/animation/test/doc_infinity_duration.html new file mode 100644 index 0000000000..10d19fc3cf --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_infinity_duration.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 100px; + } + </style> + </head> + <body> + <div class="infinity"></div> + <div class="infinity-delay-iteration-start"></div> + <div class="limited"></div> + <script> + "use strict"; + + document.querySelector(".infinity").animate( + { opacity: [1, 0] }, + { duration: Infinity } + ); + + document.querySelector(".infinity-delay-iteration-start").animate( + { opacity: [1, 0] }, + { + delay: 100000, + duration: Infinity, + iterationStart: 0.5, + } + ); + + document.querySelector(".limited").animate( + { opacity: [1, 0] }, + { + duration: 100000, + } + ); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_multi_easings.html b/devtools/client/inspector/animation/test/doc_multi_easings.html new file mode 100644 index 0000000000..cedcb027fe --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_multi_easings.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 50px; + } + </style> + </head> + <body> + <script> + "use strict"; + + function createAnimation(name, keyframes, effectEasing) { + const div = document.createElement("div"); + div.classList.add(name); + document.body.appendChild(div); + + const effect = { + duration: 100000, + fill: "forwards", + }; + + if (effectEasing) { + effect.easing = effectEasing; + } + + div.animate(keyframes, effect); + } + + createAnimation( + "no-easing", + [ + { opacity: 1 }, + { opacity: 0 }, + ] + ); + + createAnimation( + "effect-easing", + [ + { opacity: 1 }, + { opacity: 0 }, + ], + "steps(5, jump-none)" + ); + + createAnimation( + "keyframe-easing", + [ + { opacity: 1, easing: "steps(2)" }, + { opacity: 0 }, + ] + ); + + createAnimation( + "both-easing", + [ + { offset: 0, opacity: 1, easing: "steps(2)" }, + { offset: 0, marginLeft: "0px", easing: "steps(1)" }, + { marginLeft: "100px", opacity: 0 }, + ], + "steps(10)" + ); + + createAnimation( + "narrow-keyframes", + [ + { opacity: 0 }, + { offset: 0.1, opacity: 1, easing: "steps(1)" }, + { offset: 0.13, opacity: 0 }, + ] + ); + + createAnimation( + "duplicate-keyframes", + [ + { opacity: 0 }, + { offset: 0.5, opacity: 1 }, + { offset: 0.5, opacity: 0, easing: "steps(1)" }, + { opacity: 1 }, + ] + ); + + createAnimation( + "color-keyframes", + [ + { color: "red", easing: "ease-in" }, + { offset: 0.4, color: "blue", easing: "ease-out" }, + { color: "lime" }, + ] + ); + + createAnimation( + "jump-start", + [ + { opacity: 1, easing: "steps(2, jump-start)" }, + { opacity: 0 }, + ], + ); + + createAnimation( + "jump-end", + [ + { opacity: 1, easing: "steps(2, jump-end)" }, + { opacity: 0 }, + ], + ); + + createAnimation( + "jump-both", + [ + { opacity: 1, easing: "steps(3, jump-both)" }, + { opacity: 0 }, + ], + ); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_multi_keyframes.html b/devtools/client/inspector/animation/test/doc_multi_keyframes.html new file mode 100644 index 0000000000..8977f77dde --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_multi_keyframes.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + width: 50px; + height: 50px; + } + </style> + </head> + <body> + <script> + "use strict"; + + function createAnimation(name, keyframes, effectEasing) { + const div = document.createElement("div"); + div.classList.add(name); + document.body.appendChild(div); + + const effect = { + duration: 100000, + fill: "forwards", + }; + + if (effectEasing) { + effect.easing = effectEasing; + } + + div.animate(keyframes, effect); + } + + createAnimation( + "multi-types", + [ + { + backgroundColor: "red", + backgroundRepeat: "space round", + fontSize: "10px", + marginLeft: "0px", + opacity: 0, + textAlign: "right", + transform: "translate(0px)", + }, + { + backgroundColor: "lime", + backgroundRepeat: "round space", + fontSize: "20px", + marginLeft: "100px", + opacity: 1, + textAlign: "center", + transform: "translate(100px)", + }, + ] + ); + + createAnimation( + "multi-types-reverse", + [ + { + backgroundColor: "lime", + backgroundRepeat: "space", + fontSize: "20px", + marginLeft: "100px", + opacity: 1, + textAlign: "center", + transform: "translate(100px)", + }, + { + backgroundColor: "red", + backgroundRepeat: "round", + fontSize: "10px", + marginLeft: "0px", + opacity: 0, + textAlign: "right", + transform: "translate(0px)", + }, + ] + ); + + createAnimation( + "middle-keyframe", + [ + { + backgroundColor: "red", + backgroundRepeat: "space", + fontSize: "10px", + marginLeft: "0px", + opacity: 0, + textAlign: "right", + transform: "translate(0px)", + }, + { + backgroundColor: "blue", + backgroundRepeat: "round", + fontSize: "20px", + marginLeft: "100px", + opacity: 1, + textAlign: "center", + transform: "translate(100px)", + }, + { + backgroundColor: "lime", + backgroundRepeat: "space", + fontSize: "10px", + marginLeft: "0px", + opacity: 0, + textAlign: "right", + transform: "translate(0px)", + }, + ] + ); + + createAnimation( + "steps-keyframe", + [ + { + backgroundColor: "red", + backgroundRepeat: "space", + fontSize: "10px", + marginLeft: "0px", + opacity: 0, + textAlign: "right", + transform: "translate(0px)", + easing: "steps(2)", + }, + { + backgroundColor: "lime", + backgroundRepeat: "round", + fontSize: "20px", + marginLeft: "100px", + opacity: 1, + textAlign: "center", + transform: "translate(100px)", + }, + ] + ); + + createAnimation( + "steps-effect", + [ + { + opacity: 0, + }, + { + opacity: 1, + }, + ], + "steps(2)" + ); + + createAnimation( + "steps-jump-none-keyframe", + [ + { + easing: "steps(5, jump-none)", + opacity: 0, + }, + { + opacity: 1, + }, + ] + ); + + createAnimation( + "narrow-offsets", + [ + { + opacity: 0, + }, + { + opacity: 1, + easing: "steps(2)", + offset: 0.1, + }, + { + opacity: 0, + offset: 0.13, + }, + ] + ); + + createAnimation( + "duplicate-offsets", + [ + { + opacity: 1, + }, + { + opacity: 1, + offset: 0.5, + }, + { + opacity: 0, + offset: 0.5, + }, + { + opacity: 1, + offset: 1, + }, + ] + ); + + createAnimation( + "same-color", + [ + { + backgroundColor: "lime", + }, + { + backgroundColor: "lime", + }, + ] + ); + + createAnimation( + "currentcolor", + [ + { + backgroundColor: "currentColor", + }, + { + backgroundColor: "lime", + }, + ] + ); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_multi_timings.html b/devtools/client/inspector/animation/test/doc_multi_timings.html new file mode 100644 index 0000000000..a999431917 --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_multi_timings.html @@ -0,0 +1,169 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 100px; + width: 100px; + } + + .cssanimation-normal { + animation: cssanimation 1000s; + } + + .cssanimation-linear { + animation: cssanimation 1000s linear; + } + + @keyframes cssanimation { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + </style> + </head> + <body> + <div class="cssanimation-normal"></div> + <div class="cssanimation-linear"></div> + <script> + "use strict"; + + const duration = 1000000; + + function createAnimation(keyframes, effect, className) { + const div = document.createElement("div"); + div.classList.add(className); + document.body.appendChild(div); + effect.duration = duration; + div.animate(keyframes, effect); + } + + createAnimation({ opacity: [0, 1] }, + { delay: 500000, id: "test-delay-animation" }, + "delay-positive"); + + createAnimation({ opacity: [0, 1] }, + { delay: -500000, id: "test-negative-delay-animation" }, + "delay-negative"); + + createAnimation({ opacity: [0, 1] }, + { easing: "steps(2)" }, + "easing-step"); + + createAnimation({ opacity: [0, 1] }, + { endDelay: 500000 }, + "enddelay-positive"); + + createAnimation({ opacity: [0, 1] }, + { endDelay: -500000 }, + "enddelay-negative"); + + createAnimation({ opacity: [0, 1] }, + { endDelay: 500000, fill: "forwards" }, + "enddelay-with-fill-forwards"); + + createAnimation({ opacity: [0, 1] }, + { endDelay: 500000, iterations: Infinity }, + "enddelay-with-iterations-infinity"); + + createAnimation({ opacity: [0, 1] }, + { direction: "alternate", iterations: Infinity }, + "direction-alternate-with-iterations-infinity"); + + createAnimation({ opacity: [0, 1] }, + { direction: "alternate-reverse", iterations: Infinity }, + "direction-alternate-reverse-with-iterations-infinity"); + + createAnimation({ opacity: [0, 1] }, + { direction: "reverse", iterations: Infinity }, + "direction-reverse-with-iterations-infinity"); + + createAnimation({ opacity: [0, 1] }, + { fill: "backwards" }, + "fill-backwards"); + + createAnimation({ opacity: [0, 1] }, + { fill: "backwards", delay: 500000, iterationStart: 0.5 }, + "fill-backwards-with-delay-iterationstart"); + + createAnimation({ opacity: [0, 1] }, + { fill: "both" }, + "fill-both"); + + createAnimation({ opacity: [0, 1] }, + { fill: "both", delay: 500000, iterationStart: 0.5 }, + "fill-both-width-delay-iterationstart"); + + createAnimation({ opacity: [0, 1] }, + { fill: "forwards" }, + "fill-forwards"); + + createAnimation({ opacity: [0, 1] }, + { iterationStart: 0.5 }, + "iterationstart"); + + createAnimation({ width: ["100px", "150px"] }, + {}, + "no-compositor"); + + createAnimation([{ opacity: 0, easing: "steps(2)" }, { opacity: 1 }], + {}, + "keyframes-easing-step"); + + createAnimation( + [ + { + opacity: 0, + offset: 0, + }, + { + opacity: 1, + offset: 0.1, + easing: "steps(1)", + }, + { + opacity: 0, + offset: 0.13, + }, + ], + {}, + "narrow-keyframes"); + + createAnimation( + [ + { + offset: 0, + opacity: 1, + }, + { + offset: 0.5, + opacity: 1, + }, + { + offset: 0.5, + easing: "steps(1)", + opacity: 0, + }, + { + offset: 1, + opacity: 1, + }, + ], + {}, + "duplicate-offsets"); + + createAnimation({ opacity: [0, 1] }, + { delay: -250000 }, + "delay-negative-25"); + + createAnimation({ opacity: [0, 1] }, + { delay: -750000 }, + "delay-negative-75"); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html b/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html new file mode 100644 index 0000000000..c8b3db749b --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + </head> + <body> + <div></div> + + <script> + "use strict"; + + // This function is called from test. + // eslint-disable-next-line + function startMutation() { + const target = document.querySelector("div"); + const animation = target.animate({ opacity: [1, 0] }, 100000); + animation.currentTime = 1; + animation.cancel(); + } + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_mutations_fast.html b/devtools/client/inspector/animation/test/doc_mutations_fast.html new file mode 100644 index 0000000000..3622846953 --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_mutations_fast.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 20px; + opacity: 1; + transition: 0.5s opacity; + } + + .transition { + opacity: 0; + } + </style> + </head> + <body> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + + <script> + "use strict"; + + // This function is called from test. + // eslint-disable-next-line + async function startFastMutations() { + const targets = document.querySelectorAll("div"); + + for (let i = 0; i < 10; i++) { + for (const target of targets) { + target.classList.toggle("transition"); + await wait(15); + } + } + } + + async function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + } + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_negative_playback_rate.html b/devtools/client/inspector/animation/test/doc_negative_playback_rate.html new file mode 100644 index 0000000000..a98700712d --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_negative_playback_rate.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 100px; + } + </style> + </head> + <body> + <script> + "use strict"; + + const DURATION = 100000; + const KEYFRAMES = { backgroundColor: ["lime", "red"] }; + + function createAnimation(effect, className, playbackRate = -1) { + const div = document.createElement("div"); + div.classList.add(className); + document.body.appendChild(div); + effect.duration = DURATION; + effect.fill = "forwards"; + const animation = div.animate(KEYFRAMES, effect); + animation.updatePlaybackRate(playbackRate); + animation.play(); + } + + createAnimation({}, "normal"); + createAnimation({}, "normal-playbackrate-2", -2); + createAnimation({ delay: 50000 }, "positive-delay"); + createAnimation({ delay: -50000 }, "negative-delay"); + createAnimation({ endDelay: 50000 }, "positive-end-delay"); + createAnimation({ endDelay: -50000 }, "negative-end-delay"); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html b/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html new file mode 100644 index 0000000000..a4d91ae4ef --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + width: 100px; + height: 100px; + outline: 1px solid lime; + } + </style> + </head> + <body> + <div id="target"></div> + <script> + "use strict"; + + const target = document.getElementById("target"); + target.animate( + { + color: ["red", "lime"], + }, + { + id: "big-delay", + duration: 1000, + delay: Number.MAX_VALUE, + iterations: Infinity, + }); + + target.animate( + { + opacity: [1, 0], + }, + { + id: "big-end-delay", + duration: 1000, + endDelay: Number.MAX_VALUE, + iterations: Infinity, + }); + + target.animate( + { + marginLeft: ["0px", "100px"], + }, + { + id: "negative-big-delay", + duration: 1000, + delay: -Number.MAX_VALUE, + iterations: Infinity, + }); + + target.animate( + { + paddingLeft: ["0px", "100px"], + }, + { + id: "negative-big-end-delay", + duration: 1000, + endDelay: -Number.MAX_VALUE, + iterations: Infinity, + }); + + target.animate( + { + backgroundColor: ["lime", "white"], + }, + { + id: "big-iteration-start", + duration: 1000, + iterations: Infinity, + iterationStart: Number.MAX_VALUE, + }); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_pseudo.html b/devtools/client/inspector/animation/test/doc_pseudo.html new file mode 100644 index 0000000000..3cc0c93470 --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_pseudo.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + body::before { + animation: body 10s infinite; + background-color: lime; + content: "body-before"; + width: 100px; + } + + .div-before::before { + animation: div-before 10s infinite; + background-color: lime; + content: "div-before"; + width: 100px; + } + + .div-after::after { + animation: div-after 10s infinite; + background-color: lime; + content: "div-after"; + width: 100px; + } + + .div-marker { + display: list-item; + list-style-position: inside; + } + + .div-marker::marker { + content: "div-marker"; + } + + @keyframes body { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes div-before { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes div-after { + from { + opacity: 1; + } + 50% { + opacity: 0.9; + } + to { + opacity: 0; + } + } + </style> + </head> + <body> + <div class="div-before"></div> + <div class="div-after"></div> + <div class="div-marker"></div> + + <script> + "use strict"; + + // The reason why we currently run the animation on `::marker` with Web Animations API + // instead of CSS Animations is because it requires `layout.css.marker.restricted` + // pref change. + document.querySelector(".div-marker").animate( + { + color: ["black", "lime"], + }, + { + id: "div-marker", + duration: 10000, + iterations: Infinity, + pseudoElement: "::marker", + } + ); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_short_duration.html b/devtools/client/inspector/animation/test/doc_short_duration.html new file mode 100644 index 0000000000..ed9b2d94dc --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_short_duration.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + background-color: lime; + height: 100px; + } + </style> + </head> + <body> + <div class="short"></div> + <script> + "use strict"; + + document.querySelector(".short").animate( + { opacity: [1, 0] }, + { + duration: 1, + iterations: Infinity, + } + ); + </script> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_simple_animation.html b/devtools/client/inspector/animation/test/doc_simple_animation.html new file mode 100644 index 0000000000..7e145166fe --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_simple_animation.html @@ -0,0 +1,193 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + @property --ball-color { + syntax: "<color>"; + inherits: true; + initial-value: #f06; + } + + .ball { + width: 80px; + height: 80px; + /* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */ + border: 1px solid transparent; + border-radius: 50%; + background: var(--ball-color); + position: absolute; + } + + .still { + top: 0; + left: 10px; + } + + .animated { + top: 100px; + left: 10px; + + animation: simple-animation 2s infinite alternate; + } + + .multi { + top: 200px; + left: 10px; + + animation: simple-animation 2s infinite alternate, + other-animation 5s infinite alternate; + } + + .delayed { + top: 300px; + left: 10px; + background: rebeccapurple; + + animation: simple-animation 3s 60s 10; + } + + .multi-finite { + top: 400px; + left: 10px; + background: yellow; + + animation: simple-animation 3s, + other-animation 4s; + } + + .short { + top: 500px; + left: 10px; + background: red; + + animation: simple-animation 2s normal; + } + + .long { + top: 600px; + left: 10px; + background: blue; + + animation: simple-animation 120s; + } + + .negative-delay { + top: 700px; + left: 10px; + background: gray; + + animation: simple-animation 15s -10s; + animation-fill-mode: forwards; + } + + .no-compositor { + top: 0; + right: 10px; + background: gold; + + animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards; + } + + .compositor-all { + animation: compositor-all 2s infinite; + } + + .compositor-notall { + animation: compositor-notall 2s infinite; + } + + .compositor-warning { + animation: compositor-all 2s infinite; + } + + .warning-observer { + width: 10px; + height: 10px; + background-image: -moz-element(#warning); + } + + .longhand { + animation: longhand 10s infinite; + } + + @keyframes simple-animation { + 100% { + transform: translateX(300px); + } + } + + @keyframes other-animation { + 100% { + background: blue; + } + } + + @keyframes no-compositor { + 100% { + margin-right: 600px; + } + } + + @keyframes compositor-all { + to { opacity: 0.5 } + } + + @keyframes compositor-notall { + from { + --ball-color: tomato; + opacity: 0; + width: 0px; + transform: translate(0px); + } + to { + --ball-color: gold; + opacity: 1; + width: 100px; + transform: translate(100px); + } + } + + @keyframes longhand { + from { + background: red; + padding: 0 0 0 10px; + } + to { + background: lime; + padding: 0 0 0 20px; + } + } + </style> +</head> +<body> + <!-- Comment node --> + <div class="ball still"></div> + <div class="ball animated"></div> + <div class="ball multi"></div> + <div class="ball delayed"></div> + <div class="ball multi-finite"></div> + <div class="ball short"></div> + <div class="ball long"></div> + <div class="ball negative-delay"></div> + <div class="ball no-compositor"></div> + <div class="ball end-delay"></div> + <div class="ball compositor-all"></div> + <div class="ball compositor-notall"></div> + <div class="ball compositor-warning" id="warning"></div> + <div class="ball longhand"></div> + <div class="warning-observer"></div> + <script> + /* globals KeyframeEffect, Animation */ + "use strict"; + + const el = document.querySelector(".end-delay"); + const effect = new KeyframeEffect(el, [ + { opacity: 0, offset: 0 }, + { opacity: 1, offset: 1 }, + ], { duration: 1000000, endDelay: 500000, fill: "none" }); + const animation = new Animation(effect, document.timeline); + animation.play(); + </script> +</body> +</html> diff --git a/devtools/client/inspector/animation/test/doc_special_colors.html b/devtools/client/inspector/animation/test/doc_special_colors.html new file mode 100644 index 0000000000..2c71b2c963 --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_special_colors.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + animation: anim 5s infinite; + border: 1px solid lime; + height: 100px; + width: 100px; + } + + @keyframes anim { + from { + caret-color: auto; + scrollbar-color: lime red; + } + to { + caret-color: lime; + scrollbar-color: auto; + } + } + </style> + </head> + <body> + <div></div> + </body> +</html> diff --git a/devtools/client/inspector/animation/test/head.js b/devtools/client/inspector/animation/test/head.js new file mode 100644 index 0000000000..959ad270a5 --- /dev/null +++ b/devtools/client/inspector/animation/test/head.js @@ -0,0 +1,1028 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +const TAB_NAME = "animationinspector"; + +const ANIMATION_L10N = new LocalizationHelper( + "devtools/client/locales/animationinspector.properties" +); + +// Auto clean-up when a test ends. +// Clean-up all prefs that might have been changed during a test run +// (safer here because if the test fails, then the pref is never reverted) +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolsidebar-width.inspector"); +}); + +/** + * Open the toolbox, with the inspector tool visible and the animationinspector + * sidebar selected. + * + * @return {Promise} that resolves when the inspector is ready. + */ +const openAnimationInspector = async function () { + const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME); + await inspector.once("inspector-updated"); + const animationInspector = inspector.getPanel("animationinspector"); + const panel = inspector.panelWin.document.getElementById( + "animation-container" + ); + + info("Wait for loading first content"); + const count = getDisplayedGraphCount(animationInspector, panel); + await waitUntil( + () => + panel.querySelectorAll(".animation-summary-graph-path").length >= count && + panel.querySelectorAll(".animation-target .objectBox").length >= count + ); + + if ( + animationInspector.state.selectedAnimation && + animationInspector.state.detailVisibility + ) { + await waitUntil(() => panel.querySelector(".animated-property-list")); + } + + return { animationInspector, toolbox, inspector, panel }; +}; + +/** + * Close the toolbox. + * + * @return {Promise} that resolves when the toolbox has closed. + */ +const closeAnimationInspector = async function () { + return gDevTools.closeToolboxForTab(gBrowser.selectedTab); +}; + +/** + * Some animation features are not enabled by default in release/beta channels + * yet including parts of the Web Animations API. + */ +const enableAnimationFeatures = function () { + return SpecialPowers.pushPrefEnv({ + set: [["dom.animations-api.timelines.enabled", true]], + }); +}; + +/** + * Add a new test tab in the browser and load the given url. + * + * @param {String} url + * The url to be loaded in the new tab + * @return a promise that resolves to the tab object when the url is loaded + */ +const _addTab = addTab; +addTab = async function (url) { + await enableAnimationFeatures(); + return _addTab(url); +}; + +/** + * Remove animated elements from document except given selectors. + * + * @param {Array} selectors + * @return {Promise} + */ +const removeAnimatedElementsExcept = function (selectors) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selectors], + selectorsChild => { + function isRemovableElement(animation, selectorsInner) { + for (const selector of selectorsInner) { + if (animation.effect.target.matches(selector)) { + return false; + } + } + + return true; + } + + for (const animation of content.document.getAnimations()) { + if (isRemovableElement(animation, selectorsChild)) { + animation.effect.target.remove(); + } + } + } + ); +}; + +/** + * Click on an animation in the timeline to select it. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * The index of the animation to click on. + */ +const clickOnAnimation = async function (animationInspector, panel, index) { + info("Click on animation " + index + " in the timeline"); + const animationItemEl = await findAnimationItemByIndex(panel, index); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + clickOnSummaryGraph(animationInspector, panel, summaryGraphEl); +}; + +/** + * Click on an animation by given selector of node which is target element of animation. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {String} selector + * Selector of node which is target element of animation. + */ +const clickOnAnimationByTargetSelector = async function ( + animationInspector, + panel, + selector +) { + info(`Click on animation whose selector of target element is '${selector}'`); + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + selector + ); + const summaryGraphEl = animationItemEl.querySelector( + ".animation-summary-graph" + ); + clickOnSummaryGraph(animationInspector, panel, summaryGraphEl); +}; + +/** + * Click on close button for animation detail pane. + * + * @param {DOMElement} panel + * #animation-container element. + */ +const clickOnDetailCloseButton = function (panel) { + info("Click on close button for animation detail pane"); + const buttonEl = panel.querySelector(".animation-detail-close-button"); + const bounds = buttonEl.getBoundingClientRect(); + const x = bounds.width / 2; + const y = bounds.height / 2; + EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); +}; + +/** + * Click on pause/resume button. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * #animation-container element. + */ +const clickOnPauseResumeButton = function (animationInspector, panel) { + info("Click on pause/resume button"); + const buttonEl = panel.querySelector(".pause-resume-button"); + const bounds = buttonEl.getBoundingClientRect(); + const x = bounds.width / 2; + const y = bounds.height / 2; + EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); +}; + +/** + * Click on rewind button. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * #animation-container element. + */ +const clickOnRewindButton = function (animationInspector, panel) { + info("Click on rewind button"); + const buttonEl = panel.querySelector(".rewind-button"); + const bounds = buttonEl.getBoundingClientRect(); + const x = bounds.width / 2; + const y = bounds.height / 2; + EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); +}; + +/** + * Click on the scrubber controller pane to update the animation current time. + * + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} mouseDownPosition + * rate on scrubber controller pane. + * This method calculates + * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane` + * as the clientX of MouseEvent. + */ +const clickOnCurrentTimeScrubberController = function ( + animationInspector, + panel, + mouseDownPosition +) { + const controllerEl = panel.querySelector(".current-time-scrubber-area"); + const bounds = controllerEl.getBoundingClientRect(); + const mousedonwX = bounds.width * mouseDownPosition; + + info(`Click ${mousedonwX} on scrubber controller`); + EventUtils.synthesizeMouse( + controllerEl, + mousedonwX, + 0, + {}, + controllerEl.ownerGlobal + ); +}; + +/** + * Click on the inspect icon for the given AnimationTargetComponent. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * The index of the AnimationTargetComponent to click on. + */ +const clickOnInspectIcon = async function (animationInspector, panel, index) { + info(`Click on an inspect icon in animation target component[${index}]`); + const animationItemEl = await findAnimationItemByIndex(panel, index); + const iconEl = animationItemEl.querySelector( + ".animation-target .objectBox .highlight-node" + ); + iconEl.scrollIntoView(false); + EventUtils.synthesizeMouseAtCenter(iconEl, {}, iconEl.ownerGlobal); +}; + +/** + * Change playback rate selector to select given rate. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} rate + */ +const changePlaybackRateSelector = async function ( + animationInspector, + panel, + rate +) { + info(`Click on playback rate selector to select ${rate}`); + const selectEl = panel.querySelector(".playback-rate-selector"); + const optionIndex = [...selectEl.options].findIndex(o => +o.value == rate); + + if (optionIndex == -1) { + ok( + false, + `Could not find an option for rate ${rate} in the rate selector. ` + + `Values are: ${[...selectEl.options].map(o => o.value)}` + ); + return; + } + + selectEl.focus(); + + const win = selectEl.ownerGlobal; + while (selectEl.selectedIndex != optionIndex) { + const key = selectEl.selectedIndex > optionIndex ? "LEFT" : "RIGHT"; + EventUtils.sendKey(key, win); + } +}; + +/** + * Click on given summary graph element. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * #animation-container element. + * @param {Element} summaryGraphEl + */ +const clickOnSummaryGraph = function ( + animationInspector, + panel, + summaryGraphEl +) { + // Disable pointer-events of the scrubber in order to avoid to click accidently. + const scrubberEl = panel.querySelector(".current-time-scrubber"); + scrubberEl.style.pointerEvents = "none"; + // Scroll to show the timeBlock since the element may be out of displayed area. + summaryGraphEl.scrollIntoView(false); + EventUtils.synthesizeMouseAtCenter( + summaryGraphEl, + {}, + summaryGraphEl.ownerGlobal + ); + // Restore the scrubber style. + scrubberEl.style.pointerEvents = "unset"; +}; + +/** + * Click on the target node for the given AnimationTargetComponent index. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * The index of the AnimationTargetComponent to click on. + */ +const clickOnTargetNode = async function (animationInspector, panel, index) { + const { inspector } = animationInspector; + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + info(`Click on a target node in animation target component[${index}]`); + + const animationItemEl = await findAnimationItemByIndex(panel, index); + const targetEl = animationItemEl.querySelector( + ".animation-target .objectBox" + ); + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal); + await onHighlight; +}; + +/** + * Drag on the scrubber to update the animation current time. + * + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} mouseMovePixel + * Dispatch mousemove event with mouseMovePosition after mousedown. + * @param {Number} mouseYPixel + * Y of mouse in pixel. + */ +const dragOnCurrentTimeScrubber = async function ( + animationInspector, + panel, + mouseMovePixel, + mouseYPixel +) { + const controllerEl = panel.querySelector(".current-time-scrubber"); + info(`Drag scrubber to X ${mouseMovePixel}`); + EventUtils.synthesizeMouse( + controllerEl, + 0, + mouseYPixel, + { type: "mousedown" }, + controllerEl.ownerGlobal + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + + const animation = animationInspector.state.animations[0]; + let currentTime = animation.state.currentTime; + EventUtils.synthesizeMouse( + controllerEl, + mouseMovePixel, + mouseYPixel, + { type: "mousemove" }, + controllerEl.ownerGlobal + ); + await waitUntil(() => animation.state.currentTime !== currentTime); + + currentTime = animation.state.currentTime; + EventUtils.synthesizeMouse( + controllerEl, + mouseMovePixel, + mouseYPixel, + { type: "mouseup" }, + controllerEl.ownerGlobal + ); + await waitUntil(() => animation.state.currentTime !== currentTime); +}; + +/** + * Drag on the scrubber controller pane to update the animation current time. + * + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} mouseDownPosition + * rate on scrubber controller pane. + * This method calculates + * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane` + * as the clientX of MouseEvent. + * @param {Number} mouseMovePosition + * Dispatch mousemove event with mouseMovePosition after mousedown. + * Calculation for clinetX is same to above. + */ +const dragOnCurrentTimeScrubberController = async function ( + animationInspector, + panel, + mouseDownPosition, + mouseMovePosition +) { + const controllerEl = panel.querySelector(".current-time-scrubber-area"); + const bounds = controllerEl.getBoundingClientRect(); + const mousedonwX = bounds.width * mouseDownPosition; + const mousemoveX = bounds.width * mouseMovePosition; + + info(`Drag on scrubber controller from ${mousedonwX} to ${mousemoveX}`); + EventUtils.synthesizeMouse( + controllerEl, + mousedonwX, + 0, + { type: "mousedown" }, + controllerEl.ownerGlobal + ); + await waitUntilAnimationsPlayState(animationInspector, "paused"); + + const animation = animationInspector.state.animations[0]; + let currentTime = animation.state.currentTime; + EventUtils.synthesizeMouse( + controllerEl, + mousemoveX, + 0, + { type: "mousemove" }, + controllerEl.ownerGlobal + ); + await waitUntil(() => animation.state.currentTime !== currentTime); + + currentTime = animation.state.currentTime; + EventUtils.synthesizeMouse( + controllerEl, + mousemoveX, + 0, + { type: "mouseup" }, + controllerEl.ownerGlobal + ); + await waitUntil(() => animation.state.currentTime !== currentTime); +}; + +/** + * Get current animation duration and rate of + * clickOrDragOnCurrentTimeScrubberController in given pixels. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} pixels + * @return {Object} + * { + * duration, + * rate, + * } + */ +const getDurationAndRate = function (animationInspector, panel, pixels) { + const controllerEl = panel.querySelector(".current-time-scrubber-area"); + const bounds = controllerEl.getBoundingClientRect(); + const duration = + (animationInspector.state.timeScale.getDuration() / bounds.width) * pixels; + const rate = (1 / bounds.width) * pixels; + return { duration, rate }; +}; + +/** + * Mouse over the target node for the given AnimationTargetComponent index. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * The index of the AnimationTargetComponent to click on. + */ +const mouseOverOnTargetNode = function (animationInspector, panel, index) { + info(`Mouse over on a target node in animation target component[${index}]`); + const el = panel.querySelectorAll(".animation-target .objectBox")[index]; + el.scrollIntoView(false); + EventUtils.synthesizeMouse(el, 10, 5, { type: "mouseover" }, el.ownerGlobal); +}; + +/** + * Mouse out of the target node for the given AnimationTargetComponent index. + * + * @param {AnimationInspector} animationInspector. + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * The index of the AnimationTargetComponent to click on. + */ +const mouseOutOnTargetNode = function (animationInspector, panel, index) { + info(`Mouse out on a target node in animation target component[${index}]`); + const el = panel.querySelectorAll(".animation-target .objectBox")[index]; + el.scrollIntoView(false); + EventUtils.synthesizeMouse(el, -1, -1, { type: "mouseout" }, el.ownerGlobal); +}; + +/** + * Select animation inspector in sidebar and toolbar. + * + * @param {InspectorPanel} inspector + */ +const selectAnimationInspector = async function (inspector) { + await inspector.toolbox.selectTool("inspector"); + const onDispatched = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS"); + inspector.sidebar.select("animationinspector"); + await onDispatched; +}; + +/** + * Send keyboard event of space to given panel. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} target element. + */ +const sendSpaceKeyEvent = function (animationInspector, element) { + element.focus(); + EventUtils.sendKey("SPACE", element.ownerGlobal); +}; + +/** + * Set a node class attribute to the given selector. + * + * @param {AnimationInspector} animationInspector + * @param {String} selector + * @param {String} cls + * e.g. ".ball.still" + */ +const setClassAttribute = async function (animationInspector, selector, cls) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [cls, selector], + (attributeValue, selectorChild) => { + const node = content.document.querySelector(selectorChild); + if (!node) { + return; + } + + node.setAttribute("class", attributeValue); + } + ); +}; + +/** + * Set a new style properties to the node for the given selector. + * + * @param {AnimationInspector} animationInspector + * @param {String} selector + * @param {Object} properties + * e.g. { + * animationDuration: "1000ms", + * animationTimingFunction: "linear", + * } + */ +const setEffectTimingAndPlayback = async function ( + animationInspector, + selector, + effectTiming, + playbackRate +) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, playbackRate, effectTiming], + (selectorChild, playbackRateChild, effectTimingChild) => { + let selectedAnimation = null; + + for (const animation of content.document.getAnimations()) { + if (animation.effect.target.matches(selectorChild)) { + selectedAnimation = animation; + break; + } + } + + if (!selectedAnimation) { + return; + } + + selectedAnimation.playbackRate = playbackRateChild; + selectedAnimation.effect.updateTiming(effectTimingChild); + } + ); +}; + +/** + * Set the sidebar width by given parameter. + * + * @param {String} width + * Change sidebar width by given parameter. + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return {Promise} Resolves when the sidebar size changed. + */ +const setSidebarWidth = async function (width, inspector) { + const onUpdated = inspector.toolbox.once("inspector-sidebar-resized"); + inspector.splitBox.setState({ width }); + await onUpdated; +}; + +/** + * Set a new style property declaration to the node for the given selector. + * + * @param {AnimationInspector} animationInspector + * @param {String} selector + * @param {String} propertyName + * e.g. "animationDuration" + * @param {String} propertyValue + * e.g. "5.5s" + */ +const setStyle = async function ( + animationInspector, + selector, + propertyName, + propertyValue +) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, propertyName, propertyValue], + (selectorChild, propertyNameChild, propertyValueChild) => { + const node = content.document.querySelector(selectorChild); + if (!node) { + return; + } + + node.style[propertyNameChild] = propertyValueChild; + } + ); +}; + +/** + * Set a new style properties to the node for the given selector. + * + * @param {AnimationInspector} animationInspector + * @param {String} selector + * @param {Object} properties + * e.g. { + * animationDuration: "1000ms", + * animationTimingFunction: "linear", + * } + */ +const setStyles = async function (animationInspector, selector, properties) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [properties, selector], + (propertiesChild, selectorChild) => { + const node = content.document.querySelector(selectorChild); + if (!node) { + return; + } + + for (const propertyName in propertiesChild) { + const propertyValue = propertiesChild[propertyName]; + node.style[propertyName] = propertyValue; + } + } + ); +}; + +/** + * Wait until current time of animations will be changed to given current time. + * + * @param {AnimationInspector} animationInspector + * @param {Number} currentTime + */ +const waitUntilCurrentTimeChangedAt = async function ( + animationInspector, + currentTime +) { + info(`Wait until current time will be change to ${currentTime}`); + await waitUntil(() => + animationInspector.state.animations.every(a => { + return a.state.currentTime === currentTime; + }) + ); +}; + +/** + * Wait until animations' play state will be changed to given state. + * + * @param {Array} animationInspector + * @param {String} state + */ +const waitUntilAnimationsPlayState = async function ( + animationInspector, + state +) { + info(`Wait until play state will be change to ${state}`); + await waitUntil(() => + animationInspector.state.animations.every(a => a.state.playState === state) + ); +}; + +/** + * Return count of graph that animation inspector is displaying. + * + * @param {AnimationInspector} animationInspector + * @param {DOMElement} panel + * @return {Number} count + */ +const getDisplayedGraphCount = (animationInspector, panel) => { + const animationLength = animationInspector.state.animations.length; + if (animationLength === 0) { + return 0; + } + + const inspectionPanelEl = panel.querySelector(".progress-inspection-panel"); + const itemEl = panel.querySelector(".animation-item"); + const listEl = panel.querySelector(".animation-list"); + const itemHeight = itemEl.offsetHeight; + // This calculation should be same as AnimationListContainer.updateDisplayableRange. + const count = Math.floor(listEl.offsetHeight / itemHeight) + 1; + const index = Math.floor(inspectionPanelEl.scrollTop / itemHeight); + + return animationLength > index + count ? count : animationLength - index; +}; + +/** + * Check whether the animations are pausing. + * + * @param {AnimationInspector} animationInspector + */ +function assertAnimationsPausing(animationInspector) { + assertAnimationsPausingOrRunning(animationInspector, true); +} + +/** + * Check whether the animations are pausing/running. + * + * @param {AnimationInspector} animationInspector + * @param {boolean} shouldPause + */ +function assertAnimationsPausingOrRunning(animationInspector, shouldPause) { + const hasRunningAnimation = animationInspector.state.animations.some( + ({ state }) => state.playState === "running" + ); + + if (shouldPause) { + is(hasRunningAnimation, false, "All animations should be paused"); + } else { + is(hasRunningAnimation, true, "Animations should be running at least one"); + } +} + +/** + * Check whether the animations are running. + * + * @param {AnimationInspector} animationInspector + */ +function assertAnimationsRunning(animationInspector) { + assertAnimationsPausingOrRunning(animationInspector, false); +} + +/** + * Check the <stop> element in the given linearGradientEl for the correct offset + * and color attributes. + * + * @param {Element} linearGradientEl + <linearGradient> element which has <stop> element. + * @param {Number} offset + * float which represents the "offset" attribute of <stop>. + * @param {String} expectedColor + * e.g. rgb(0, 0, 255) + */ +function assertLinearGradient(linearGradientEl, offset, expectedColor) { + const stopEl = findStopElement(linearGradientEl, offset); + ok(stopEl, `stop element at offset ${offset} should exist`); + is( + stopEl.getAttribute("stop-color"), + expectedColor, + `stop-color of stop element at offset ${offset} should be ${expectedColor}` + ); +} + +/** + * SummaryGraph is constructed by <path> element. + * This function checks the vertex of path segments. + * + * @param {Element} pathEl + * <path> element. + * @param {boolean} hasClosePath + * Set true if the path shoud be closing. + * @param {Object} expectedValues + * JSON object format. We can test the vertex and color. + * e.g. + * [ + * { x: 0, y: 0 }, + * { x: 0, y: 1 }, + * ] + */ +function assertPathSegments(pathEl, hasClosePath, expectedValues) { + ok( + isExpectedPath(pathEl, hasClosePath, expectedValues), + "All of path segments are correct" + ); +} + +function isExpectedPath(pathEl, hasClosePath, expectedValues) { + const pathSegList = pathEl.pathSegList; + if (!pathSegList) { + return false; + } + + if ( + !expectedValues.every(value => + isPassingThrough(pathSegList, value.x, value.y) + ) + ) { + return false; + } + + if (hasClosePath) { + const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1); + if (closePathSeg.pathSegType !== closePathSeg.PATHSEG_CLOSEPATH) { + return false; + } + } + + return true; +} + +/** + * Check whether the given vertex is passing throug on the path. + * + * @param {pathSegList} pathSegList - pathSegList of <path> element. + * @param {float} x - x of vertex. + * @param {float} y - y of vertex. + * @return {boolean} true: passing through, false: no on the path. + */ +function isPassingThrough(pathSegList, x, y) { + let previousPathSeg = pathSegList.getItem(0); + for (let i = 0; i < pathSegList.numberOfItems; i++) { + const pathSeg = pathSegList.getItem(i); + if (pathSeg.x === undefined) { + continue; + } + const currentX = parseFloat(pathSeg.x.toFixed(3)); + const currentY = parseFloat(pathSeg.y.toFixed(3)); + if (currentX === x && currentY === y) { + return true; + } + const previousX = parseFloat(previousPathSeg.x.toFixed(3)); + const previousY = parseFloat(previousPathSeg.y.toFixed(3)); + if ( + previousX <= x && + x <= currentX && + Math.min(previousY, currentY) <= y && + y <= Math.max(previousY, currentY) + ) { + return true; + } + previousPathSeg = pathSeg; + } + return false; +} + +/** + * Return animation item element by the index. + * + * @param {DOMElement} panel + * #animation-container element. + * @param {Number} index + * @return {DOMElement} + * Animation item element. + */ +async function findAnimationItemByIndex(panel, index) { + const itemEls = [...panel.querySelectorAll(".animation-item")]; + const itemEl = itemEls[index]; + itemEl.scrollIntoView(false); + + await waitUntil( + () => + itemEl.querySelector(".animation-target .attrName") && + itemEl.querySelector(".animation-computed-timing-path") + ); + + return itemEl; +} + +/** + * Return animation item element by target node selector. + * This function compares betweem animation-target textContent and given selector. + * Then returns matched first item. + * + * @param {DOMElement} panel + * #animation-container element. + * @param {String} selector + * Selector of tested element. + * @return {DOMElement} + * Animation item element. + */ +async function findAnimationItemByTargetSelector(panel, selector) { + for (const itemEl of panel.querySelectorAll(".animation-item")) { + itemEl.scrollIntoView(false); + + await waitUntil( + () => + itemEl.querySelector(".animation-target .attrName") && + itemEl.querySelector(".animation-computed-timing-path") + ); + + const attrNameEl = itemEl.querySelector(".animation-target .attrName"); + const regexp = new RegExp(`\\${selector}(\\.|$)`, "gi"); + if (regexp.exec(attrNameEl.textContent)) { + return itemEl; + } + } + + return null; +} + +/** + * Find the <stop> element which has the given offset in the given linearGradientEl. + * + * @param {Element} linearGradientEl + * <linearGradient> element which has <stop> element. + * @param {Number} offset + * Float which represents the "offset" attribute of <stop>. + * @return {Element} + * If can't find suitable element, returns null. + */ +function findStopElement(linearGradientEl, offset) { + for (const stopEl of linearGradientEl.querySelectorAll("stop")) { + if (offset <= parseFloat(stopEl.getAttribute("offset"))) { + return stopEl; + } + } + + return null; +} + +/** + * Do test for keyframes-graph_computed-value-path-1/2. + * + * @param {Array} testData + */ +async function testKeyframesGraphComputedValuePath(testData) { + await addTab(URL_ROOT + "doc_multi_keyframes.html"); + await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`)); + const { animationInspector, panel } = await openAnimationInspector(); + + for (const { properties, targetClass } of testData) { + info(`Checking keyframes graph for ${targetClass}`); + const onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + `.${targetClass}` + ); + await onDetailRendered; + + for (const property of properties) { + const { + name, + computedValuePathClass, + expectedPathSegments, + expectedStopColors, + } = property; + + const testTarget = `${name} in ${targetClass}`; + info(`Checking keyframes graph for ${testTarget}`); + info(`Checking keyframes graph path existence for ${testTarget}`); + const keyframesGraphPathEl = panel.querySelector(`.${name}`); + ok( + keyframesGraphPathEl, + `The keyframes graph path element of ${testTarget} should be existence` + ); + + info(`Checking computed value path existence for ${testTarget}`); + const computedValuePathEl = keyframesGraphPathEl.querySelector( + `.${computedValuePathClass}` + ); + ok( + computedValuePathEl, + `The computed value path element of ${testTarget} should be existence` + ); + + info(`Checking path segments for ${testTarget}`); + const pathEl = computedValuePathEl.querySelector("path"); + ok(pathEl, `The <path> element of ${testTarget} should be existence`); + assertPathSegments(pathEl, true, expectedPathSegments); + + if (!expectedStopColors) { + continue; + } + + info(`Checking linearGradient for ${testTarget}`); + const linearGradientEl = + computedValuePathEl.querySelector("linearGradient"); + ok( + linearGradientEl, + `The <linearGradientEl> element of ${testTarget} should be existence` + ); + + for (const expectedStopColor of expectedStopColors) { + const { offset, color } = expectedStopColor; + assertLinearGradient(linearGradientEl, offset, color); + } + } + } +} + +/** + * Check the adjusted current time and created time from specified two animations. + * + * @param {AnimationPlayerFront.state} animation1 + * @param {AnimationPlayerFront.state} animation2 + */ +function checkAdjustingTheTime(animation1, animation2) { + const adjustedCurrentTimeDiff = + animation2.currentTime / animation2.playbackRate - + animation1.currentTime / animation1.playbackRate; + const createdTimeDiff = animation1.createdTime - animation2.createdTime; + Assert.less( + Math.abs(adjustedCurrentTimeDiff - createdTimeDiff), + 0.1, + "Adjusted time is correct" + ); +} diff --git a/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js b/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js new file mode 100644 index 0000000000..97c7040553 --- /dev/null +++ b/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +// Test for following keyframe marker. +// * element existence +// * title +// * and marginInlineStart style + +const KEYFRAMES_TEST_DATA = [ + { + targetClass: "multi-types", + properties: [ + { + name: "background-color", + expectedValues: [ + { + title: "rgb(255, 0, 0)", + marginInlineStart: "0%", + }, + { + title: "rgb(0, 255, 0)", + marginInlineStart: "100%", + }, + ], + }, + { + name: "background-repeat", + expectedValues: [ + { + title: "space round", + marginInlineStart: "0%", + }, + { + title: "round space", + marginInlineStart: "100%", + }, + ], + }, + { + name: "font-size", + expectedValues: [ + { + title: "10px", + marginInlineStart: "0%", + }, + { + title: "20px", + marginInlineStart: "100%", + }, + ], + }, + { + name: "margin-left", + expectedValues: [ + { + title: "0px", + marginInlineStart: "0%", + }, + { + title: "100px", + marginInlineStart: "100%", + }, + ], + }, + { + name: "opacity", + expectedValues: [ + { + title: "0", + marginInlineStart: "0%", + }, + { + title: "1", + marginInlineStart: "100%", + }, + ], + }, + { + name: "text-align", + expectedValues: [ + { + title: "right", + marginInlineStart: "0%", + }, + { + title: "center", + marginInlineStart: "100%", + }, + ], + }, + { + name: "transform", + expectedValues: [ + { + title: "translate(0px)", + marginInlineStart: "0%", + }, + { + title: "translate(100px)", + marginInlineStart: "100%", + }, + ], + }, + ], + }, + { + targetClass: "narrow-offsets", + properties: [ + { + name: "opacity", + expectedValues: [ + { + title: "0", + marginInlineStart: "0%", + }, + { + title: "1", + marginInlineStart: "10%", + }, + { + title: "0", + marginInlineStart: "13%", + }, + { + title: "1", + marginInlineStart: "100%", + }, + ], + }, + ], + }, + { + targetClass: "same-color", + properties: [ + { + name: "background-color", + expectedValues: [ + { + title: "rgb(0, 255, 0)", + marginInlineStart: "0%", + }, + { + title: "rgb(0, 255, 0)", + marginInlineStart: "100%", + }, + ], + }, + ], + }, + { + targetClass: "currentcolor", + properties: [ + { + name: "background-color", + expectedValues: [ + { + title: "currentcolor", + marginInlineStart: "0%", + }, + { + title: "rgb(0, 255, 0)", + marginInlineStart: "100%", + }, + ], + }, + ], + }, +]; + +/** + * Do test for keyframes-graph_keyframe-marker-ltf/rtl. + * + * @param {Array} testData + */ +// eslint-disable-next-line no-unused-vars +async function testKeyframesGraphKeyframesMarker() { + await addTab(URL_ROOT + "doc_multi_keyframes.html"); + await removeAnimatedElementsExcept( + KEYFRAMES_TEST_DATA.map(t => `.${t.targetClass}`) + ); + const { animationInspector, panel } = await openAnimationInspector(); + + for (const { properties, targetClass } of KEYFRAMES_TEST_DATA) { + info(`Checking keyframe marker for ${targetClass}`); + const onDetailRendered = animationInspector.once( + "animation-keyframes-rendered" + ); + await clickOnAnimationByTargetSelector( + animationInspector, + panel, + `.${targetClass}` + ); + await onDetailRendered; + + for (const { name, expectedValues } of properties) { + const testTarget = `${name} in ${targetClass}`; + info(`Checking keyframe marker for ${testTarget}`); + info(`Checking keyframe marker existence for ${testTarget}`); + const markerEls = panel.querySelectorAll( + `.${name} .keyframe-marker-item` + ); + is( + markerEls.length, + expectedValues.length, + `Count of keyframe marker elements of ${testTarget} ` + + `should be ${expectedValues.length}` + ); + + for (let i = 0; i < expectedValues.length; i++) { + const hintTarget = `.keyframe-marker-item[${i}] of ${testTarget}`; + + info(`Checking ${hintTarget}`); + const markerEl = markerEls[i]; + const expectedValue = expectedValues[i]; + + info(`Checking title in ${hintTarget}`); + is( + markerEl.getAttribute("title"), + expectedValue.title, + `title in ${hintTarget} should be ${expectedValue.title}` + ); + + info(`Checking marginInlineStart style in ${hintTarget}`); + is( + markerEl.style.marginInlineStart, + expectedValue.marginInlineStart, + `marginInlineStart in ${hintTarget} should be ` + + `${expectedValue.marginInlineStart}` + ); + } + } + } +} diff --git a/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js b/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js new file mode 100644 index 0000000000..8516e96fa3 --- /dev/null +++ b/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +/** + * Test for computed timing path on summary graph using given test data. + * @param {Array} testData + */ +// eslint-disable-next-line no-unused-vars +async function testComputedTimingPath(testData) { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { + expectedDelayPath, + expectedEndDelayPath, + expectedForwardsPath, + expectedIterationPathList, + isInfinity, + targetClass, + } of testData) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking computed timing path existance for ${targetClass}`); + const computedTimingPathEl = animationItemEl.querySelector( + ".animation-computed-timing-path" + ); + ok( + computedTimingPathEl, + "The computed timing path element should be in each animation item element" + ); + + info(`Checking delay path for ${targetClass}`); + const delayPathEl = computedTimingPathEl.querySelector( + ".animation-delay-path" + ); + + if (expectedDelayPath) { + ok(delayPathEl, "delay path should be existance"); + assertPathSegments(delayPathEl, true, expectedDelayPath); + } else { + ok(!delayPathEl, "delay path should not be existance"); + } + + info(`Checking iteration path list for ${targetClass}`); + const iterationPathEls = computedTimingPathEl.querySelectorAll( + ".animation-iteration-path" + ); + is( + iterationPathEls.length, + expectedIterationPathList.length, + `Number of iteration path should be ${expectedIterationPathList.length}` + ); + + for (const [j, iterationPathEl] of iterationPathEls.entries()) { + assertPathSegments(iterationPathEl, true, expectedIterationPathList[j]); + + info(`Checking infinity ${targetClass}`); + if (isInfinity && j >= 1) { + ok( + iterationPathEl.classList.contains("infinity"), + "iteration path should have 'infinity' class" + ); + } else { + ok( + !iterationPathEl.classList.contains("infinity"), + "iteration path should not have 'infinity' class" + ); + } + } + + info(`Checking endDelay path for ${targetClass}`); + const endDelayPathEl = computedTimingPathEl.querySelector( + ".animation-enddelay-path" + ); + + if (expectedEndDelayPath) { + ok(endDelayPathEl, "endDelay path should be existance"); + assertPathSegments(endDelayPathEl, true, expectedEndDelayPath); + } else { + ok(!endDelayPathEl, "endDelay path should not be existance"); + } + + info(`Checking forwards fill path for ${targetClass}`); + const forwardsPathEl = computedTimingPathEl.querySelector( + ".animation-fill-forwards-path" + ); + + if (expectedForwardsPath) { + ok(forwardsPathEl, "forwards path should be existance"); + assertPathSegments(forwardsPathEl, true, expectedForwardsPath); + } else { + ok(!forwardsPathEl, "forwards path should not be existance"); + } + } +} diff --git a/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js b/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js new file mode 100644 index 0000000000..fd601821b6 --- /dev/null +++ b/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +// Test for following DelaySign component works. +// * element existance +// * marginInlineStart position +// * width +// * additinal class + +const TEST_DATA = [ + { + targetClass: "delay-positive", + expectedResult: { + marginInlineStart: "25%", + width: "25%", + }, + }, + { + targetClass: "delay-negative", + expectedResult: { + additionalClass: "negative", + marginInlineStart: "0%", + width: "25%", + }, + }, + { + targetClass: "fill-backwards-with-delay-iterationstart", + expectedResult: { + additionalClass: "fill", + marginInlineStart: "25%", + width: "25%", + }, + }, + { + targetClass: "fill-both", + }, + { + targetClass: "fill-both-width-delay-iterationstart", + expectedResult: { + additionalClass: "fill", + marginInlineStart: "25%", + width: "25%", + }, + }, + { + targetClass: "keyframes-easing-step", + }, +]; + +// eslint-disable-next-line no-unused-vars +async function testSummaryGraphDelaySign() { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedResult } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking delay sign existance for ${targetClass}`); + const delaySignEl = animationItemEl.querySelector(".animation-delay-sign"); + + if (expectedResult) { + ok( + delaySignEl, + "The delay sign element should be in animation item element" + ); + + function assertExpected(key) { + const actual = parseFloat(delaySignEl.style[key]); + const expected = parseFloat(expectedResult[key]); + ok( + Math.abs(actual - expected) < 0.01, + `${key} should be ${expected} (got ${actual})` + ); + } + + assertExpected(`marginInlineStart`); + assertExpected(`width`); + + if (expectedResult.additionalClass) { + ok( + delaySignEl.classList.contains(expectedResult.additionalClass), + `delay sign element should have ${expectedResult.additionalClass} class` + ); + } else { + ok( + !delaySignEl.classList.contains(expectedResult.additionalClass), + "delay sign element should not have " + + `${expectedResult.additionalClass} class` + ); + } + } else { + ok( + !delaySignEl, + "The delay sign element should not be in animation item element" + ); + } + } +} diff --git a/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js b/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js new file mode 100644 index 0000000000..f87a554420 --- /dev/null +++ b/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +// Test for following EndDelaySign component works. +// * element existance +// * marginInlineStart position +// * width +// * additinal class + +const TEST_DATA = [ + { + targetClass: "enddelay-positive", + expectedResult: { + marginInlineStart: "75%", + width: "25%", + }, + }, + { + targetClass: "enddelay-negative", + expectedResult: { + additionalClass: "negative", + marginInlineStart: "50%", + width: "25%", + }, + }, + { + targetClass: "enddelay-with-fill-forwards", + expectedResult: { + additionalClass: "fill", + marginInlineStart: "75%", + width: "25%", + }, + }, + { + targetClass: "enddelay-with-iterations-infinity", + }, + { + targetClass: "delay-negative", + }, +]; + +// eslint-disable-next-line no-unused-vars +async function testSummaryGraphEndDelaySign() { + await addTab(URL_ROOT + "doc_multi_timings.html"); + await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`)); + const { panel } = await openAnimationInspector(); + + for (const { targetClass, expectedResult } of TEST_DATA) { + const animationItemEl = await findAnimationItemByTargetSelector( + panel, + `.${targetClass}` + ); + + info(`Checking endDelay sign existance for ${targetClass}`); + const endDelaySignEl = animationItemEl.querySelector( + ".animation-end-delay-sign" + ); + + if (expectedResult) { + ok( + endDelaySignEl, + "The endDelay sign element should be in animation item element" + ); + + function assertExpected(key) { + const actual = parseFloat(endDelaySignEl.style[key]); + const expected = parseFloat(expectedResult[key]); + ok( + Math.abs(actual - expected) < 0.01, + `${key} should be ${expected} (got ${actual})` + ); + } + + assertExpected(`marginInlineStart`); + assertExpected(`width`); + + if (expectedResult.additionalClass) { + ok( + endDelaySignEl.classList.contains(expectedResult.additionalClass), + `endDelay sign element should have ${expectedResult.additionalClass} class` + ); + } else { + ok( + !endDelaySignEl.classList.contains(expectedResult.additionalClass), + "endDelay sign element should not have " + + `${expectedResult.additionalClass} class` + ); + } + } else { + ok( + !endDelaySignEl, + "The endDelay sign element should not be in animation item element" + ); + } + } +} diff --git a/devtools/client/inspector/animation/utils/graph-helper.js b/devtools/client/inspector/animation/utils/graph-helper.js new file mode 100644 index 0000000000..cca2713254 --- /dev/null +++ b/devtools/client/inspector/animation/utils/graph-helper.js @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start +// and end bounds when dividing duration in createPathSegments. +const BOUND_EXCLUDING_TIME = 0.001; +// We define default graph height since if the height of viewport in SVG is +// too small (e.g. 1), vector-effect may not be able to calculate correctly. +const DEFAULT_GRAPH_HEIGHT = 100; +// Default animation duration for keyframes graph. +const DEFAULT_KEYFRAMES_GRAPH_DURATION = 1000; +// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1. +const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1; +// In the createPathSegments function, an animation duration is divided by +// DEFAULT_DURATION_RESOLUTION in order to draw the way the animation progresses. +// But depending on the timing-function, we may be not able to make the graph +// smoothly progress if this resolution is not high enough. +// So, if the difference of animation progress between 2 divisions is more than +// DEFAULT_MIN_PROGRESS_THRESHOLD * DEFAULT_GRAPH_HEIGHT, then createPathSegments +// re-divides by DEFAULT_DURATION_RESOLUTION. +// DEFAULT_DURATION_RESOLUTION shoud be integer and more than 2. +const DEFAULT_DURATION_RESOLUTION = 4; +// Stroke width for easing hint. +const DEFAULT_EASING_HINT_STROKE_WIDTH = 5; + +/** + * The helper class for creating summary graph. + */ +class SummaryGraphHelper { + /** + * Constructor. + * + * @param {Object} state + * State of animation. + * @param {Array} keyframes + * Array of keyframe. + * @param {Number} totalDuration + * Total displayable duration. + * @param {Number} minSegmentDuration + * Minimum segment duration. + * @param {Function} getValueFunc + * Which returns graph value of given time. + * The function should return a number value between 0 - 1. + * e.g. time => { return 1.0 }; + * @param {Function} toPathStringFunc + * Which returns a path string for 'd' attribute for <path> from given segments. + */ + constructor( + state, + keyframes, + totalDuration, + minSegmentDuration, + getValueFunc, + toPathStringFunc + ) { + this.totalDuration = totalDuration; + this.minSegmentDuration = minSegmentDuration; + this.minProgressThreshold = + getPreferredProgressThreshold(state, keyframes) * DEFAULT_GRAPH_HEIGHT; + this.durationResolution = getPreferredDurationResolution(keyframes); + this.getValue = getValueFunc; + this.toPathString = toPathStringFunc; + + this.getSegment = this.getSegment.bind(this); + } + + /** + * Create the path segments from given parameters. + * + * @param {Number} startTime + * Starting time of animation. + * @param {Number} endTime + * Ending time of animation. + * @return {Array} + * Array of path segment. + * e.g.[{x: {Number} time, y: {Number} progress}, ...] + */ + createPathSegments(startTime, endTime) { + return createPathSegments( + startTime, + endTime, + this.minSegmentDuration, + this.minProgressThreshold, + this.durationResolution, + this.getSegment + ); + } + + /** + * Return a coordinate as a graph segment at given time. + * + * @param {Number} time + * @return {Object} + * { x: Number, y: Number } + */ + getSegment(time) { + const value = this.getValue(time); + return { x: time, y: value * DEFAULT_GRAPH_HEIGHT }; + } +} + +/** + * Create the path segments from given parameters. + * + * @param {Number} startTime + * Starting time of animation. + * @param {Number} endTime + * Ending time of animation. + * @param {Number} minSegmentDuration + * Minimum segment duration. + * @param {Number} minProgressThreshold + * Minimum progress threshold. + * @param {Number} resolution + * Duration resolution for first time. + * @param {Function} getSegment + * A function that calculate the graph segment. + * @return {Array} + * Array of path segment. + * e.g.[{x: {Number} time, y: {Number} progress}, ...] + */ +function createPathSegments( + startTime, + endTime, + minSegmentDuration, + minProgressThreshold, + resolution, + getSegment +) { + // If the duration is too short, early return. + if (endTime - startTime < minSegmentDuration) { + return [getSegment(startTime), getSegment(endTime)]; + } + + // Otherwise, start creating segments. + let pathSegments = []; + + // Append the segment for the startTime position. + const startTimeSegment = getSegment(startTime); + pathSegments.push(startTimeSegment); + let previousSegment = startTimeSegment; + + // Split the duration in equal intervals, and iterate over them. + // See the definition of DEFAULT_DURATION_RESOLUTION for more information about this. + const interval = (endTime - startTime) / resolution; + for (let index = 1; index <= resolution; index++) { + // Create a segment for this interval. + const currentSegment = getSegment(startTime + index * interval); + + // If the distance between the Y coordinate (the animation's progress) of + // the previous segment and the Y coordinate of the current segment is too + // large, then recurse with a smaller duration to get more details + // in the graph. + if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) { + // Divide the current interval (excluding start and end bounds + // by adding/subtracting BOUND_EXCLUDING_TIME). + const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME; + const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME; + const segments = createPathSegments( + nextStartTime, + nextEndTime, + minSegmentDuration, + minProgressThreshold, + DEFAULT_DURATION_RESOLUTION, + getSegment + ); + pathSegments = pathSegments.concat(segments); + } + + pathSegments.push(currentSegment); + previousSegment = currentSegment; + } + + return pathSegments; +} + +/** + * Create a function which is used as parameter (toPathStringFunc) in constructor + * of SummaryGraphHelper. + * + * @param {Number} endTime + * end time of animation + * e.g. 200 + * @param {Number} playbackRate + * playback rate of animation + * e.g. -1 + * @return {Function} + */ +function createSummaryGraphPathStringFunction(endTime, playbackRate) { + return segments => { + segments = mapSegmentsToPlaybackRate(segments, endTime, playbackRate); + const firstSegment = segments[0]; + let pathString = `M${firstSegment.x},0 `; + pathString += toPathString(segments); + const lastSegment = segments[segments.length - 1]; + pathString += `L${lastSegment.x},0 Z`; + return pathString; + }; +} + +/** + * Return preferred duration resolution. + * This corresponds to narrow interval keyframe offset. + * + * @param {Array} keyframes + * Array of keyframe. + * @return {Number} + * Preferred duration resolution. + */ +function getPreferredDurationResolution(keyframes) { + if (!keyframes) { + return DEFAULT_DURATION_RESOLUTION; + } + + let durationResolution = DEFAULT_DURATION_RESOLUTION; + let previousOffset = 0; + for (const keyframe of keyframes) { + if (previousOffset && previousOffset != keyframe.offset) { + const interval = keyframe.offset - previousOffset; + durationResolution = Math.max( + durationResolution, + Math.ceil(1 / interval) + ); + } + previousOffset = keyframe.offset; + } + + return durationResolution; +} + +/** + * Return preferred progress threshold to render summary graph. + * + * @param {Object} state + * State of animation. + * @param {Array} keyframes + * Array of keyframe. + * @return {float} + * Preferred threshold. + */ +function getPreferredProgressThreshold(state, keyframes) { + const steps = getStepsCount(state.easing); + const threshold = Math.min(DEFAULT_MIN_PROGRESS_THRESHOLD, 1 / (steps + 1)); + + if (!keyframes) { + return threshold; + } + + return Math.min( + threshold, + getPreferredProgressThresholdByKeyframes(keyframes) + ); +} + +/** + * Return preferred progress threshold by keyframes. + * + * @param {Array} keyframes + * Array of keyframe. + * @return {float} + * Preferred threshold. + */ +function getPreferredProgressThresholdByKeyframes(keyframes) { + let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD; + + for (let i = 0; i < keyframes.length - 1; i++) { + const keyframe = keyframes[i]; + + if (!keyframe.easing) { + continue; + } + + const steps = getStepsCount(keyframe.easing); + + if (steps) { + const nextKeyframe = keyframes[i + 1]; + threshold = Math.min( + threshold, + (1 / (steps + 1)) * (nextKeyframe.offset - keyframe.offset) + ); + } + } + + return threshold; +} + +function getStepsCount(easing) { + const stepsFunction = easing.match(/(steps)\((\d+)/); + return stepsFunction ? parseInt(stepsFunction[2], 10) : 0; +} + +function mapSegmentsToPlaybackRate(segments, endTime, playbackRate) { + if (playbackRate > 0) { + return segments; + } + + return segments.map(segment => { + segment.x = endTime - segment.x; + return segment; + }); +} + +/** + * Return path string for 'd' attribute for <path> from given segments. + * + * @param {Array} segments + * e.g. [{ x: 100, y: 0 }, { x: 200, y: 1 }] + * @return {String} + * Path string. + * e.g. "L100,0 L200,1" + */ +function toPathString(segments) { + let pathString = ""; + segments.forEach(segment => { + pathString += `L${segment.x},${segment.y} `; + }); + return pathString; +} + +exports.createPathSegments = createPathSegments; +exports.createSummaryGraphPathStringFunction = + createSummaryGraphPathStringFunction; +exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION; +exports.DEFAULT_EASING_HINT_STROKE_WIDTH = DEFAULT_EASING_HINT_STROKE_WIDTH; +exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT; +exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION; +exports.getPreferredProgressThresholdByKeyframes = + getPreferredProgressThresholdByKeyframes; +exports.SummaryGraphHelper = SummaryGraphHelper; +exports.toPathString = toPathString; diff --git a/devtools/client/inspector/animation/utils/l10n.js b/devtools/client/inspector/animation/utils/l10n.js new file mode 100644 index 0000000000..6fffa98b65 --- /dev/null +++ b/devtools/client/inspector/animation/utils/l10n.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const L10N = new LocalizationHelper( + "devtools/client/locales/animationinspector.properties" +); +const INSPECTOR_L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +/** + * Get a formatted title for this animation. This will be either: + * "%S", "%S : CSS Transition", "%S : CSS Animation", + * "%S : Script Animation", or "Script Animation", depending + * if the server provides the type, what type it is and if the animation + * has a name. + * + * @param {Object} state + */ +function getFormattedTitle(state) { + // Older servers don't send a type, and only know about + // CSSAnimations and CSSTransitions, so it's safe to use + // just the name. + if (!state.type) { + return state.name; + } + + // Script-generated animations may not have a name. + if (state.type === "scriptanimation" && !state.name) { + return L10N.getStr("timeline.scriptanimation.unnamedLabel"); + } + + return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name); +} + +module.exports = { + getFormatStr: (...args) => L10N.getFormatStr(...args), + getFormattedTitle, + getInspectorStr: (...args) => INSPECTOR_L10N.getStr(...args), + getStr: (...args) => L10N.getStr(...args), + numberWithDecimals: (...args) => L10N.numberWithDecimals(...args), +}; diff --git a/devtools/client/inspector/animation/utils/moz.build b/devtools/client/inspector/animation/utils/moz.build new file mode 100644 index 0000000000..ae73627a29 --- /dev/null +++ b/devtools/client/inspector/animation/utils/moz.build @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "graph-helper.js", + "l10n.js", + "timescale.js", + "utils.js", +) diff --git a/devtools/client/inspector/animation/utils/timescale.js b/devtools/client/inspector/animation/utils/timescale.js new file mode 100644 index 0000000000..77297f748c --- /dev/null +++ b/devtools/client/inspector/animation/utils/timescale.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + getFormatStr, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +// If total duration for all animations is eqaul to or less than +// TIME_FORMAT_MAX_DURATION_IN_MS, the text which expresses time is in milliseconds, +// and seconds otherwise. Use in formatTime function. +const TIME_FORMAT_MAX_DURATION_IN_MS = 4000; + +/** + * TimeScale object holds the total duration, start time and end time and zero position + * time information for all animations which should be displayed, and is used to calculate + * the displayed area for each animation. + */ +class TimeScale { + constructor(animations) { + let resultCurrentTime = -Number.MAX_VALUE; + let resultMinStartTime = Infinity; + let resultMaxEndTime = 0; + let resultZeroPositionTime = 0; + + for (const animation of animations) { + const { + currentTime, + currentTimeAtCreated, + delay, + endTime, + startTimeAtCreated, + } = animation.state.absoluteValues; + let { startTime } = animation.state.absoluteValues; + + const negativeDelay = Math.min(delay, 0); + let zeroPositionTime = 0; + + // To shift the zero position time is the following two patterns. + // * Animation has negative current time which is smaller than negative delay. + // * Animation has negative delay. + // Furthermore, we should override the zero position time if we will need to + // expand the duration due to this negative current time or negative delay of + // this target animation. + if (currentTimeAtCreated < negativeDelay) { + startTime = startTimeAtCreated; + zeroPositionTime = Math.abs(currentTimeAtCreated); + } else if (negativeDelay < 0) { + zeroPositionTime = Math.abs(negativeDelay); + } + + if (startTime < resultMinStartTime) { + resultMinStartTime = startTime; + // Override the previous calculated zero position only if the duration will be + // expanded. + resultZeroPositionTime = zeroPositionTime; + } else { + resultZeroPositionTime = Math.max( + resultZeroPositionTime, + zeroPositionTime + ); + } + + resultMaxEndTime = Math.max(resultMaxEndTime, endTime); + resultCurrentTime = Math.max(resultCurrentTime, currentTime); + } + + this.minStartTime = resultMinStartTime; + this.maxEndTime = resultMaxEndTime; + this.currentTime = resultCurrentTime; + this.zeroPositionTime = resultZeroPositionTime; + } + + /** + * Convert a distance in % to a time, in the current time scale. The time + * will be relative to the zero position time. + * i.e., If zeroPositionTime will be negative and specified time is shorter + * than the absolute value of zero position time, relative time will be + * negative time. + * + * @param {Number} distance + * @return {Number} + */ + distanceToRelativeTime(distance) { + return (this.getDuration() * distance) / 100 - this.zeroPositionTime; + } + + /** + * Depending on the time scale, format the given time as milliseconds or + * seconds. + * + * @param {Number} time + * @return {String} The formatted time string. + */ + formatTime(time) { + // Ignore negative zero + if (Math.abs(time) < 1 / 1000) { + time = 0.0; + } + + // Format in milliseconds if the total duration is short enough. + if (this.getDuration() <= TIME_FORMAT_MAX_DURATION_IN_MS) { + return getFormatStr("timeline.timeGraduationLabel", time.toFixed(0)); + } + + // Otherwise format in seconds. + return getFormatStr("player.timeLabel", (time / 1000).toFixed(1)); + } + + /** + * Return entire animations duration. + * + * @return {Number} duration + */ + getDuration() { + return this.maxEndTime - this.minStartTime; + } + + /** + * Return current time of this time scale represents. + * + * @return {Number} + */ + getCurrentTime() { + return this.currentTime - this.minStartTime; + } + + /** + * Return end time of given animation. + * This time does not include playbackRate and cratedTime. + * Also, if the animation has infinite iterations, this returns Infinity. + * + * @param {Object} animation + * @return {Numbber} end time + */ + getEndTime({ state }) { + return state.iterationCount + ? state.delay + state.duration * state.iterationCount + state.endDelay + : Infinity; + } +} + +module.exports = TimeScale; diff --git a/devtools/client/inspector/animation/utils/utils.js b/devtools/client/inspector/animation/utils/utils.js new file mode 100644 index 0000000000..9040c27213 --- /dev/null +++ b/devtools/client/inspector/animation/utils/utils.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// The maximum number of times we can loop before we find the optimal time interval in the +// timeline graph. +const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100; +// Time graduations should be multiple of one of these number. +const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5]; + +/** + * Find the optimal interval between time graduations in the animation timeline + * graph based on a minimum time interval. + * + * @param {Number} minTimeInterval + * Minimum time in ms in one interval + * @return {Number} The optimal interval time in ms + */ +function findOptimalTimeInterval(minTimeInterval) { + if (!minTimeInterval) { + return 0; + } + + let numIters = 0; + let multiplier = 1; + let interval; + + while (true) { + for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) { + interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier; + + if (minTimeInterval <= interval) { + return interval; + } + } + + if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) { + return interval; + } + + multiplier *= 10; + } +} + +/** + * Check whether or not the given list of animations has an iteration count of infinite. + * + * @param {Array} animations. + * @return {Boolean} true if there is an animation in the list of animations + * whose animation iteration count is infinite. + */ +function hasAnimationIterationCountInfinite(animations) { + return animations.some(({ state }) => !state.iterationCount); +} + +/** + * Check wether the animations are running at least one. + * + * @param {Array} animations. + * @return {Boolean} true: running + */ +function hasRunningAnimation(animations) { + return animations.some(({ state }) => state.playState === "running"); +} + +exports.findOptimalTimeInterval = findOptimalTimeInterval; +exports.hasAnimationIterationCountInfinite = hasAnimationIterationCountInfinite; +exports.hasRunningAnimation = hasRunningAnimation; diff --git a/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js new file mode 100644 index 0000000000..9033dc46ae --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This module exports thunks. + * Thunks are functions that can be dispatched to the Inspector Redux store. + * + * These functions receive one object with options that contains: + * - dispatch() => function to dispatch Redux actions to the store + * - getState() => function to get the current state of the entire Inspector Redux store + * - inspector => object instance of Inspector client + * + * They provide a shortcut for React components to invoke the box model highlighter + * without having to know where the highlighter exists. + */ + +module.exports = { + /** + * Show the box model highlighter for the currently selected node front. + * The selected node is obtained from the Selection instance on the Inspector. + * + * @param {Object} options + * Optional configuration options passed to the box model highlighter + */ + highlightSelectedNode(options = {}) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + const { nodeFront } = inspector.selection; + if (!nodeFront) { + return; + } + + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }; + }, + + /** + * Show the box model highlighter for the given node front. + * + * @param {NodeFront} nodeFront + * Node that should be highlighted. + * @param {Object} options + * Optional configuration options passed to the box model highlighter + */ + highlightNode(nodeFront, options = {}) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }; + }, + + /** + * Hide the box model highlighter for any highlighted node. + */ + unhighlightNode() { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + }; + }, +}; diff --git a/devtools/client/inspector/boxmodel/actions/box-model.js b/devtools/client/inspector/boxmodel/actions/box-model.js new file mode 100644 index 0000000000..322cf6cb00 --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/box-model.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_GEOMETRY_EDITOR_ENABLED, + UPDATE_LAYOUT, + UPDATE_OFFSET_PARENT, +} = require("resource://devtools/client/inspector/boxmodel/actions/index.js"); + +module.exports = { + /** + * Updates the geometry editor's enabled state. + * + * @param {Boolean} enabled + * Whether or not the geometry editor is enabled or not. + */ + updateGeometryEditorEnabled(enabled) { + return { + type: UPDATE_GEOMETRY_EDITOR_ENABLED, + enabled, + }; + }, + + /** + * Updates the layout state with the new layout properties. + */ + updateLayout(layout) { + return { + type: UPDATE_LAYOUT, + layout, + }; + }, + + /** + * Updates the offset parent state with the new DOM node. + */ + updateOffsetParent(offsetParent) { + return { + type: UPDATE_OFFSET_PARENT, + offsetParent, + }; + }, +}; diff --git a/devtools/client/inspector/boxmodel/actions/index.js b/devtools/client/inspector/boxmodel/actions/index.js new file mode 100644 index 0000000000..813b4e9e11 --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/index.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Updates the geometry editor's enabled state. + "UPDATE_GEOMETRY_EDITOR_ENABLED", + + // Updates the layout state with the latest layout properties. + "UPDATE_LAYOUT", + + // Updates the offset parent state with the new DOM node. + "UPDATE_OFFSET_PARENT", + ], + module.exports +); diff --git a/devtools/client/inspector/boxmodel/actions/moz.build b/devtools/client/inspector/boxmodel/actions/moz.build new file mode 100644 index 0000000000..7ee681b35c --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "box-model-highlighter.js", + "box-model.js", + "index.js", +) diff --git a/devtools/client/inspector/boxmodel/box-model.js b/devtools/client/inspector/boxmodel/box-model.js new file mode 100644 index 0000000000..c6870ef016 --- /dev/null +++ b/devtools/client/inspector/boxmodel/box-model.js @@ -0,0 +1,446 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const boxModelReducer = require("resource://devtools/client/inspector/boxmodel/reducers/box-model.js"); +const { + updateGeometryEditorEnabled, + updateLayout, + updateOffsetParent, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model.js"); + +loader.lazyRequireGetter( + this, + "EditingSession", + "resource://devtools/client/inspector/boxmodel/utils/editing-session.js" +); +loader.lazyRequireGetter( + this, + "InplaceEditor", + "resource://devtools/client/shared/inplace-editor.js", + true +); +loader.lazyRequireGetter( + this, + "RulePreviewTooltip", + "resource://devtools/client/shared/widgets/tooltip/RulePreviewTooltip.js" +); + +const NUMERIC = /^-?[\d\.]+$/; + +/** + * A singleton instance of the box model controllers. + * + * @param {Inspector} inspector + * An instance of the Inspector currently loaded in the toolbox. + * @param {Window} window + * The document window of the toolbox. + */ +function BoxModel(inspector, window) { + this.document = window.document; + this.inspector = inspector; + this.store = inspector.store; + + this.store.injectReducer("boxModel", boxModelReducer); + + this.updateBoxModel = this.updateBoxModel.bind(this); + + this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this); + this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this); + this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this); + this.onShowRulePreviewTooltip = this.onShowRulePreviewTooltip.bind(this); + this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this); + + this.inspector.selection.on("new-node-front", this.onNewSelection); + this.inspector.sidebar.on("select", this.onSidebarSelect); +} + +BoxModel.prototype = { + /** + * Destruction function called when the inspector is destroyed. Removes event listeners + * and cleans up references. + */ + destroy() { + this.inspector.selection.off("new-node-front", this.onNewSelection); + this.inspector.sidebar.off("select", this.onSidebarSelect); + + if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + + if (this._tooltip) { + this._tooltip.destroy(); + } + + this.untrackReflows(); + + this.elementRules = null; + this._highlighters = null; + this._tooltip = null; + this.document = null; + this.inspector = null; + }, + + get highlighters() { + if (!this._highlighters) { + // highlighters is a lazy getter in the inspector. + this._highlighters = this.inspector.highlighters; + } + + return this._highlighters; + }, + + get rulePreviewTooltip() { + if (!this._tooltip) { + this._tooltip = new RulePreviewTooltip(this.inspector.toolbox.doc); + } + + return this._tooltip; + }, + + /** + * Returns an object containing the box model's handler functions used in the box + * model's React component props. + */ + getComponentProps() { + return { + onShowBoxModelEditor: this.onShowBoxModelEditor, + onShowRulePreviewTooltip: this.onShowRulePreviewTooltip, + onToggleGeometryEditor: this.onToggleGeometryEditor, + }; + }, + + /** + * Returns true if the layout panel is visible, and false otherwise. + */ + isPanelVisible() { + return ( + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() === "layoutview" + ); + }, + + /** + * Returns true if the layout panel is visible and the current element is valid to + * be displayed in the view. + */ + isPanelVisibleAndNodeValid() { + return ( + this.isPanelVisible() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ); + }, + + /** + * Starts listening to reflows in the current tab. + */ + trackReflows() { + this.inspector.on("reflow-in-selected-target", this.updateBoxModel); + }, + + /** + * Stops listening to reflows in the current tab. + */ + untrackReflows() { + this.inspector.off("reflow-in-selected-target", this.updateBoxModel); + }, + + /** + * Updates the box model panel by dispatching the new layout data. + * + * @param {String} reason + * Optional string describing the reason why the boxmodel is updated. + */ + updateBoxModel(reason) { + this._updateReasons = this._updateReasons || []; + if (reason) { + this._updateReasons.push(reason); + } + + const lastRequest = async function () { + if ( + !this.inspector || + !this.isPanelVisible() || + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode() + ) { + return null; + } + + const { nodeFront } = this.inspector.selection; + const inspectorFront = this.getCurrentInspectorFront(); + const { pageStyle } = inspectorFront; + + let layout = await pageStyle.getLayout(nodeFront, { + autoMargins: true, + }); + + const styleEntries = await pageStyle.getApplied(nodeFront, { + // We don't need styles applied to pseudo elements of the current node. + skipPseudo: true, + }); + this.elementRules = styleEntries.map(e => e.rule); + + // Update the layout properties with whether or not the element's position is + // editable with the geometry editor. + const isPositionEditable = await pageStyle.isPositionEditable(nodeFront); + + layout = Object.assign({}, layout, { + isPositionEditable, + }); + + // Update the redux store with the latest offset parent DOM node + const offsetParent = await inspectorFront.walker.getOffsetParent( + nodeFront + ); + this.store.dispatch(updateOffsetParent(offsetParent)); + + // Update the redux store with the latest layout properties and update the box + // model view. + this.store.dispatch(updateLayout(layout)); + + // If a subsequent request has been made, wait for that one instead. + if (this._lastRequest != lastRequest) { + return this._lastRequest; + } + + this.inspector.emit("boxmodel-view-updated", this._updateReasons); + + this._lastRequest = null; + this._updateReasons = []; + + return null; + } + .bind(this)() + .catch(error => { + // If we failed because we were being destroyed while waiting for a request, ignore. + if (this.document) { + console.error(error); + } + }); + + this._lastRequest = lastRequest; + }, + + /** + * Hides the geometry editor and updates the box moodel store with the new + * geometry editor enabled state. + */ + onHideGeometryEditor() { + this.highlighters.hideGeometryEditor(); + this.store.dispatch(updateGeometryEditorEnabled(false)); + + if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + }, + + /** + * Handler function that re-shows the geometry editor for an element that already + * had the geometry editor enabled. This handler function is called on a "leave" event + * on the markup view. + */ + onMarkupViewLeave() { + const state = this.store.getState(); + const enabled = state.boxModel.geometryEditorEnabled; + + if (!enabled) { + return; + } + + const nodeFront = this.inspector.selection.nodeFront; + this.highlighters.showGeometryEditor(nodeFront); + }, + + /** + * Handler function that temporarily hides the geomery editor when the + * markup view has a "node-hover" event. + */ + onMarkupViewNodeHover() { + this.highlighters.hideGeometryEditor(); + }, + + /** + * Selection 'new-node-front' event handler. + */ + onNewSelection() { + if (!this.isPanelVisibleAndNodeValid()) { + return; + } + + if ( + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ) { + this.trackReflows(); + } + + this.updateBoxModel("new-selection"); + }, + + /** + * Shows the RulePreviewTooltip when a box model editable value is hovered on the + * box model panel. + * + * @param {Element} target + * The target element. + * @param {String} property + * The name of the property. + */ + onShowRulePreviewTooltip(target, property) { + const { highlightProperty } = this.inspector.getPanel("ruleview").view; + const isHighlighted = highlightProperty(property); + + // Only show the tooltip if the property is not highlighted. + // TODO: In the future, use an associated ruleId for toggling the tooltip instead of + // the Boolean returned from highlightProperty. + if (!isHighlighted) { + this.rulePreviewTooltip.show(target); + } + }, + + /** + * Shows the inplace editor when a box model editable value is clicked on the + * box model panel. + * + * @param {DOMNode} element + * The element that was clicked. + * @param {Event} event + * The event object. + * @param {String} property + * The name of the property. + */ + onShowBoxModelEditor(element, event, property) { + const session = new EditingSession({ + inspector: this.inspector, + doc: this.document, + elementRules: this.elementRules, + }); + const initialValue = session.getProperty(property); + + const editor = new InplaceEditor( + { + element, + initial: initialValue, + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: { + name: property, + }, + start: self => { + self.elt.parentNode.classList.add("boxmodel-editing"); + }, + change: value => { + if (NUMERIC.test(value)) { + value += "px"; + } + + const properties = [{ name: property, value }]; + + if (property.substring(0, 7) == "border-") { + const bprop = property.substring(0, property.length - 5) + "style"; + const style = session.getProperty(bprop); + if (!style || style == "none" || style == "hidden") { + properties.push({ name: bprop, value: "solid" }); + } + } + + if (property.substring(0, 9) == "position-") { + properties[0].name = property.substring(9); + } + + session.setProperties(properties).catch(console.error); + }, + done: (value, commit) => { + editor.elt.parentNode.classList.remove("boxmodel-editing"); + if (!commit) { + session.revert().then(() => { + session.destroy(); + }, console.error); + return; + } + + this.updateBoxModel("editable-value-change"); + }, + cssProperties: this.inspector.cssProperties, + }, + event + ); + }, + + /** + * Handler for the inspector sidebar select event. Starts tracking reflows if the + * layout panel is visible. Otherwise, stop tracking reflows. Finally, refresh the box + * model view if it is visible. + */ + onSidebarSelect() { + if (!this.isPanelVisible()) { + this.untrackReflows(); + return; + } + + if ( + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ) { + this.trackReflows(); + } + + this.updateBoxModel(); + }, + + /** + * Toggles on/off the geometry editor for the current element when the geometry editor + * toggle button is clicked. + */ + onToggleGeometryEditor() { + const { markup, selection, toolbox } = this.inspector; + const nodeFront = this.inspector.selection.nodeFront; + const state = this.store.getState(); + const enabled = !state.boxModel.geometryEditorEnabled; + + this.highlighters.toggleGeometryHighlighter(nodeFront); + this.store.dispatch(updateGeometryEditorEnabled(enabled)); + + if (enabled) { + this._geometryEditorEventsAbortController = new AbortController(); + const eventListenersConfig = { + signal: this._geometryEditorEventsAbortController.signal, + }; + // Hide completely the geometry editor if: + // - the picker is clicked + // - or if a new node is selected + toolbox.nodePicker.on( + "picker-started", + this.onHideGeometryEditor, + eventListenersConfig + ); + selection.on( + "new-node-front", + this.onHideGeometryEditor, + eventListenersConfig + ); + // Temporarily hide the geometry editor + markup.on("leave", this.onMarkupViewLeave, eventListenersConfig); + markup.on("node-hover", this.onMarkupViewNodeHover, eventListenersConfig); + } else if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + }, + + getCurrentInspectorFront() { + return this.inspector.selection.nodeFront.inspectorFront; + }, +}; + +module.exports = BoxModel; diff --git a/devtools/client/inspector/boxmodel/components/BoxModel.js b/devtools/client/inspector/boxmodel/components/BoxModel.js new file mode 100644 index 0000000000..23d6fc0ed4 --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModel.js @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const BoxModelInfo = createFactory( + require("resource://devtools/client/inspector/boxmodel/components/BoxModelInfo.js") +); +const BoxModelMain = createFactory( + require("resource://devtools/client/inspector/boxmodel/components/BoxModelMain.js") +); +const BoxModelProperties = createFactory( + require("resource://devtools/client/inspector/boxmodel/components/BoxModelProperties.js") +); + +const Types = require("resource://devtools/client/inspector/boxmodel/types.js"); + +class BoxModel extends PureComponent { + static get propTypes() { + return { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + dispatch: PropTypes.func.isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + onShowRulePreviewTooltip: PropTypes.func.isRequired, + onToggleGeometryEditor: PropTypes.func.isRequired, + showBoxModelProperties: PropTypes.bool.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onKeyDown = this.onKeyDown.bind(this); + } + + onKeyDown(event) { + const { target } = event; + + if (target == this.boxModelContainer) { + this.boxModelMain.onKeyDown(event); + } + } + + render() { + const { + boxModel, + dispatch, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + onToggleGeometryEditor, + setSelectedNode, + showBoxModelProperties, + } = this.props; + + return dom.div( + { + className: "boxmodel-container", + tabIndex: 0, + ref: div => { + this.boxModelContainer = div; + }, + onKeyDown: this.onKeyDown, + }, + BoxModelMain({ + boxModel, + boxModelContainer: this.boxModelContainer, + dispatch, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + ref: boxModelMain => { + this.boxModelMain = boxModelMain; + }, + }), + BoxModelInfo({ + boxModel, + onToggleGeometryEditor, + }), + showBoxModelProperties + ? BoxModelProperties({ + boxModel, + dispatch, + setSelectedNode, + }) + : null + ); + } +} + +module.exports = BoxModel; diff --git a/devtools/client/inspector/boxmodel/components/BoxModelEditable.js b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js new file mode 100644 index 0000000000..c60a3da6be --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + editableItem, +} = require("resource://devtools/client/shared/inplace-editor.js"); + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties"; +const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI); + +const LONG_TEXT_ROTATE_LIMIT = 3; +const HIGHLIGHT_RULE_PREF = Services.prefs.getBoolPref( + "devtools.layout.boxmodel.highlightProperty" +); + +class BoxModelEditable extends PureComponent { + static get propTypes() { + return { + box: PropTypes.string.isRequired, + direction: PropTypes.string, + focusable: PropTypes.bool.isRequired, + level: PropTypes.string, + onShowBoxModelEditor: PropTypes.func.isRequired, + onShowRulePreviewTooltip: PropTypes.func.isRequired, + property: PropTypes.string.isRequired, + textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + }; + } + + constructor(props) { + super(props); + this.onMouseOver = this.onMouseOver.bind(this); + } + + componentDidMount() { + const { property, onShowBoxModelEditor } = this.props; + + editableItem( + { + element: this.boxModelEditable, + }, + (element, event) => { + onShowBoxModelEditor(element, event, property); + } + ); + } + + onMouseOver(event) { + const { onShowRulePreviewTooltip, property } = this.props; + + if (event.shiftKey && HIGHLIGHT_RULE_PREF) { + onShowRulePreviewTooltip(event.target, property); + } + } + + render() { + const { box, direction, focusable, level, property, textContent } = + this.props; + + const rotate = + direction && + (direction == "left" || direction == "right") && + box !== "position" && + textContent.toString().length > LONG_TEXT_ROTATE_LIMIT; + + return dom.p( + { + className: `boxmodel-${box} + ${ + direction + ? " boxmodel-" + direction + : "boxmodel-" + property + } + ${rotate ? " boxmodel-rotate" : ""}`, + id: property + "-id", + }, + dom.span( + { + className: "boxmodel-editable", + "aria-label": SHARED_L10N.getFormatStr( + "boxModelEditable.accessibleLabel", + property, + textContent + ), + "data-box": box, + tabIndex: box === level && focusable ? 0 : -1, + title: property, + onMouseOver: this.onMouseOver, + ref: span => { + this.boxModelEditable = span; + }, + }, + textContent + ) + ); + } +} + +module.exports = BoxModelEditable; diff --git a/devtools/client/inspector/boxmodel/components/BoxModelInfo.js b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js new file mode 100644 index 0000000000..e64faba05a --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +const Types = require("resource://devtools/client/inspector/boxmodel/types.js"); + +const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties"; +const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI); + +const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties"; +const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI); + +class BoxModelInfo extends PureComponent { + static get propTypes() { + return { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + onToggleGeometryEditor: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this); + } + + onToggleGeometryEditor(e) { + this.props.onToggleGeometryEditor(); + } + + render() { + const { boxModel } = this.props; + const { geometryEditorEnabled, layout } = boxModel; + const { height = "-", isPositionEditable, position, width = "-" } = layout; + + let buttonClass = "layout-geometry-editor devtools-button"; + if (geometryEditorEnabled) { + buttonClass += " checked"; + } + + return dom.div( + { + className: "boxmodel-info", + role: "region", + "aria-label": SHARED_L10N.getFormatStr( + "boxModelInfo.accessibleLabel", + width, + height, + position + ), + }, + dom.span( + { className: "boxmodel-element-size" }, + SHARED_L10N.getFormatStr("dimensions", width, height) + ), + dom.section( + { className: "boxmodel-position-group" }, + isPositionEditable + ? dom.button({ + className: buttonClass, + title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"), + onClick: this.onToggleGeometryEditor, + }) + : null, + dom.span({ className: "boxmodel-element-position" }, position) + ) + ); + } +} + +module.exports = BoxModelInfo; diff --git a/devtools/client/inspector/boxmodel/components/BoxModelMain.js b/devtools/client/inspector/boxmodel/components/BoxModelMain.js new file mode 100644 index 0000000000..e7065f797a --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModelMain.js @@ -0,0 +1,774 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +const BoxModelEditable = createFactory( + require("resource://devtools/client/inspector/boxmodel/components/BoxModelEditable.js") +); + +const Types = require("resource://devtools/client/inspector/boxmodel/types.js"); + +const { + highlightSelectedNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties"; +const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI); + +class BoxModelMain extends PureComponent { + static get propTypes() { + return { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + boxModelContainer: PropTypes.object, + dispatch: PropTypes.func.isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + onShowRulePreviewTooltip: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + activeDescendant: null, + focusable: false, + }; + + this.getActiveDescendant = this.getActiveDescendant.bind(this); + this.getBorderOrPaddingValue = this.getBorderOrPaddingValue.bind(this); + this.getContextBox = this.getContextBox.bind(this); + this.getDisplayPosition = this.getDisplayPosition.bind(this); + this.getHeightValue = this.getHeightValue.bind(this); + this.getMarginValue = this.getMarginValue.bind(this); + this.getPositionValue = this.getPositionValue.bind(this); + this.getWidthValue = this.getWidthValue.bind(this); + this.moveFocus = this.moveFocus.bind(this); + this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onLevelClick = this.onLevelClick.bind(this); + this.setActive = this.setActive.bind(this); + } + + componentDidUpdate() { + const displayPosition = this.getDisplayPosition(); + const isContentBox = this.getContextBox(); + + this.layouts = { + position: new Map([ + [KeyCodes.DOM_VK_ESCAPE, this.positionLayout], + [KeyCodes.DOM_VK_DOWN, this.marginLayout], + [KeyCodes.DOM_VK_RETURN, this.positionEditable], + [KeyCodes.DOM_VK_UP, null], + ["click", this.positionLayout], + ]), + margin: new Map([ + [KeyCodes.DOM_VK_ESCAPE, this.marginLayout], + [KeyCodes.DOM_VK_DOWN, this.borderLayout], + [KeyCodes.DOM_VK_RETURN, this.marginEditable], + [KeyCodes.DOM_VK_UP, displayPosition ? this.positionLayout : null], + ["click", this.marginLayout], + ]), + border: new Map([ + [KeyCodes.DOM_VK_ESCAPE, this.borderLayout], + [KeyCodes.DOM_VK_DOWN, this.paddingLayout], + [KeyCodes.DOM_VK_RETURN, this.borderEditable], + [KeyCodes.DOM_VK_UP, this.marginLayout], + ["click", this.borderLayout], + ]), + padding: new Map([ + [KeyCodes.DOM_VK_ESCAPE, this.paddingLayout], + [KeyCodes.DOM_VK_DOWN, isContentBox ? this.contentLayout : null], + [KeyCodes.DOM_VK_RETURN, this.paddingEditable], + [KeyCodes.DOM_VK_UP, this.borderLayout], + ["click", this.paddingLayout], + ]), + content: new Map([ + [KeyCodes.DOM_VK_ESCAPE, this.contentLayout], + [KeyCodes.DOM_VK_DOWN, null], + [KeyCodes.DOM_VK_RETURN, this.contentEditable], + [KeyCodes.DOM_VK_UP, this.paddingLayout], + ["click", this.contentLayout], + ]), + }; + } + + getActiveDescendant() { + let { activeDescendant } = this.state; + + if (!activeDescendant) { + const displayPosition = this.getDisplayPosition(); + const nextLayout = displayPosition + ? this.positionLayout + : this.marginLayout; + activeDescendant = nextLayout.getAttribute("data-box"); + this.setActive(nextLayout); + } + + return activeDescendant; + } + + getBorderOrPaddingValue(property) { + const { layout } = this.props.boxModel; + return layout[property] ? parseFloat(layout[property]) : "-"; + } + + /** + * Returns true if the layout box sizing is context box and false otherwise. + */ + getContextBox() { + const { layout } = this.props.boxModel; + return layout["box-sizing"] == "content-box"; + } + + /** + * Returns true if the position is displayed and false otherwise. + */ + getDisplayPosition() { + const { layout } = this.props.boxModel; + return layout.position && layout.position != "static"; + } + + getHeightValue(property) { + if (property == undefined) { + return "-"; + } + + const { layout } = this.props.boxModel; + + property -= + parseFloat(layout["border-top-width"]) + + parseFloat(layout["border-bottom-width"]) + + parseFloat(layout["padding-top"]) + + parseFloat(layout["padding-bottom"]); + property = parseFloat(property.toPrecision(6)); + + return property; + } + + getMarginValue(property, direction) { + const { layout } = this.props.boxModel; + const autoMargins = layout.autoMargins || {}; + let value = "-"; + + if (direction in autoMargins) { + value = autoMargins[direction]; + } else if (layout[property]) { + const parsedValue = parseFloat(layout[property]); + + if (Number.isNaN(parsedValue)) { + // Not a number. We use the raw string. + // Useful for pseudo-elements with auto margins since they + // don't appear in autoMargins. + value = layout[property]; + } else { + value = parsedValue; + } + } + + return value; + } + + getPositionValue(property) { + const { layout } = this.props.boxModel; + let value = "-"; + + if (!layout[property]) { + return value; + } + + const parsedValue = parseFloat(layout[property]); + + if (Number.isNaN(parsedValue)) { + // Not a number. We use the raw string. + value = layout[property]; + } else { + value = parsedValue; + } + + return value; + } + + getWidthValue(property) { + if (property == undefined) { + return "-"; + } + + const { layout } = this.props.boxModel; + + property -= + parseFloat(layout["border-left-width"]) + + parseFloat(layout["border-right-width"]) + + parseFloat(layout["padding-left"]) + + parseFloat(layout["padding-right"]); + property = parseFloat(property.toPrecision(6)); + + return property; + } + + /** + * Move the focus to the next/previous editable element of the current layout. + * + * @param {Element} target + * Node to be observed + * @param {Boolean} shiftKey + * Determines if shiftKey was pressed + */ + moveFocus({ target, shiftKey }) { + const editBoxes = [ + ...this.positionLayout.querySelectorAll("[data-box].boxmodel-editable"), + ]; + const editingMode = target.tagName === "input"; + // target.nextSibling is input field + let position = editingMode + ? editBoxes.indexOf(target.nextSibling) + : editBoxes.indexOf(target); + + if (position === editBoxes.length - 1 && !shiftKey) { + position = 0; + } else if (position === 0 && shiftKey) { + position = editBoxes.length - 1; + } else { + shiftKey ? position-- : position++; + } + + const editBox = editBoxes[position]; + this.setActive(editBox); + editBox.focus(); + + if (editingMode) { + editBox.click(); + } + } + + /** + * Active level set to current layout. + * + * @param {Element} nextLayout + * Element of next layout that user has navigated to + */ + setActive(nextLayout) { + const { boxModelContainer } = this.props; + + // We set this attribute for testing purposes. + if (boxModelContainer) { + boxModelContainer.dataset.activeDescendantClassName = + nextLayout.className; + } + + this.setState({ + activeDescendant: nextLayout.getAttribute("data-box"), + }); + } + + onHighlightMouseOver(event) { + let region = event.target.getAttribute("data-box"); + + if (!region) { + let el = event.target; + + do { + el = el.parentNode; + + if (el && el.getAttribute("data-box")) { + region = el.getAttribute("data-box"); + break; + } + } while (el.parentNode); + + this.props.dispatch(unhighlightNode()); + } + + this.props.dispatch( + highlightSelectedNode({ + region, + showOnly: region, + onlyRegionArea: true, + }) + ); + + event.preventDefault(); + } + + /** + * Handle keyboard navigation and focus for box model layouts. + * + * Updates active layout on arrow key navigation + * Focuses next layout's editboxes on enter key + * Unfocuses current layout's editboxes when active layout changes + * Controls tabbing between editBoxes + * + * @param {Event} event + * The event triggered by a keypress on the box model + */ + onKeyDown(event) { + const { target, keyCode } = event; + const isEditable = target._editable || target.editor; + + const level = this.getActiveDescendant(); + const editingMode = target.tagName === "input"; + + switch (keyCode) { + case KeyCodes.DOM_VK_RETURN: + if (!isEditable) { + this.setState({ focusable: true }, () => { + const editableBox = this.layouts[level].get(keyCode); + if (editableBox) { + editableBox.boxModelEditable.focus(); + } + }); + } + break; + case KeyCodes.DOM_VK_DOWN: + case KeyCodes.DOM_VK_UP: + if (!editingMode) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ focusable: false }, () => { + const nextLayout = this.layouts[level].get(keyCode); + + if (!nextLayout) { + return; + } + + this.setActive(nextLayout); + + if (target?._editable) { + target.blur(); + } + + this.props.boxModelContainer.focus(); + }); + } + break; + case KeyCodes.DOM_VK_TAB: + if (isEditable) { + event.preventDefault(); + this.moveFocus(event); + } + break; + case KeyCodes.DOM_VK_ESCAPE: + if (target._editable) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ focusable: false }, () => { + this.props.boxModelContainer.focus(); + }); + } + break; + default: + break; + } + } + + /** + * Update active on mouse click. + * + * @param {Event} event + * The event triggered by a mouse click on the box model + */ + onLevelClick(event) { + const { target } = event; + const displayPosition = this.getDisplayPosition(); + const isContentBox = this.getContextBox(); + + // Avoid switching the active descendant to the position or content layout + // if those are not editable. + if ( + (!displayPosition && target == this.positionLayout) || + (!isContentBox && target == this.contentLayout) + ) { + return; + } + + const nextLayout = + this.layouts[target.getAttribute("data-box")].get("click"); + this.setActive(nextLayout); + + if (target?._editable) { + target.blur(); + } + } + + render() { + const { + boxModel, + dispatch, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + } = this.props; + const { layout } = boxModel; + let { height, width } = layout; + const { activeDescendant: level, focusable } = this.state; + + const borderTop = this.getBorderOrPaddingValue("border-top-width"); + const borderRight = this.getBorderOrPaddingValue("border-right-width"); + const borderBottom = this.getBorderOrPaddingValue("border-bottom-width"); + const borderLeft = this.getBorderOrPaddingValue("border-left-width"); + + const paddingTop = this.getBorderOrPaddingValue("padding-top"); + const paddingRight = this.getBorderOrPaddingValue("padding-right"); + const paddingBottom = this.getBorderOrPaddingValue("padding-bottom"); + const paddingLeft = this.getBorderOrPaddingValue("padding-left"); + + const displayPosition = this.getDisplayPosition(); + const positionTop = this.getPositionValue("top"); + const positionRight = this.getPositionValue("right"); + const positionBottom = this.getPositionValue("bottom"); + const positionLeft = this.getPositionValue("left"); + + const marginTop = this.getMarginValue("margin-top", "top"); + const marginRight = this.getMarginValue("margin-right", "right"); + const marginBottom = this.getMarginValue("margin-bottom", "bottom"); + const marginLeft = this.getMarginValue("margin-left", "left"); + + height = this.getHeightValue(height); + width = this.getWidthValue(width); + + const contentBox = + layout["box-sizing"] == "content-box" + ? dom.div( + { className: "boxmodel-size" }, + BoxModelEditable({ + box: "content", + focusable, + level, + property: "width", + ref: editable => { + this.contentEditable = editable; + }, + textContent: width, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + dom.span({}, "\u00D7"), + BoxModelEditable({ + box: "content", + focusable, + level, + property: "height", + textContent: height, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }) + ) + : dom.p( + { + className: "boxmodel-size", + id: "boxmodel-size-id", + }, + dom.span( + { title: "content" }, + SHARED_L10N.getFormatStr("dimensions", width, height) + ) + ); + + return dom.div( + { + className: "boxmodel-main devtools-monospace", + "data-box": "position", + ref: div => { + this.positionLayout = div; + }, + onClick: this.onLevelClick, + onKeyDown: this.onKeyDown, + onMouseOver: this.onHighlightMouseOver, + onMouseOut: () => dispatch(unhighlightNode()), + }, + displayPosition + ? dom.span( + { + className: "boxmodel-legend", + "data-box": "position", + title: "position", + }, + "position" + ) + : null, + dom.div( + { className: "boxmodel-box" }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "margin", + title: "margin", + role: "region", + "aria-level": "1", // margin, outermost box + "aria-owns": + "margin-top-id margin-right-id margin-bottom-id margin-left-id margins-div", + }, + "margin" + ), + dom.div( + { + className: "boxmodel-margins", + id: "margins-div", + "data-box": "margin", + title: "margin", + ref: div => { + this.marginLayout = div; + }, + }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "border", + title: "border", + role: "region", + "aria-level": "2", // margin -> border, second box + "aria-owns": + "border-top-width-id border-right-width-id border-bottom-width-id border-left-width-id borders-div", + }, + "border" + ), + dom.div( + { + className: "boxmodel-borders", + id: "borders-div", + "data-box": "border", + title: "border", + ref: div => { + this.borderLayout = div; + }, + }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "padding", + title: "padding", + role: "region", + "aria-level": "3", // margin -> border -> padding + "aria-owns": + "padding-top-id padding-right-id padding-bottom-id padding-left-id padding-div", + }, + "padding" + ), + dom.div( + { + className: "boxmodel-paddings", + id: "padding-div", + "data-box": "padding", + title: "padding", + "aria-owns": "boxmodel-contents-id", + ref: div => { + this.paddingLayout = div; + }, + }, + dom.div({ + className: "boxmodel-contents", + id: "boxmodel-contents-id", + "data-box": "content", + title: "content", + role: "region", + "aria-level": "4", // margin -> border -> padding -> content + "aria-label": SHARED_L10N.getFormatStr( + "boxModelSize.accessibleLabel", + width, + height + ), + "aria-owns": "boxmodel-size-id", + ref: div => { + this.contentLayout = div; + }, + }) + ) + ) + ) + ), + displayPosition + ? BoxModelEditable({ + box: "position", + direction: "top", + focusable, + level, + property: "position-top", + ref: editable => { + this.positionEditable = editable; + }, + textContent: positionTop, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }) + : null, + displayPosition + ? BoxModelEditable({ + box: "position", + direction: "right", + focusable, + level, + property: "position-right", + textContent: positionRight, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }) + : null, + displayPosition + ? BoxModelEditable({ + box: "position", + direction: "bottom", + focusable, + level, + property: "position-bottom", + textContent: positionBottom, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }) + : null, + displayPosition + ? BoxModelEditable({ + box: "position", + direction: "left", + focusable, + level, + property: "position-left", + textContent: positionLeft, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }) + : null, + BoxModelEditable({ + box: "margin", + direction: "top", + focusable, + level, + property: "margin-top", + ref: editable => { + this.marginEditable = editable; + }, + textContent: marginTop, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "margin", + direction: "right", + focusable, + level, + property: "margin-right", + textContent: marginRight, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "margin", + direction: "bottom", + focusable, + level, + property: "margin-bottom", + textContent: marginBottom, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "margin", + direction: "left", + focusable, + level, + property: "margin-left", + textContent: marginLeft, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "border", + direction: "top", + focusable, + level, + property: "border-top-width", + ref: editable => { + this.borderEditable = editable; + }, + textContent: borderTop, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "border", + direction: "right", + focusable, + level, + property: "border-right-width", + textContent: borderRight, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "border", + direction: "bottom", + focusable, + level, + property: "border-bottom-width", + textContent: borderBottom, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "border", + direction: "left", + focusable, + level, + property: "border-left-width", + textContent: borderLeft, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "padding", + direction: "top", + focusable, + level, + property: "padding-top", + ref: editable => { + this.paddingEditable = editable; + }, + textContent: paddingTop, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "padding", + direction: "right", + focusable, + level, + property: "padding-right", + textContent: paddingRight, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "padding", + direction: "bottom", + focusable, + level, + property: "padding-bottom", + textContent: paddingBottom, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + BoxModelEditable({ + box: "padding", + direction: "left", + focusable, + level, + property: "padding-left", + textContent: paddingLeft, + onShowBoxModelEditor, + onShowRulePreviewTooltip, + }), + contentBox + ); + } +} + +module.exports = BoxModelMain; diff --git a/devtools/client/inspector/boxmodel/components/BoxModelProperties.js b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js new file mode 100644 index 0000000000..8b314a46ed --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +const ComputedProperty = createFactory( + require("resource://devtools/client/inspector/boxmodel/components/ComputedProperty.js") +); + +const Types = require("resource://devtools/client/inspector/boxmodel/types.js"); + +const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties"; +const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI); + +class BoxModelProperties extends PureComponent { + static get propTypes() { + return { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + dispatch: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + isOpen: true, + }; + + this.getReferenceElement = this.getReferenceElement.bind(this); + this.onToggleExpander = this.onToggleExpander.bind(this); + } + + /** + * Various properties can display a reference element. E.g. position displays an offset + * parent if its value is other than fixed or static. Or z-index displays a stacking + * context, etc. + * This returns the right element if there needs to be one, and one was passed in the + * props. + * + * @return {Object} An object with 2 properties: + * - referenceElement {NodeFront} + * - referenceElementType {String} + */ + getReferenceElement(propertyName) { + const value = this.props.boxModel.layout[propertyName]; + + if ( + propertyName === "position" && + value !== "static" && + value !== "fixed" && + this.props.boxModel.offsetParent + ) { + return { + referenceElement: this.props.boxModel.offsetParent, + referenceElementType: BOXMODEL_L10N.getStr("boxmodel.offsetParent"), + }; + } + + return {}; + } + + onToggleExpander(event) { + this.setState({ + isOpen: !this.state.isOpen, + }); + event.stopPropagation(); + } + + render() { + const { boxModel, dispatch, setSelectedNode } = this.props; + const { layout } = boxModel; + + const layoutInfo = [ + "box-sizing", + "display", + "float", + "line-height", + "position", + "z-index", + ]; + + const properties = layoutInfo.map(info => { + const { referenceElement, referenceElementType } = + this.getReferenceElement(info); + + return ComputedProperty({ + dispatch, + key: info, + name: info, + referenceElement, + referenceElementType, + setSelectedNode, + value: layout[info], + }); + }); + + return dom.div( + { className: "layout-properties" }, + dom.div( + { + className: "layout-properties-header", + role: "heading", + "aria-level": "3", + onDoubleClick: this.onToggleExpander, + }, + dom.span({ + className: "layout-properties-expander theme-twisty", + open: this.state.isOpen, + role: "button", + "aria-label": BOXMODEL_L10N.getStr( + this.state.isOpen + ? "boxmodel.propertiesHideLabel" + : "boxmodel.propertiesShowLabel" + ), + onClick: this.onToggleExpander, + }), + BOXMODEL_L10N.getStr("boxmodel.propertiesLabel") + ), + dom.div( + { + className: "layout-properties-wrapper devtools-monospace", + hidden: !this.state.isOpen, + role: "table", + }, + properties + ) + ); + } +} + +module.exports = BoxModelProperties; diff --git a/devtools/client/inspector/boxmodel/components/ComputedProperty.js b/devtools/client/inspector/boxmodel/components/ComputedProperty.js new file mode 100644 index 0000000000..330ae7512e --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/ComputedProperty.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties"; +const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI); + +class ComputedProperty extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + referenceElement: PropTypes.object, + referenceElementType: PropTypes.string, + setSelectedNode: PropTypes.func, + value: PropTypes.string, + }; + } + + constructor(props) { + super(props); + + this.renderReferenceElementPreview = + this.renderReferenceElementPreview.bind(this); + } + + renderReferenceElementPreview() { + const { + dispatch, + referenceElement, + referenceElementType, + setSelectedNode, + } = this.props; + + if (!referenceElement) { + return null; + } + + return dom.div( + { className: "reference-element" }, + dom.span( + { + className: "reference-element-type", + role: "button", + title: BOXMODEL_L10N.getStr("boxmodel.offsetParent.title"), + }, + referenceElementType + ), + getNodeRep(referenceElement, { + onInspectIconClick: () => + setSelectedNode(referenceElement, { reason: "box-model" }), + onDOMNodeMouseOver: () => dispatch(highlightNode(referenceElement)), + onDOMNodeMouseOut: () => dispatch(unhighlightNode()), + }) + ); + } + + render() { + const { name, value } = this.props; + + return dom.div( + { + className: "computed-property-view", + role: "row", + "data-property-name": name, + ref: container => { + this.container = container; + }, + }, + dom.div( + { + className: "computed-property-name-container", + role: "presentation", + }, + dom.div( + { + className: "computed-property-name theme-fg-color3", + role: "cell", + title: name, + }, + name + ) + ), + dom.div( + { + className: "computed-property-value-container", + role: "presentation", + }, + dom.div( + { + className: "computed-property-value theme-fg-color1", + dir: "ltr", + role: "cell", + }, + value + ), + this.renderReferenceElementPreview() + ) + ); + } +} + +module.exports = ComputedProperty; diff --git a/devtools/client/inspector/boxmodel/components/moz.build b/devtools/client/inspector/boxmodel/components/moz.build new file mode 100644 index 0000000000..ed57c93eb7 --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "BoxModel.js", + "BoxModelEditable.js", + "BoxModelInfo.js", + "BoxModelMain.js", + "BoxModelProperties.js", + "ComputedProperty.js", +) diff --git a/devtools/client/inspector/boxmodel/moz.build b/devtools/client/inspector/boxmodel/moz.build new file mode 100644 index 0000000000..df8e53009e --- /dev/null +++ b/devtools/client/inspector/boxmodel/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "actions", + "components", + "reducers", + "utils", +] + +DevToolsModules( + "box-model.js", + "types.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/devtools/client/inspector/boxmodel/reducers/box-model.js b/devtools/client/inspector/boxmodel/reducers/box-model.js new file mode 100644 index 0000000000..ea3863e56a --- /dev/null +++ b/devtools/client/inspector/boxmodel/reducers/box-model.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_GEOMETRY_EDITOR_ENABLED, + UPDATE_LAYOUT, + UPDATE_OFFSET_PARENT, +} = require("resource://devtools/client/inspector/boxmodel/actions/index.js"); + +const INITIAL_BOX_MODEL = { + geometryEditorEnabled: false, + layout: {}, + offsetParent: null, +}; + +const reducers = { + [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) { + return Object.assign({}, boxModel, { + geometryEditorEnabled: enabled, + }); + }, + + [UPDATE_LAYOUT](boxModel, { layout }) { + return Object.assign({}, boxModel, { + layout, + }); + }, + + [UPDATE_OFFSET_PARENT](boxModel, { offsetParent }) { + return Object.assign({}, boxModel, { + offsetParent, + }); + }, +}; + +module.exports = function (boxModel = INITIAL_BOX_MODEL, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return boxModel; + } + return reducer(boxModel, action); +}; diff --git a/devtools/client/inspector/boxmodel/reducers/moz.build b/devtools/client/inspector/boxmodel/reducers/moz.build new file mode 100644 index 0000000000..fe216631de --- /dev/null +++ b/devtools/client/inspector/boxmodel/reducers/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "box-model.js", +) diff --git a/devtools/client/inspector/boxmodel/test/browser.toml b/devtools/client/inspector/boxmodel/test/browser.toml new file mode 100644 index 0000000000..4da0e53eeb --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser.toml @@ -0,0 +1,72 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "doc_boxmodel_iframe1.html", + "doc_boxmodel_iframe2.html", + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_boxmodel.js"] + +["browser_boxmodel_edit-position-visible-position-change.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_editablemodel.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_editablemodel_allproperties.js"] +disabled = "too many intermittent failures (bug 1009322)" + +["browser_boxmodel_editablemodel_bluronclick.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_editablemodel_border.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_editablemodel_pseudo.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_editablemodel_stylerules.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_guides.js"] + +["browser_boxmodel_jump-to-rule-on-hover.js"] + +["browser_boxmodel_layout-accordion-state.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_navigation.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_offsetparent.js"] + +["browser_boxmodel_positions.js"] + +["browser_boxmodel_properties.js"] + +["browser_boxmodel_pseudo-element.js"] + +["browser_boxmodel_rotate-labels-on-sides.js"] + +["browser_boxmodel_show-tooltip-for-unassociated-rule.js"] + +["browser_boxmodel_sync.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_boxmodel_tooltips.js"] +skip-if = ["true"] # Bug 1336198 + +["browser_boxmodel_update-after-navigation.js"] +skip-if = ["(os == 'linux' || os == 'win') && bits == 64"] #Bug 1582395 + +["browser_boxmodel_update-after-reload.js"] + +["browser_boxmodel_update-in-iframes.js"] +disabled = "Bug 1020038 boxmodel-view updates for iframe elements changes" diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js new file mode 100644 index 0000000000..f5017a5f70 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values and that it updates when +// the node's style is changed + +// Expected values: +var res1 = [ + { + selector: ".boxmodel-element-size", + value: "160" + "\u00D7" + "160.117", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "100", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "100.117", + }, + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "42", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "42", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto", + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "20", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "10", + }, +]; + +var res2 = [ + { + selector: ".boxmodel-element-size", + value: "190" + "\u00D7" + "210", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "100", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "150", + }, + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "50", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "42", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto", + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "50", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "10", + }, +]; + +add_task(async function () { + const style = + "div { position: absolute; top: 42px; left: 42px; " + + "height: 100.111px; width: 100px; border: 10px solid black; " + + "padding: 20px; margin: 30px auto;}"; + const html = "<style>" + style + "</style><div></div>"; + + await addTab("data:text/html," + encodeURIComponent(html)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const doc = boxmodel.document; + + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute( + "div", + "style", + "height:150px;padding-right:50px;top:50px" + ); + await onUpdated; + + for (let i = 0; i < res2.length; i++) { + const elt = doc.querySelector(res2[i].selector); + is( + elt.textContent, + res2[i].value, + res2[i].selector + " has the right value after style update." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js new file mode 100644 index 0000000000..0cae75aaaf --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the 'Edit Position' button is still visible after +// layout is changed. +// see bug 1398722 + +const TEST_URI = ` + <div id="mydiv" style="background:tomato; + position:absolute; + top:10px; + left:10px; + width:100px; + height:100px"> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await selectNode("#mydiv", inspector); + let editPositionButton = boxmodel.document.querySelector( + ".layout-geometry-editor" + ); + + ok( + isNodeVisible(editPositionButton), + "Edit Position button is visible initially" + ); + + const positionLeftTextbox = boxmodel.document.querySelector( + ".boxmodel-editable[title=position-left]" + ); + ok(isNodeVisible(positionLeftTextbox), "Position-left edit box exists"); + + info("Change the value of position-left and submit"); + const onUpdate = waitForUpdate(inspector); + EventUtils.synthesizeMouseAtCenter( + positionLeftTextbox, + {}, + boxmodel.document.defaultView + ); + EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + await onUpdate; + editPositionButton = boxmodel.document.querySelector( + ".layout-geometry-editor" + ); + ok( + isNodeVisible(editPositionButton), + "Edit Position button is still visible after layout change" + ); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js new file mode 100644 index 0000000000..262a28cc5f --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing the box-model values works as expected and test various +// key bindings + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "#div4 { margin: 1px; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div>" + + "<div id='div3'></div><div id='div4'></div>"; + +add_task(async function () { + // Make sure the toolbox is tall enough to have empty space below the + // boxmodel-container. + await pushPref("devtools.toolbox.footer.height", 500); + + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + await testEditingMargins(inspector, boxmodel, browser); + await testKeyBindings(inspector, boxmodel, browser); + await testEscapeToUndo(inspector, boxmodel, browser); + await testDeletingValue(inspector, boxmodel, browser); + await testRefocusingOnClick(inspector, boxmodel, browser); +}); + +async function testEditingMargins(inspector, boxmodel, browser) { + info( + "Test that editing margin dynamically updates the document, pressing " + + "escape cancels the changes" + ); + + is( + await getStyle(browser, "#div1", "margin-top"), + "", + "Should be no margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("3", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-top"), + "3px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-top"), + "", + "Should be no margin-top on the element." + ); + + await waitForElementTextContent(span, "5"); +} + +async function testKeyBindings(inspector, boxmodel, browser) { + info( + "Test that arrow keys work correctly and pressing enter commits the " + + "changes" + ); + + is( + await getStyle(browser, "#div1", "margin-left"), + "", + "Should be no margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-left > span" + ); + is(span.textContent, "10", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "10px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "11px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "11px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_DOWN", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "10px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "10px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey( + "VK_UP", + { shiftKey: true }, + boxmodel.document.defaultView + ); + await waitForUpdate(inspector); + + is(editor.value, "20px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should have updated the margin." + ); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + + await waitForElementTextContent(span, "20"); +} + +async function testEscapeToUndo(inspector, boxmodel, browser) { + info( + "Test that deleting the value removes the property but escape undoes " + + "that" + ); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-left > span" + ); + is(span.textContent, "20", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "20px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + is(span.textContent, "20", "Should have the right value in the box model."); +} + +async function testDeletingValue(inspector, boxmodel, browser) { + info("Test that deleting the value removes the property"); + + await setStyle(browser, "#div1", "marginRight", "15px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-right > span" + ); + is(span.textContent, "15", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "15px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-right"), + "", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "margin-right"), + "", + "Should be the right margin-top on the element." + ); + await waitForElementTextContent(span, "10"); +} + +async function testRefocusingOnClick(inspector, boxmodel, browser) { + info("Test that clicking in the editor input does not remove focus"); + + await selectNode("#div4", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + is(span.textContent, "1", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + info("Click in the already opened editor input"); + EventUtils.synthesizeMouseAtCenter(editor, {}, boxmodel.document.defaultView); + is( + editor, + boxmodel.document.activeElement, + "Inplace editor input should still have focus." + ); + + info("Check the input can still be used as expected"); + EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "2px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div4", "margin-top"), + "2px", + "Should have updated the margin." + ); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div4", "margin-top"), + "2px", + "Should be the right margin-top on the element." + ); + await waitForElementTextContent(span, "2"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js new file mode 100644 index 0000000000..a465d50e4f --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test editing box model values when all values are set + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + await testEditing(inspector, boxmodel, browser); + await testEditingAndCanceling(inspector, boxmodel, browser); + await testDeleting(inspector, boxmodel, browser); + await testDeletingAndCanceling(inspector, boxmodel, browser); +}); + +async function testEditing(inspector, boxmodel, browser) { + info("When all properties are set on the node editing one should work"); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-bottom > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "7", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-bottom"), + "7px", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-bottom"), + "7px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "7"); +} + +async function testEditingAndCanceling(inspector, boxmodel, browser) { + info( + "When all properties are set on the node editing one and then " + + "cancelling with ESCAPE should work" + ); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "8", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "8px", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-left"), + "5px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "5"); +} + +async function testDeleting(inspector, boxmodel, browser) { + info("When all properties are set on the node deleting one should work"); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should be the right padding." + ); + await waitForElementTextContent(span, "3"); +} + +async function testDeletingAndCanceling(inspector, boxmodel, browser) { + info( + "When all properties are set on the node deleting one then cancelling " + + "should work" + ); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-left"), + "5px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "5"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js new file mode 100644 index 0000000000..3e40eda951 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that inplace editors can be blurred by clicking outside of the editor. + +const TEST_URI = `<style> + #div1 { + margin: 10px; + padding: 3px; + } + </style> + <div id="div1"></div>`; + +add_task(async function () { + // Make sure the toolbox is tall enough to have empty space below the + // boxmodel-container. + await pushPref("devtools.toolbox.footer.height", 500); + + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await selectNode("#div1", inspector); + await testClickingOutsideEditor(boxmodel); + await testClickingBelowContainer(boxmodel); +}); + +async function testClickingOutsideEditor(boxmodel) { + info("Test that clicking outside the editor blurs it"); + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "10"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + info("Click next to the opened editor input."); + const onBlur = once(editor, "blur"); + const rect = editor.getBoundingClientRect(); + EventUtils.synthesizeMouse( + editor, + rect.width + 10, + rect.height / 2, + {}, + boxmodel.document.defaultView + ); + await onBlur; + + is( + boxmodel.document.querySelector(".styleinspector-propertyeditor"), + null, + "Inplace editor has been removed." + ); +} + +async function testClickingBelowContainer(boxmodel) { + info("Test that clicking below the box-model container blurs it"); + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "10"); + + info( + "Test that clicking below the boxmodel-container blurs the opened editor" + ); + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + const onBlur = once(editor, "blur"); + const container = boxmodel.document.querySelector(".boxmodel-container"); + // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse) + // use an erroneous height of ~50px for the boxmodel-container. + const bounds = container + .getBoxQuads({ relativeTo: boxmodel.document })[0] + .getBounds(); + EventUtils.synthesizeMouseAtPoint( + bounds.left + 10, + bounds.top + bounds.height + 10, + {}, + boxmodel.document.defaultView + ); + await onBlur; + + is( + boxmodel.document.querySelector(".styleinspector-propertyeditor"), + null, + "Inplace editor has been removed." + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js new file mode 100644 index 0000000000..98372cabbc --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing the border value in the box model applies the border style + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + is( + await getStyle(browser, "#div1", "border-top-width"), + "", + "Should have the right border" + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "", + "Should have the right border" + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-border.boxmodel-top > span" + ); + await waitForElementTextContent(span, "0"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "0", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "1", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "border-top-width"), + "1px", + "Should have the right border" + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "solid", + "Should have the right border" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "border-top-width"), + "", + "Should be the right padding." + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "", + "Should have the right border" + ); + await waitForElementTextContent(span, "0"); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js new file mode 100644 index 0000000000..b2f96fc522 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that pseudo elements have no side effect on the box model widget for their +// container. See bug 1350499. + +const TEST_URI = `<style> + .test::before { + content: 'before'; + margin-top: 5px; + padding-top: 5px; + width: 5px; + } + </style> + <div style='width:200px;'> + <div class=test></div> + </div>`; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const browser = tab.linkedBrowser; + + await selectNode(".test", inspector); + + // No margin-top defined. + info("Test that margins are not impacted by a pseudo element"); + is( + await getStyle(browser, ".test", "margin-top"), + "", + "margin-top is correct" + ); + await checkValueInBoxModel( + ".boxmodel-margin.boxmodel-top", + "0", + boxmodel.document + ); + + // No padding-top defined. + info("Test that paddings are not impacted by a pseudo element"); + is( + await getStyle(browser, ".test", "padding-top"), + "", + "padding-top is correct" + ); + await checkValueInBoxModel( + ".boxmodel-padding.boxmodel-top", + "0", + boxmodel.document + ); + + // Width should be driven by the parent div. + info("Test that dimensions are not impacted by a pseudo element"); + is(await getStyle(browser, ".test", "width"), "", "width is correct"); + await checkValueInBoxModel( + ".boxmodel-content.boxmodel-width", + "200", + boxmodel.document + ); +}); + +async function checkValueInBoxModel(selector, expectedValue, doc) { + const span = doc.querySelector(selector + " > span"); + await waitForElementTextContent(span, expectedValue); + + EventUtils.synthesizeMouseAtCenter(span, {}, doc.defaultView); + const editor = doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, expectedValue, "Should have the right value in the editor."); + + const onBlur = once(editor, "blur"); + EventUtils.synthesizeKey("VK_RETURN", {}, doc.defaultView); + await onBlur; +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js new file mode 100644 index 0000000000..06b2467d1d --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that units are displayed correctly when editing values in the box model +// and that values are retrieved and parsed correctly from the back-end + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testUnits(inspector, boxmodel, browser); + await testValueComesFromStyleRule(inspector, boxmodel, browser); + await testShorthandsAreParsed(inspector, boxmodel, browser); +}); + +async function testUnits(inspector, boxmodel, browser) { + info("Test that entering units works"); + + is( + await getStyle(browser, "#div1", "padding-top"), + "", + "Should have the right padding" + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + await waitForElementTextContent(span, "3"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "3px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + EventUtils.synthesizeKey("e", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-top"), + "", + "An invalid value is handled cleanly" + ); + + EventUtils.synthesizeKey("m", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "1em", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-top"), + "1em", + "Should have updated the padding." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-top"), + "1em", + "Should be the right padding." + ); + await waitForElementTextContent(span, "16"); +} + +async function testValueComesFromStyleRule(inspector, boxmodel, browser) { + info("Test that we pick up the value from a higher style rule"); + + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "", + "Should have the right border-bottom-width" + ); + await selectNode("#div2", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-border.boxmodel-bottom > span" + ); + await waitForElementTextContent(span, "16"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "1em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("0", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "0", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "0px", + "Should have updated the border." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "0px", + "Should be the right border-bottom-width." + ); + await waitForElementTextContent(span, "0"); +} + +async function testShorthandsAreParsed(inspector, boxmodel, browser) { + info("Test that shorthand properties are parsed correctly"); + + is( + await getStyle(browser, "#div3", "padding-right"), + "", + "Should have the right padding" + ); + await selectNode("#div3", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-right > span" + ); + await waitForElementTextContent(span, "32"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "2em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div3", "padding-right"), + "", + "Should be the right padding." + ); + await waitForElementTextContent(span, "32"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js new file mode 100644 index 0000000000..341b8f525a --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that hovering over regions in the box-model shows the highlighter with +// the right options. +// Tests that actually check the highlighter is displayed and correct are in the +// devtools/inspector/test folder. This test only cares about checking that the +// box model view does call the highlighter, and it does so by mocking it. + +const STYLE = + "div { position: absolute; top: 50px; left: 50px; " + + "height: 10px; width: 10px; border: 10px solid black; " + + "padding: 10px; margin: 10px;}"; +const HTML = "<style>" + STYLE + "</style><div></div>"; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +add_task(async function () { + await addTab(TEST_URL); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + let elt = boxmodel.document.querySelector(".boxmodel-margins"); + await testGuideOnLayoutHover(elt, "margin", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-borders"); + await testGuideOnLayoutHover(elt, "border", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-paddings"); + await testGuideOnLayoutHover(elt, "padding", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-content"); + await testGuideOnLayoutHover(elt, "content", inspector); +}); + +async function testGuideOnLayoutHover(elt, expectedRegion, inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + info("Synthesizing mouseover on the boxmodel-view"); + EventUtils.synthesizeMouse( + elt, + 50, + 2, + { type: "mouseover" }, + elt.ownerDocument.defaultView + ); + + info("Waiting for the node-highlight event from the toolbox"); + const { nodeFront, options } = await onHighlighterShown; + + // Wait for the next event tick to make sure the remaining part of the + // test is executed after finishing synthesizing mouse event. + await new Promise(executeSoon); + + is( + nodeFront, + inspector.selection.nodeFront, + "The right nodeFront was highlighted" + ); + is( + options.region, + expectedRegion, + "Region " + expectedRegion + " was highlighted" + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js new file mode 100644 index 0000000000..74f382259e --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that hovering over a box model value will jump to its source CSS rule in the +// rules view when the shift key is pressed. + +const TEST_URI = `<style> + #box { + margin: 5px; + } + </style> + <div id="box"></div>`; + +add_task(async function () { + await pushPref("devtools.layout.boxmodel.highlightProperty", true); + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("#box", inspector); + + info( + "Test that hovering over margin-top value highlights the property in rules view." + ); + const ruleView = await inspector.getPanel("ruleview").view; + const el = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + + info("Wait for mouse to hover over margin-top element."); + const onHighlightProperty = ruleView.once("scrolled-to-element"); + EventUtils.synthesizeMouseAtCenter( + el, + { type: "mousemove", shiftKey: true }, + boxmodel.document.defaultView + ); + await onHighlightProperty; + + info("Check that margin-top is visible in the rule view."); + const { rules, styleWindow } = ruleView; + const marginTop = rules[1].textProps[0].computed[0]; + ok( + isInViewport(marginTop.element, styleWindow), + "margin-top is visible in the rule view." + ); +}); + +function isInViewport(element, win) { + const { top, left, bottom, right } = element.getBoundingClientRect(); + return ( + top >= 0 && + bottom <= win.innerHeight && + left >= 0 && + right <= win.innerWidth + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js new file mode 100644 index 0000000000..d6802f4e5d --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the box model's accordion state is persistent through hide/show in the +// layout view. + +const TEST_URI = ` + <style> + #div1 { + margin: 10px; + padding: 3px; + } + </style> + <div id="div1"></div> +`; + +const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened"; +const ACCORDION_HEADER_SELECTOR = ".accordion-header"; +const ACCORDION_CONTENT_SELECTOR = ".accordion-content"; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel, toolbox } = await openLayoutView(); + const { document: doc } = boxmodel; + + await testAccordionStateAfterClickingHeader(doc); + await testAccordionStateAfterSwitchingSidebars(inspector, doc); + await testAccordionStateAfterReopeningLayoutView(toolbox); + + Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF); +}); + +function testAccordionStateAfterClickingHeader(doc) { + const item = doc.querySelector("#layout-section-boxmodel"); + const header = item.querySelector(ACCORDION_HEADER_SELECTOR); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking initial state of the box model panel."); + ok( + !content.hidden && content.childElementCount > 0, + "The box model panel content is visible." + ); + ok( + Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref on by default.` + ); + + info("Clicking the box model header to hide the box model panel."); + header.click(); + + info("Checking the new state of the box model panel."); + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} + +function testAccordionStateAfterSwitchingSidebars(inspector, doc) { + info( + "Checking the box model accordion state is persistent after switching sidebars." + ); + + info("Selecting the computed view."); + inspector.sidebar.select("computedview"); + + info("Selecting the layout view."); + inspector.sidebar.select("layoutview"); + + info("Checking the state of the box model panel."); + const item = doc.querySelector("#layout-section-boxmodel"); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} + +async function testAccordionStateAfterReopeningLayoutView(toolbox) { + info( + "Checking the box model accordion state is persistent after closing and " + + "re-opening the layout view." + ); + + info("Closing the toolbox."); + await toolbox.destroy(); + + info("Re-opening the layout view."); + const { boxmodel } = await openLayoutView(); + const item = boxmodel.document.querySelector("#layout-section-boxmodel"); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking the state of the box model panel."); + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js new file mode 100644 index 0000000000..7274092a1a --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that keyboard and mouse navigation updates aria-active and focus +// of elements. + +const TEST_URI = ` + <style> + div { position: absolute; top: 42px; left: 42px; + height: 100.111px; width: 100px; border: 10px solid black; + padding: 20px; margin: 30px auto;} + </style><div></div> +`; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialFocus(inspector, boxmodel); + await testChangingLevels(inspector, boxmodel); + await testTabbingThroughItems(inspector, boxmodel); + await testChangingLevelsByClicking(inspector, boxmodel); +}); + +function testInitialFocus(inspector, boxmodel) { + info("Test that the focus is(on margin layout."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + is( + container.dataset.activeDescendantClassName, + "boxmodel-main devtools-monospace", + "Should be set to the position layout." + ); +} + +function testChangingLevels(inspector, boxmodel) { + info("Test that using arrow keys updates level."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + EventUtils.synthesizeKey("KEY_Escape"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-margins", + "Should be set to the margin layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-borders", + "Should be set to the border layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-paddings", + "Should be set to the padding layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-contents", + "Should be set to the content layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-paddings", + "Should be set to the padding layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-borders", + "Should be set to the border layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-margins", + "Should be set to the margin layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-main devtools-monospace", + "Should be set to the position layout." + ); +} + +function testTabbingThroughItems(inspector, boxmodel) { + info("Test that using Tab key moves focus to next/previous input field."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + const editBoxes = [...doc.querySelectorAll("[data-box].boxmodel-editable")]; + + const editBoxesInfo = [ + { name: "position-top", itemId: "position-top-id" }, + { name: "position-right", itemId: "position-right-id" }, + { name: "position-bottom", itemId: "position-bottom-id" }, + { name: "position-left", itemId: "position-left-id" }, + { name: "margin-top", itemId: "margin-top-id" }, + { name: "margin-right", itemId: "margin-right-id" }, + { name: "margin-bottom", itemId: "margin-bottom-id" }, + { name: "margin-left", itemId: "margin-left-id" }, + { name: "border-top-width", itemId: "border-top-width-id" }, + { name: "border-right-width", itemId: "border-right-width-id" }, + { name: "border-bottom-width", itemId: "border-bottom-width-id" }, + { name: "border-left-width", itemId: "border-left-width-id" }, + { name: "padding-top", itemId: "padding-top-id" }, + { name: "padding-right", itemId: "padding-right-id" }, + { name: "padding-bottom", itemId: "padding-bottom-id" }, + { name: "padding-left", itemId: "padding-left-id" }, + { name: "width", itemId: "width-id" }, + { name: "height", itemId: "height-id" }, + ]; + + // Check whether tabbing through box model items works + // Note that the test checks whether wrapping around the box model works + // by letting the loop run beyond the number of indexes to start with + // the first item again. + for (let i = 0; i <= editBoxesInfo.length; i++) { + const itemIndex = i % editBoxesInfo.length; + const editBoxInfo = editBoxesInfo[itemIndex]; + is( + editBoxes[itemIndex].parentElement.id, + editBoxInfo.itemId, + `${editBoxInfo.name} item is current` + ); + is( + editBoxes[itemIndex].previousElementSibling?.localName, + "input", + `Input shown for ${editBoxInfo.name} item` + ); + + // Pressing Tab should not be synthesized for the last item to + // wrap to the very last item again when tabbing in reversed order. + if (i < editBoxesInfo.length) { + EventUtils.synthesizeKey("KEY_Tab"); + } + } + + // Check whether reversed tabbing through box model items works + for (let i = editBoxesInfo.length; i >= 0; i--) { + const itemIndex = i % editBoxesInfo.length; + const editBoxInfo = editBoxesInfo[itemIndex]; + is( + editBoxes[itemIndex].parentElement.id, + editBoxInfo.itemId, + `${editBoxInfo.name} item is current` + ); + is( + editBoxes[itemIndex].previousElementSibling?.localName, + "input", + `Input shown for ${editBoxInfo.name} item` + ); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + } +} + +function testChangingLevelsByClicking(inspector, boxmodel) { + info("Test that clicking on levels updates level."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + + const marginLayout = doc.querySelector(".boxmodel-margins"); + const borderLayout = doc.querySelector(".boxmodel-borders"); + const paddingLayout = doc.querySelector(".boxmodel-paddings"); + const contentLayout = doc.querySelector(".boxmodel-contents"); + const layouts = [contentLayout, paddingLayout, borderLayout, marginLayout]; + + layouts.forEach(layout => { + layout.click(); + is( + container.dataset.activeDescendantClassName, + layout.className, + `Should be set to ${layout.getAttribute("data-box")} layout.` + ); + }); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js new file mode 100644 index 0000000000..000d548bdd --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for the offset parent and that it +// updates when the node's style is changed + +const TEST_URI = ` + <div id="relative_parent" style="position: relative"> + <div id="absolute_child" style="position: absolute"></div> + </div> + <div id="static"></div> + <div id="no_parent" style="position: absolute"></div> + <div id="fixed" style="position: fixed"></div> +`; + +const OFFSET_PARENT_SELECTOR = + ".computed-property-value-container .objectBox-node"; + +const res1 = [ + { + selector: "#absolute_child", + offsetParentValue: "div#relative_parent", + }, + { + selector: "#no_parent", + offsetParentValue: "body", + }, + { + selector: "#relative_parent", + offsetParentValue: "body", + }, + { + selector: "#static", + offsetParentValue: null, + }, + { + selector: "#fixed", + offsetParentValue: null, + }, +]; + +const updates = [ + { + selector: "#absolute_child", + update: "position: static", + }, +]; + +const res2 = [ + { + selector: "#absolute_child", + offsetParentValue: null, + }, +]; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +async function testInitialValues(inspector, boxmodel) { + info( + "Test that the initial values of the box model offset parent are correct" + ); + const viewdoc = boxmodel.document; + + for (const { selector, offsetParentValue } of res1) { + await selectNode(selector, inspector); + + const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR); + is( + elt && elt.textContent, + offsetParentValue, + selector + " has the right value." + ); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const viewdoc = boxmodel.document; + + for (const { selector, update } of updates) { + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute(selector, "style", update); + await onUpdated; + } + + for (const { selector, offsetParentValue } of res2) { + await selectNode(selector, inspector); + + const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR); + is( + elt && elt.textContent, + offsetParentValue, + selector + " has the right value after style update." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js new file mode 100644 index 0000000000..ca182d7130 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for positions. + +const TEST_URI = ` + <style type='text/css'> + div { + position: absolute; + left: 0; + margin: 0; + padding: 0; + display: none; + height: 100px; + width: 100px; + border: 10px solid black; + } + </style> + <div>Test Node</div> +`; + +// Expected values: +const res1 = [ + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-bottom > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "0", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const node = await getNodeFront("div", inspector); + const children = await inspector.markup.walker.children(node); + const beforeElement = children.nodes[0]; + + await selectNode(beforeElement, inspector); + await testPositionValues(inspector, boxmodel); +}); + +function testPositionValues(inspector, boxmodel) { + info("Test that the position values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js new file mode 100644 index 0000000000..6ebf5ab761 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model properties list displays the right values +// and that it updates when the node's style is changed. + +const TEST_URI = ` + <style type='text/css'> + div { + box-sizing: border-box; + display: block; + float: left; + line-height: 20px; + position: relative; + z-index: 2; + height: 100px; + width: 100px; + border: 10px solid black; + padding: 20px; + margin: 30px auto; + } + </style> + <div>Test Node</div> +`; + +const res1 = [ + { + property: "box-sizing", + value: "border-box", + }, + { + property: "display", + value: "block", + }, + { + property: "float", + value: "left", + }, + { + property: "line-height", + value: "20px", + }, + { + property: "position", + value: "relative", + }, + { + property: "z-index", + value: "2", + }, +]; + +const res2 = [ + { + property: "box-sizing", + value: "content-box", + }, + { + property: "display", + value: "block", + }, + { + property: "float", + value: "right", + }, + { + property: "line-height", + value: "10px", + }, + { + property: "position", + value: "static", + }, + { + property: "z-index", + value: "5", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (const { property, value } of res1) { + const elt = doc.querySelector(getPropertySelector(property)); + is(elt.textContent, value, property + " has the right value."); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const doc = boxmodel.document; + + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute( + "div", + "style", + "box-sizing:content-box;float:right;" + + "line-height:10px;position:static;z-index:5;" + ); + await onUpdated; + + for (const { property, value } of res2) { + const elt = doc.querySelector(getPropertySelector(property)); + is( + elt.textContent, + value, + property + " has the right value after style update." + ); + } +} + +function getPropertySelector(propertyName) { + return ( + `.boxmodel-container .computed-property-view` + + `[data-property-name=${propertyName}] .computed-property-value` + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js new file mode 100644 index 0000000000..046ee067ba --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for a pseudo-element. + +const TEST_URI = ` + <style type='text/css'> + div { + box-sizing: border-box; + display: block; + float: left; + line-height: 20px; + position: relative; + z-index: 2; + height: 100px; + width: 100px; + border: 10px solid black; + padding: 20px; + margin: 30px auto; + } + + div::before { + content: 'before'; + display: block; + width: 32px; + height: 32px; + margin: 0 auto 6px; + } + </style> + <div>Test Node</div> +`; + +// Expected values: +const res1 = [ + { + selector: ".boxmodel-element-size", + value: "32" + "\u00D7" + "32", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "32", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "32", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2 + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "6", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2 + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "0", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const node = await getNodeFront("div", inspector); + const children = await inspector.markup.walker.children(node); + const beforeElement = children.nodes[0]; + + await selectNode(beforeElement, inspector); + await testInitialValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js new file mode 100644 index 0000000000..f84ca9d8b0 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that longer values are rotated on the side + +const res1 = [ + { selector: ".boxmodel-margin.boxmodel-top > span", value: 30 }, + { selector: ".boxmodel-margin.boxmodel-left > span", value: "auto" }, + { selector: ".boxmodel-margin.boxmodel-bottom > span", value: 30 }, + { selector: ".boxmodel-margin.boxmodel-right > span", value: "auto" }, + { selector: ".boxmodel-padding.boxmodel-top > span", value: 20 }, + { selector: ".boxmodel-padding.boxmodel-left > span", value: 2000000 }, + { selector: ".boxmodel-padding.boxmodel-bottom > span", value: 20 }, + { selector: ".boxmodel-padding.boxmodel-right > span", value: 20 }, + { selector: ".boxmodel-border.boxmodel-top > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-left > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-bottom > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-right > span", value: 10 }, +]; + +const TEST_URI = encodeURIComponent( + [ + "<style>", + "div { border:10px solid black; padding: 20px 20px 20px 2000000px; " + + "margin: 30px auto; }", + "</style>", + "<div></div>", + ].join("") +); +const LONG_TEXT_ROTATE_LIMIT = 3; + +add_task(async function () { + await addTab("data:text/html," + TEST_URI); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + for (let i = 0; i < res1.length; i++) { + const elt = boxmodel.document.querySelector(res1[i].selector); + const isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT; + const classList = elt.parentNode.classList; + const canBeRotated = + classList.contains("boxmodel-left") || + classList.contains("boxmodel-right"); + const isRotated = classList.contains("boxmodel-rotate"); + + is( + canBeRotated && isLong, + isRotated, + res1[i].selector + " correctly rotated." + ); + } +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js new file mode 100644 index 0000000000..ffb911b342 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +// Test that hovering over a box model value with no associated rule will show a tooltip +// saying: "No associated rule". + +const TEST_URI = `<style> + #box {} + </style> + <div id="box"></div>`; + +add_task(async function () { + await pushPref("devtools.layout.boxmodel.highlightProperty", true); + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const { rulePreviewTooltip } = boxmodel; + await selectNode("#box", inspector); + + info( + "Test that hovering over margin-top shows tooltip showing 'No associated rule'." + ); + const el = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + + info("Wait for mouse to hover over margin-top element."); + const onRulePreviewTooltipShown = rulePreviewTooltip._tooltip.once( + "shown", + () => { + ok(true, "Tooltip shown."); + is( + rulePreviewTooltip.message.textContent, + L10N.getStr("rulePreviewTooltip.noAssociatedRule"), + `Tooltip shows ${L10N.getStr("rulePreviewTooltip.noAssociatedRule")}` + ); + } + ); + EventUtils.synthesizeMouseAtCenter( + el, + { type: "mousemove", shiftKey: true }, + boxmodel.document.defaultView + ); + await onRulePreviewTooltipShown; +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js new file mode 100644 index 0000000000..fed1e85519 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test editing box model syncs with the rule view. + +const TEST_URI = "<p>hello</p>"; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + info("When a property is edited, it should sync in the rule view"); + + await selectNode("p", inspector); + + info("Modify padding-bottom in box model view"); + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-bottom > span" + ); + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + + const onRuleViewRefreshed = once(inspector, "rule-view-refreshed"); + EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + await onRuleViewRefreshed; + is(editor.value, "7", "Should have the right value in the editor."); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + info("Check that the property was synced with the rule view"); + const ruleView = selectRuleView(inspector); + const ruleEditor = getRuleViewRuleEditor(ruleView, 0); + const textProp = ruleEditor.rule.textProps[0]; + is(textProp.value, "7px", "The property has the right value"); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js new file mode 100644 index 0000000000..de1ab24936 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the regions in the box model view have tooltips, and that individual +// values too. Also test that values that are set from a css rule have tooltips +// referencing the rule. + +const TEST_URI = + "<style>" + + "#div1 { color: red; margin: 3em; }\n" + + "#div2 { border-bottom: 1px solid black; background: red; }\n" + + "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +// Test data for the tooltips over individual values. +// Each entry should contain: +// - selector: The selector for the node to be selected before starting to test +// - values: An array containing objects for each of the values that are defined +// by css rules. Each entry should contain: +// - name: the name of the property that is set by the css rule +// - ruleSelector: the selector of the rule +// - styleSheetLocation: the fileName:lineNumber +const VALUES_TEST_DATA = [ + { + selector: "#div1", + values: [ + { + name: "margin-top", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-right", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-bottom", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-left", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + ], + }, + { + selector: "#div2", + values: [ + { + name: "border-bottom-width", + ruleSelector: "#div2", + styleSheetLocation: "inline:2", + }, + ], + }, + { + selector: "#div3", + values: [ + { + name: "padding-top", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-right", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-bottom", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-left", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + ], + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + info("Checking the regions tooltips"); + + ok( + boxmodel.document.querySelector(".boxmodel-margins").hasAttribute("title"), + "The margin region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-margins").getAttribute("title"), + "margin", + "The margin region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-borders").hasAttribute("title"), + "The border region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-borders").getAttribute("title"), + "border", + "The border region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-paddings").hasAttribute("title"), + "The padding region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-paddings").getAttribute("title"), + "padding", + "The padding region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-content").hasAttribute("title"), + "The content region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-content").getAttribute("title"), + "content", + "The content region has the correct tooltip content" + ); + + for (const { selector, values } of VALUES_TEST_DATA) { + info("Selecting " + selector + " and checking the values tooltips"); + await selectNode(selector, inspector); + + info("Iterate over all values"); + for (const key in boxmodel.map) { + if (key === "position") { + continue; + } + + const name = boxmodel.map[key].property; + const expectedTooltipData = values.find(o => o.name === name); + const el = boxmodel.document.querySelector(boxmodel.map[key].selector); + + ok(el.hasAttribute("title"), "The " + name + " value has a tooltip"); + + if (expectedTooltipData) { + info("The " + name + " value comes from a css rule"); + const expectedTooltip = + name + + "\n" + + expectedTooltipData.ruleSelector + + "\n" + + expectedTooltipData.styleSheetLocation; + is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct"); + } else { + info("The " + name + " isn't set by a css rule"); + is(el.getAttribute("title"), name, "The tooltip is correct"); + } + } + } +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js new file mode 100644 index 0000000000..4cd83590b0 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view continues to work after a page navigation and that +// it also works after going back + +const IFRAME1 = URL_ROOT_SSL + "doc_boxmodel_iframe1.html"; +const IFRAME2 = URL_ROOT_SSL + "doc_boxmodel_iframe2.html"; + +add_task(async function () { + const tab = await addTab(IFRAME1); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testFirstPage(inspector, boxmodel, browser); + + info("Navigate to the second page"); + let onMarkupLoaded = waitForMarkupLoaded(inspector); + await navigateTo(IFRAME2); + await onMarkupLoaded; + + await testSecondPage(inspector, boxmodel, browser); + + info("Go back to the first page"); + onMarkupLoaded = waitForMarkupLoaded(inspector); + gBrowser.goBack(); + await onMarkupLoaded; + + await testBackToFirstPage(inspector, boxmodel, browser); +}); + +async function testFirstPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the first page"); + + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "20px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} + +async function testSecondPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the second page"); + + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100" + "\u00D7" + "100"); + + info("Listening for box model view changes and modifying the size"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200" + "\u00D7" + "100"); +} + +async function testBackToFirstPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the first page after going back"); + + await selectNode("p", inspector); + + info( + "Checking that the box model view shows the right value, which is the" + + "modified value from step one because of the bfcache" + ); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "20"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "100px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "100"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js new file mode 100644 index 0000000000..f6d309c8e2 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view continues to work after the page is reloaded + +add_task(async function () { + const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + info("Test that the box model view works on the first page"); + await assertBoxModelView(inspector, boxmodel, browser); + + info("Reload the page"); + const onMarkupLoaded = waitForMarkupLoaded(inspector); + await reloadBrowser(); + await onMarkupLoaded; + + info("Test that the box model view works on the reloaded page"); + await assertBoxModelView(inspector, boxmodel, browser); +}); + +async function assertBoxModelView(inspector, boxmodel, browser) { + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "20px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js new file mode 100644 index 0000000000..eee72f696c --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view for elements within iframes also updates when they +// change + +add_task(async function () { + const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testResizingInIframe(inspector, boxmodel, browser); + await testReflowsAfterIframeDeletion(inspector, boxmodel, browser); +}); + +async function testResizingInIframe(inspector, boxmodel, browser) { + info("Test that resizing an element in an iframe updates its box model"); + + info("Selecting the nested test node"); + await selectNodeInFrames(["iframe", "iframe", "div"], inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "400\u00D7200"); + + info("Listening for box model view changes and modifying its size"); + const onUpdated = waitForUpdate(inspector); + await setStyleInNestedIframe(browser, "div", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7200"); +} + +async function testReflowsAfterIframeDeletion(inspector, boxmodel, browser) { + info( + "Test reflows are still sent to the box model view after deleting an " + + "iframe" + ); + + info("Deleting the iframe2"); + const onInspectorUpdated = inspector.once("inspector-updated"); + await removeNestedIframe(browser); + await onInspectorUpdated; + + info("Selecting the test node in iframe1"); + await selectNodeInFrames(["iframe", "p"], inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100\u00D7100"); + + info("Listening for box model view changes and modifying its size"); + const onUpdated = waitForUpdate(inspector); + await setStyleInIframe(browser, "p", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7100"); +} + +async function setStyleInIframe(browser, selector, propertyName, value) { + const context = await getBrowsingContextInFrames(browser, ["iframe"]); + return setStyle(context, selector, propertyName, value); +} + +async function setStyleInNestedIframe(browser, selector, propertyName, value) { + const context = await getBrowsingContextInFrames(browser, [ + "iframe", + "iframe", + ]); + return setStyle(context, selector, propertyName, value); +} + +async function removeNestedIframe(browser) { + const context = await getBrowsingContextInFrames(browser, ["iframe"]); + await SpecialPowers.spawn(context, [], () => + content.document.querySelector("iframe").remove() + ); +} diff --git a/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html new file mode 100644 index 0000000000..eef48ce079 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="padding:50px;color:#f06;">Root page</p> +<iframe src="doc_boxmodel_iframe2.html"></iframe> diff --git a/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html new file mode 100644 index 0000000000..0fa6dc02e9 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="width:100px;height:100px;background:red;box-sizing:border-box">iframe 1</p> +<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;box-sizing:border-box'>iframe 2</div>"></iframe> diff --git a/devtools/client/inspector/boxmodel/test/head.js b/devtools/client/inspector/boxmodel/test/head.js new file mode 100644 index 0000000000..f15424acfb --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/head.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +Services.prefs.setIntPref("devtools.toolbox.footer.height", 350); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); +}); + +/** + * Is the given node visible in the page (rendered in the frame tree). + * @param {DOMNode} + * @return {Boolean} + */ +function isNodeVisible(node) { + return !!node.getClientRects().length; +} + +/** + * Wait for the boxmodel-view-updated event. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox. + * @param {Boolean} waitForSelectionUpdate + * Should the boxmodel-view-updated event come from a new selection. + * @return {Promise} a promise + */ +async function waitForUpdate(inspector, waitForSelectionUpdate) { + /** + * While the highlighter is visible (mouse over the fields of the box model editor), + * reflow events are prevented; see ReflowActor -> setIgnoreLayoutChanges() + * The box model view updates in response to reflow events. + * To ensure reflow events are fired, hide the highlighter. + */ + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + + return new Promise(resolve => { + inspector.on("boxmodel-view-updated", function onUpdate(reasons) { + // Wait for another update event if we are waiting for a selection related event. + if (waitForSelectionUpdate && !reasons.includes("new-selection")) { + return; + } + + inspector.off("boxmodel-view-updated", onUpdate); + resolve(); + }); + }); +} + +/** + * Wait for both boxmode-view-updated and markuploaded events. + * + * @return {Promise} a promise that resolves when both events have been received. + */ +function waitForMarkupLoaded(inspector) { + return Promise.all([ + waitForUpdate(inspector), + inspector.once("markuploaded"), + ]); +} + +function getStyle(browser, selector, propertyName) { + return SpecialPowers.spawn( + browser, + [selector, propertyName], + async function (_selector, _propertyName) { + return content.document + .querySelector(_selector) + .style.getPropertyValue(_propertyName); + } + ); +} + +function setStyle(browser, selector, propertyName, value) { + return SpecialPowers.spawn( + browser, + [selector, propertyName, value], + async function (_selector, _propertyName, _value) { + content.document.querySelector(_selector).style[_propertyName] = _value; + } + ); +} + +/** + * The box model doesn't participate in the inspector's update mechanism, so simply + * calling the default selectNode isn't enough to guarantee that the box model view has + * finished updating. We also need to wait for the "boxmodel-view-updated" event. + */ +var _selectNode = selectNode; +selectNode = async function (node, inspector, reason) { + const onUpdate = waitForUpdate(inspector, true); + await _selectNode(node, inspector, reason); + await onUpdate; +}; + +/** + * Wait until the provided element's text content matches the provided text. + * Based on the waitFor helper, see documentation in + * devtools/client/shared/test/shared-head.js + * + * @param {DOMNode} element + * The element to check. + * @param {String} expectedText + * The text that is expected to be set as textContent of the element. + */ +async function waitForElementTextContent(element, expectedText) { + await waitFor( + () => element.textContent === expectedText, + `Couldn't get "${expectedText}" as the text content of the given element` + ); + ok(true, `Found the expected text (${expectedText}) for the given element`); +} diff --git a/devtools/client/inspector/boxmodel/types.js b/devtools/client/inspector/boxmodel/types.js new file mode 100644 index 0000000000..b477974b65 --- /dev/null +++ b/devtools/client/inspector/boxmodel/types.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +/** + * The box model data for the current selected node. + */ +exports.boxModel = { + // Whether or not the geometry editor is enabled + geometryEditorEnabled: PropTypes.bool, + + // The layout information of the current selected node + layout: PropTypes.object, + + // The offset parent for the selected node + offsetParent: PropTypes.object, +}; diff --git a/devtools/client/inspector/boxmodel/utils/editing-session.js b/devtools/client/inspector/boxmodel/utils/editing-session.js new file mode 100644 index 0000000000..3175375f22 --- /dev/null +++ b/devtools/client/inspector/boxmodel/utils/editing-session.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * An instance of EditingSession tracks changes that have been made during the + * modification of box model values. All of these changes can be reverted by + * calling revert. + * + * @param {InspectorPanel} inspector + * The inspector panel. + * @param {Document} doc + * A DOM document that can be used to test style rules. + * @param {Array} rules + * An array of the style rules defined for the node being + * edited. These should be in order of priority, least + * important first. + */ +function EditingSession({ inspector, doc, elementRules }) { + this._doc = doc; + this._inspector = inspector; + this._rules = elementRules; + this._modifications = new Map(); +} + +EditingSession.prototype = { + /** + * Gets the value of a single property from the CSS rule. + * + * @param {StyleRuleFront} rule + * The CSS rule. + * @param {String} property + * The name of the property. + * @return {String} the value. + */ + getPropertyFromRule(rule, property) { + // Use the parsed declarations in the StyleRuleFront object if available. + const index = this.getPropertyIndex(property, rule); + if (index !== -1) { + return rule.declarations[index].value; + } + + // Fallback to parsing the cssText locally otherwise. + const dummyStyle = this._element.style; + dummyStyle.cssText = rule.cssText; + return dummyStyle.getPropertyValue(property); + }, + + /** + * Returns the current value for a property as a string or the empty string if + * no style rules affect the property. + * + * @param {String} property + * The name of the property as a string + */ + getProperty(property) { + // Create a hidden element for getPropertyFromRule to use + const div = this._doc.createElement("div"); + div.setAttribute("style", "display: none"); + this._doc.getElementById("inspector-main-content").appendChild(div); + this._element = this._doc.createElement("p"); + div.appendChild(this._element); + + // As the rules are in order of priority we can just iterate until we find + // the first that defines a value for the property and return that. + for (const rule of this._rules) { + const value = this.getPropertyFromRule(rule, property); + if (value !== "") { + div.remove(); + return value; + } + } + div.remove(); + return ""; + }, + + /** + * Get the index of a given css property name in a CSS rule. + * Or -1, if there are no properties in the rule yet. + * + * @param {String} name + * The property name. + * @param {StyleRuleFront} rule + * Optional, defaults to the element style rule. + * @return {Number} The property index in the rule. + */ + getPropertyIndex(name, rule = this._rules[0]) { + if (!rule.declarations.length) { + return -1; + } + + return rule.declarations.findIndex(p => p.name === name); + }, + + /** + * Sets a number of properties on the node. + * + * @param {Array} properties + * An array of properties, each is an object with name and + * value properties. If the value is "" then the property + * is removed. + * @return {Promise} Resolves when the modifications are complete. + */ + async setProperties(properties) { + for (const property of properties) { + // Get a RuleModificationList or RuleRewriter helper object from the + // StyleRuleActor to make changes to CSS properties. + // Note that RuleRewriter doesn't support modifying several properties at + // once, so we do this in a sequence here. + const modifications = this._rules[0].startModifyingProperties( + this._inspector.cssProperties + ); + + // Remember the property so it can be reverted. + if (!this._modifications.has(property.name)) { + this._modifications.set( + property.name, + this.getPropertyFromRule(this._rules[0], property.name) + ); + } + + // Find the index of the property to be changed, or get the next index to + // insert the new property at. + let index = this.getPropertyIndex(property.name); + if (index === -1) { + index = this._rules[0].declarations.length; + } + + if (property.value == "") { + modifications.removeProperty(index, property.name); + } else { + modifications.setProperty(index, property.name, property.value, ""); + } + + await modifications.apply(); + } + }, + + /** + * Reverts all of the property changes made by this instance. + * + * @return {Promise} Resolves when all properties have been reverted. + */ + async revert() { + // Revert each property that we modified previously, one by one. See + // setProperties for information about why. + for (const [property, value] of this._modifications) { + const modifications = this._rules[0].startModifyingProperties( + this._inspector.cssProperties + ); + + // Find the index of the property to be reverted. + let index = this.getPropertyIndex(property); + + if (value != "") { + // If the property doesn't exist anymore, insert at the beginning of the + // rule. + if (index === -1) { + index = 0; + } + modifications.setProperty(index, property, value, ""); + } else { + // If the property doesn't exist anymore, no need to remove it. It had + // not been added after all. + if (index === -1) { + continue; + } + modifications.removeProperty(index, property); + } + + await modifications.apply(); + } + }, + + destroy() { + this._modifications.clear(); + + this._cssProperties = null; + this._doc = null; + this._inspector = null; + this._modifications = null; + this._rules = null; + }, +}; + +module.exports = EditingSession; diff --git a/devtools/client/inspector/boxmodel/utils/moz.build b/devtools/client/inspector/boxmodel/utils/moz.build new file mode 100644 index 0000000000..76a5656294 --- /dev/null +++ b/devtools/client/inspector/boxmodel/utils/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "editing-session.js", +) diff --git a/devtools/client/inspector/breadcrumbs.js b/devtools/client/inspector/breadcrumbs.js new file mode 100644 index 0000000000..68bafb591c --- /dev/null +++ b/devtools/client/inspector/breadcrumbs.js @@ -0,0 +1,973 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const flags = require("resource://devtools/shared/flags.js"); +const { ELLIPSIS } = require("resource://devtools/shared/l10n.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +loader.lazyRequireGetter( + this, + "KeyShortcuts", + "resource://devtools/client/shared/key-shortcuts.js" +); + +const MAX_LABEL_LENGTH = 40; + +const NS_XHTML = "http://www.w3.org/1999/xhtml"; +const SCROLL_REPEAT_MS = 100; + +// Some margin may be required for visible element detection. +const SCROLL_MARGIN = 1; + +const SHADOW_ROOT_TAGNAME = "#shadow-root"; + +/** + * Component to replicate functionality of XUL arrowscrollbox + * for breadcrumbs + * + * @param {Window} win The window containing the breadcrumbs + * @parem {DOMNode} container The element in which to put the scroll box + */ +function ArrowScrollBox(win, container) { + this.win = win; + this.doc = win.document; + this.container = container; + EventEmitter.decorate(this); + this.init(); +} + +ArrowScrollBox.prototype = { + // Scroll behavior, exposed for testing + scrollBehavior: "smooth", + + /** + * Build the HTML, add to the DOM and start listening to + * events + */ + init() { + this.constructHtml(); + + this.onScroll = this.onScroll.bind(this); + this.onStartBtnClick = this.onStartBtnClick.bind(this); + this.onEndBtnClick = this.onEndBtnClick.bind(this); + this.onStartBtnDblClick = this.onStartBtnDblClick.bind(this); + this.onEndBtnDblClick = this.onEndBtnDblClick.bind(this); + this.onUnderflow = this.onUnderflow.bind(this); + this.onOverflow = this.onOverflow.bind(this); + + this.inner.addEventListener("scroll", this.onScroll); + this.startBtn.addEventListener("mousedown", this.onStartBtnClick); + this.endBtn.addEventListener("mousedown", this.onEndBtnClick); + this.startBtn.addEventListener("dblclick", this.onStartBtnDblClick); + this.endBtn.addEventListener("dblclick", this.onEndBtnDblClick); + + // Overflow and underflow are moz specific events + this.inner.addEventListener("underflow", this.onUnderflow); + this.inner.addEventListener("overflow", this.onOverflow); + }, + + /** + * Scroll to the specified element using the current scroll behavior + * @param {Element} element element to scroll + * @param {String} block desired alignment of element after scrolling + */ + scrollToElement(element, block) { + element.scrollIntoView({ block, behavior: this.scrollBehavior }); + }, + + /** + * Call the given function once; then continuously + * while the mouse button is held + * @param {Function} repeatFn the function to repeat while the button is held + */ + clickOrHold(repeatFn) { + let timer; + const container = this.container; + + function handleClick() { + cancelHold(); + repeatFn(); + } + + const window = this.win; + function cancelHold() { + window.clearTimeout(timer); + container.removeEventListener("mouseout", cancelHold); + container.removeEventListener("mouseup", handleClick); + } + + function repeated() { + repeatFn(); + timer = window.setTimeout(repeated, SCROLL_REPEAT_MS); + } + + container.addEventListener("mouseout", cancelHold); + container.addEventListener("mouseup", handleClick); + timer = window.setTimeout(repeated, SCROLL_REPEAT_MS); + }, + + /** + * When start button is dbl clicked scroll to first element + */ + onStartBtnDblClick() { + const children = this.inner.childNodes; + if (children.length < 1) { + return; + } + + const element = this.inner.childNodes[0]; + this.scrollToElement(element, "start"); + }, + + /** + * When end button is dbl clicked scroll to last element + */ + onEndBtnDblClick() { + const children = this.inner.childNodes; + if (children.length < 1) { + return; + } + + const element = children[children.length - 1]; + this.scrollToElement(element, "start"); + }, + + /** + * When start arrow button is clicked scroll towards first element + */ + onStartBtnClick() { + const scrollToStart = () => { + const element = this.getFirstInvisibleElement(); + if (!element) { + return; + } + + this.scrollToElement(element, "start"); + }; + + this.clickOrHold(scrollToStart); + }, + + /** + * When end arrow button is clicked scroll towards last element + */ + onEndBtnClick() { + const scrollToEnd = () => { + const element = this.getLastInvisibleElement(); + if (!element) { + return; + } + + this.scrollToElement(element, "end"); + }; + + this.clickOrHold(scrollToEnd); + }, + + /** + * Event handler for scrolling, update the + * enabled/disabled status of the arrow buttons + */ + onScroll() { + const first = this.getFirstInvisibleElement(); + if (!first) { + this.startBtn.setAttribute("disabled", "true"); + } else { + this.startBtn.removeAttribute("disabled"); + } + + const last = this.getLastInvisibleElement(); + if (!last) { + this.endBtn.setAttribute("disabled", "true"); + } else { + this.endBtn.removeAttribute("disabled"); + } + }, + + /** + * On underflow, make the arrow buttons invisible + */ + onUnderflow() { + this.startBtn.style.visibility = "collapse"; + this.endBtn.style.visibility = "collapse"; + this.emit("underflow"); + }, + + /** + * On overflow, show the arrow buttons + */ + onOverflow() { + this.startBtn.style.visibility = "visible"; + this.endBtn.style.visibility = "visible"; + this.emit("overflow"); + }, + + /** + * Check whether the element is to the left of its container but does + * not also span the entire container. + * @param {Number} left the left scroll point of the container + * @param {Number} right the right edge of the container + * @param {Number} elementLeft the left edge of the element + * @param {Number} elementRight the right edge of the element + */ + elementLeftOfContainer(left, right, elementLeft, elementRight) { + return ( + elementLeft < left - SCROLL_MARGIN && elementRight < right - SCROLL_MARGIN + ); + }, + + /** + * Check whether the element is to the right of its container but does + * not also span the entire container. + * @param {Number} left the left scroll point of the container + * @param {Number} right the right edge of the container + * @param {Number} elementLeft the left edge of the element + * @param {Number} elementRight the right edge of the element + */ + elementRightOfContainer(left, right, elementLeft, elementRight) { + return ( + elementLeft > left + SCROLL_MARGIN && elementRight > right + SCROLL_MARGIN + ); + }, + + /** + * Get the first (i.e. furthest left for LTR) + * non or partly visible element in the scroll box + */ + getFirstInvisibleElement() { + const elementsList = Array.from(this.inner.childNodes).reverse(); + + const predicate = this.elementLeftOfContainer; + return this.findFirstWithBounds(elementsList, predicate); + }, + + /** + * Get the last (i.e. furthest right for LTR) + * non or partly visible element in the scroll box + */ + getLastInvisibleElement() { + const predicate = this.elementRightOfContainer; + return this.findFirstWithBounds(this.inner.childNodes, predicate); + }, + + /** + * Find the first element that matches the given predicate, called with bounds + * information + * @param {Array} elements an ordered list of elements + * @param {Function} predicate a function to be called with bounds + * information + */ + findFirstWithBounds(elements, predicate) { + const left = this.inner.scrollLeft; + const right = left + this.inner.clientWidth; + for (const element of elements) { + const elementLeft = element.offsetLeft - element.parentElement.offsetLeft; + const elementRight = elementLeft + element.offsetWidth; + + // Check that the starting edge of the element is out of the visible area + // and that the ending edge does not span the whole container + if (predicate(left, right, elementLeft, elementRight)) { + return element; + } + } + + return null; + }, + + /** + * Build the HTML for the scroll box and insert it into the DOM + */ + constructHtml() { + this.startBtn = this.createElement( + "div", + "scrollbutton-up", + this.container + ); + this.createElement("div", "toolbarbutton-icon", this.startBtn); + + this.createElement( + "div", + "arrowscrollbox-overflow-start-indicator", + this.container + ); + this.inner = this.createElement( + "div", + "html-arrowscrollbox-inner", + this.container + ); + this.createElement( + "div", + "arrowscrollbox-overflow-end-indicator", + this.container + ); + + this.endBtn = this.createElement( + "div", + "scrollbutton-down", + this.container + ); + this.createElement("div", "toolbarbutton-icon", this.endBtn); + }, + + /** + * Create an XHTML element with the given class name, and append it to the + * parent. + * @param {String} tagName name of the tag to create + * @param {String} className class of the element + * @param {DOMNode} parent the parent node to which it should be appended + * @return {DOMNode} The new element + */ + createElement(tagName, className, parent) { + const el = this.doc.createElementNS(NS_XHTML, tagName); + el.className = className; + if (parent) { + parent.appendChild(el); + } + + return el; + }, + + /** + * Remove event handlers and clean up + */ + destroy() { + this.inner.removeEventListener("scroll", this.onScroll); + this.startBtn.removeEventListener("mousedown", this.onStartBtnClick); + this.endBtn.removeEventListener("mousedown", this.onEndBtnClick); + this.startBtn.removeEventListener("dblclick", this.onStartBtnDblClick); + this.endBtn.removeEventListener("dblclick", this.onRightBtnDblClick); + + // Overflow and underflow are moz specific events + this.inner.removeEventListener("underflow", this.onUnderflow); + this.inner.removeEventListener("overflow", this.onOverflow); + }, +}; + +/** + * Display the ancestors of the current node and its children. + * Only one "branch" of children are displayed (only one line). + * + * Mechanism: + * - If no nodes displayed yet: + * then display the ancestor of the selected node and the selected node; + * else select the node; + * - If the selected node is the last node displayed, append its first (if any). + * + * @param {InspectorPanel} inspector The inspector hosting this widget. + */ +function HTMLBreadcrumbs(inspector) { + this.inspector = inspector; + this.selection = this.inspector.selection; + this.win = this.inspector.panelWin; + this.doc = this.inspector.panelDoc; + this._init(); +} + +exports.HTMLBreadcrumbs = HTMLBreadcrumbs; + +HTMLBreadcrumbs.prototype = { + get walker() { + return this.inspector.walker; + }, + + _init() { + this.outer = this.doc.getElementById("inspector-breadcrumbs"); + this.arrowScrollBox = new ArrowScrollBox(this.win, this.outer); + + this.container = this.arrowScrollBox.inner; + this.scroll = this.scroll.bind(this); + this.arrowScrollBox.on("overflow", this.scroll); + + this.outer.addEventListener("click", this, true); + this.outer.addEventListener("mouseover", this, true); + this.outer.addEventListener("mouseout", this, true); + this.outer.addEventListener("focus", this, true); + + this.handleShortcut = this.handleShortcut.bind(this); + + if (flags.testing) { + // In tests, we start listening immediately to avoid having to simulate a focus. + this.initKeyShortcuts(); + } else { + this.outer.addEventListener( + "focus", + () => { + this.initKeyShortcuts(); + }, + { once: true } + ); + } + + // We will save a list of already displayed nodes in this array. + this.nodeHierarchy = []; + + // Last selected node in nodeHierarchy. + this.currentIndex = -1; + + // Used to build a unique breadcrumb button Id. + this.breadcrumbsWidgetItemId = 0; + + this.update = this.update.bind(this); + this.updateWithMutations = this.updateWithMutations.bind(this); + this.updateSelectors = this.updateSelectors.bind(this); + this.selection.on("new-node-front", this.update); + this.selection.on("pseudoclass", this.updateSelectors); + this.selection.on("attribute-changed", this.updateSelectors); + this.inspector.on("markupmutation", this.updateWithMutations); + this.update(); + }, + + initKeyShortcuts() { + this.shortcuts = new KeyShortcuts({ window: this.win, target: this.outer }); + this.shortcuts.on("Right", this.handleShortcut); + this.shortcuts.on("Left", this.handleShortcut); + }, + + /** + * Build a string that represents the node: tagName#id.class1.class2. + * @param {NodeFront} node The node to pretty-print + * @return {String} + */ + prettyPrintNodeAsText(node) { + let text = node.isShadowRoot ? SHADOW_ROOT_TAGNAME : node.displayName; + if (node.isMarkerPseudoElement) { + text = "::marker"; + } else if (node.isBeforePseudoElement) { + text = "::before"; + } else if (node.isAfterPseudoElement) { + text = "::after"; + } + + if (node.id) { + text += "#" + node.id; + } + + if (node.className) { + const classList = node.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + text += "." + classList[i]; + } + } + + for (const pseudo of node.pseudoClassLocks) { + text += pseudo; + } + + return text; + }, + + /** + * Build <span>s that represent the node: + * <span class="breadcrumbs-widget-item-tag">tagName</span> + * <span class="breadcrumbs-widget-item-id">#id</span> + * <span class="breadcrumbs-widget-item-classes">.class1.class2</span> + * @param {NodeFront} node The node to pretty-print + * @returns {DocumentFragment} + */ + prettyPrintNodeAsXHTML(node) { + const tagLabel = this.doc.createElementNS(NS_XHTML, "span"); + tagLabel.className = "breadcrumbs-widget-item-tag plain"; + + const idLabel = this.doc.createElementNS(NS_XHTML, "span"); + idLabel.className = "breadcrumbs-widget-item-id plain"; + + const classesLabel = this.doc.createElementNS(NS_XHTML, "span"); + classesLabel.className = "breadcrumbs-widget-item-classes plain"; + + const pseudosLabel = this.doc.createElementNS(NS_XHTML, "span"); + pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain"; + + let tagText = node.isShadowRoot ? SHADOW_ROOT_TAGNAME : node.displayName; + if (node.isMarkerPseudoElement) { + tagText = "::marker"; + } else if (node.isBeforePseudoElement) { + tagText = "::before"; + } else if (node.isAfterPseudoElement) { + tagText = "::after"; + } + let idText = node.id ? "#" + node.id : ""; + let classesText = ""; + + if (node.className) { + const classList = node.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + classesText += "." + classList[i]; + } + } + + // Figure out which element (if any) needs ellipsing. + // Substring for that element, then clear out any extras + // (except for pseudo elements). + const maxTagLength = MAX_LABEL_LENGTH; + const maxIdLength = MAX_LABEL_LENGTH - tagText.length; + const maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length; + + if (tagText.length > maxTagLength) { + tagText = tagText.substr(0, maxTagLength) + ELLIPSIS; + idText = classesText = ""; + } else if (idText.length > maxIdLength) { + idText = idText.substr(0, maxIdLength) + ELLIPSIS; + classesText = ""; + } else if (classesText.length > maxClassLength) { + classesText = classesText.substr(0, maxClassLength) + ELLIPSIS; + } + + tagLabel.textContent = tagText; + idLabel.textContent = idText; + classesLabel.textContent = classesText; + pseudosLabel.textContent = node.pseudoClassLocks.join(""); + + const fragment = this.doc.createDocumentFragment(); + fragment.appendChild(tagLabel); + fragment.appendChild(idLabel); + fragment.appendChild(classesLabel); + fragment.appendChild(pseudosLabel); + + return fragment; + }, + + /** + * Generic event handler. + * @param {DOMEvent} event. + */ + handleEvent(event) { + if (event.type == "click" && event.button == 0) { + this.handleClick(event); + } else if (event.type == "mouseover") { + this.handleMouseOver(event); + } else if (event.type == "mouseout") { + this.handleMouseOut(event); + } else if (event.type == "focus") { + this.handleFocus(event); + } + }, + + /** + * Focus event handler. When breadcrumbs container gets focus, + * aria-activedescendant needs to be updated to currently selected + * breadcrumb. Ensures that the focus stays on the container at all times. + * @param {DOMEvent} event. + */ + handleFocus(event) { + event.stopPropagation(); + + const node = this.nodeHierarchy[this.currentIndex]; + if (node) { + this.outer.setAttribute("aria-activedescendant", node.button.id); + } else { + this.outer.removeAttribute("aria-activedescendant"); + } + + this.outer.focus(); + }, + + /** + * On click navigate to the correct node. + * @param {DOMEvent} event. + */ + handleClick(event) { + const target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsClick(); + } + }, + + /** + * On mouse over, highlight the corresponding content DOM Node. + * @param {DOMEvent} event. + */ + handleMouseOver(event) { + const target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsHover(); + } + }, + + /** + * On mouse out, make sure to unhighlight. + * @param {DOMEvent} event. + */ + handleMouseOut(event) { + this.inspector.highlighters.hideHighlighterType( + this.inspector.highlighters.TYPES.BOXMODEL + ); + }, + + /** + * Handle a keyboard shortcut supported by the breadcrumbs widget. + * + * @param {String} name + * Name of the keyboard shortcut received. + * @param {DOMEvent} event + * Original event that triggered the shortcut. + */ + handleShortcut(event) { + if (!this.selection.isElementNode()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + this.keyPromise = (this.keyPromise || Promise.resolve(null)).then(() => { + let currentnode; + + const isLeft = event.code === "ArrowLeft"; + const isRight = event.code === "ArrowRight"; + + if (isLeft && this.currentIndex != 0) { + currentnode = this.nodeHierarchy[this.currentIndex - 1]; + } else if (isRight && this.currentIndex < this.nodeHierarchy.length - 1) { + currentnode = this.nodeHierarchy[this.currentIndex + 1]; + } else { + return null; + } + + this.outer.setAttribute("aria-activedescendant", currentnode.button.id); + return this.selection.setNodeFront(currentnode.node, { + reason: "breadcrumbs", + }); + }); + }, + + /** + * Remove nodes and clean up. + */ + destroy() { + this.selection.off("new-node-front", this.update); + this.selection.off("pseudoclass", this.updateSelectors); + this.selection.off("attribute-changed", this.updateSelectors); + this.inspector.off("markupmutation", this.updateWithMutations); + + this.container.removeEventListener("click", this, true); + this.container.removeEventListener("mouseover", this, true); + this.container.removeEventListener("mouseout", this, true); + this.container.removeEventListener("focus", this, true); + + if (this.shortcuts) { + this.shortcuts.destroy(); + } + + this.empty(); + + this.arrowScrollBox.off("overflow", this.scroll); + this.arrowScrollBox.destroy(); + this.arrowScrollBox = null; + this.outer = null; + this.container = null; + this.nodeHierarchy = null; + + this.isDestroyed = true; + }, + + /** + * Empty the breadcrumbs container. + */ + empty() { + while (this.container.hasChildNodes()) { + this.container.firstChild.remove(); + } + }, + + /** + * Set which button represent the selected node. + * @param {Number} index Index of the displayed-button to select. + */ + setCursor(index) { + // Unselect the previously selected button + if ( + this.currentIndex > -1 && + this.currentIndex < this.nodeHierarchy.length + ) { + this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked"); + } + if (index > -1) { + this.nodeHierarchy[index].button.setAttribute("checked", "true"); + } else { + // Unset active active descendant when all buttons are unselected. + this.outer.removeAttribute("aria-activedescendant"); + } + this.currentIndex = index; + }, + + /** + * Get the index of the node in the cache. + * @param {NodeFront} node. + * @returns {Number} The index for this node or -1 if not found. + */ + indexOf(node) { + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + if (this.nodeHierarchy[i].node === node) { + return i; + } + } + return -1; + }, + + /** + * Remove all the buttons and their references in the cache after a given + * index. + * @param {Number} index. + */ + cutAfter(index) { + while (this.nodeHierarchy.length > index + 1) { + const toRemove = this.nodeHierarchy.pop(); + this.container.removeChild(toRemove.button); + } + }, + + /** + * Build a button representing the node. + * @param {NodeFront} node The node from the page. + * @return {DOMNode} The <button> for this node. + */ + buildButton(node) { + const button = this.doc.createElementNS(NS_XHTML, "button"); + button.appendChild(this.prettyPrintNodeAsXHTML(node)); + button.className = "breadcrumbs-widget-item"; + button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++; + + button.setAttribute("tabindex", "-1"); + button.setAttribute("title", this.prettyPrintNodeAsText(node)); + + button.onclick = () => { + button.focus(); + }; + + button.onBreadcrumbsClick = () => { + this.selection.setNodeFront(node, { reason: "breadcrumbs" }); + }; + + button.onBreadcrumbsHover = () => { + this.inspector.highlighters.showHighlighterTypeForNode( + this.inspector.highlighters.TYPES.BOXMODEL, + node + ); + }; + + return button; + }, + + /** + * Connecting the end of the breadcrumbs to a node. + * @param {NodeFront} node The node to reach. + */ + expand(node) { + const fragment = this.doc.createDocumentFragment(); + let lastButtonInserted = null; + const originalLength = this.nodeHierarchy.length; + let stopNode = null; + if (originalLength > 0) { + stopNode = this.nodeHierarchy[originalLength - 1].node; + } + while (node && node != stopNode) { + if (node.tagName || node.isShadowRoot) { + const button = this.buildButton(node); + fragment.insertBefore(button, lastButtonInserted); + lastButtonInserted = button; + this.nodeHierarchy.splice(originalLength, 0, { + node, + button, + currentPrettyPrintText: this.prettyPrintNodeAsText(node), + }); + } + node = node.parentOrHost(); + } + this.container.appendChild(fragment, this.container.firstChild); + }, + + /** + * Find the "youngest" ancestor of a node which is already in the breadcrumbs. + * @param {NodeFront} node. + * @return {Number} Index of the ancestor in the cache, or -1 if not found. + */ + getCommonAncestor(node) { + while (node) { + const idx = this.indexOf(node); + if (idx > -1) { + return idx; + } + node = node.parentNode(); + } + return -1; + }, + + /** + * Ensure the selected node is visible. + */ + scroll() { + // FIXME bug 684352: make sure its immediate neighbors are visible too. + if (!this.isDestroyed) { + const element = this.nodeHierarchy[this.currentIndex].button; + this.arrowScrollBox.scrollToElement(element, "end"); + } + }, + + /** + * Update all button outputs. + */ + updateSelectors() { + if (this.isDestroyed) { + return; + } + + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + const { node, button, currentPrettyPrintText } = this.nodeHierarchy[i]; + + // If the output of the node doesn't change, skip the update. + const textOutput = this.prettyPrintNodeAsText(node); + if (currentPrettyPrintText === textOutput) { + continue; + } + + // Otherwise, update the whole markup for the button. + while (button.hasChildNodes()) { + button.firstChild.remove(); + } + button.appendChild(this.prettyPrintNodeAsXHTML(node)); + button.setAttribute("title", textOutput); + + this.nodeHierarchy[i].currentPrettyPrintText = textOutput; + } + }, + + /** + * Given a list of mutation changes (passed by the markupmutation event), + * decide whether or not they are "interesting" to the current state of the + * breadcrumbs widget, i.e. at least one of them should cause part of the + * widget to be updated. + * @param {Array} mutations The mutations array. + * @return {Boolean} + */ + _hasInterestingMutations(mutations) { + if (!mutations || !mutations.length) { + return false; + } + + for (const { type, added, removed, target, attributeName } of mutations) { + if (type === "childList") { + // Only interested in childList mutations if the added or removed + // nodes are currently displayed. + return ( + added.some(node => this.indexOf(node) > -1) || + removed.some(node => this.indexOf(node) > -1) + ); + } else if (type === "attributes" && this.indexOf(target) > -1) { + // Only interested in attributes mutations if the target is + // currently displayed, and the attribute is either id or class. + return attributeName === "class" || attributeName === "id"; + } + } + + // Catch all return in case the mutations array was empty, or in case none + // of the changes iterated above were interesting. + return false; + }, + + /** + * Update the breadcrumbs display when a new node is selected and there are + * mutations. + * @param {Array} mutations An array of mutations in case this was called as + * the "markupmutation" event listener. + */ + updateWithMutations(mutations) { + return this.update("markupmutation", mutations); + }, + + /** + * Update the breadcrumbs display when a new node is selected. + * @param {String} reason The reason for the update, if any. + * @param {Array} mutations An array of mutations in case this was called as + * the "markupmutation" event listener. + */ + update(reason, mutations) { + if (this.isDestroyed) { + return; + } + + const hasInterestingMutations = this._hasInterestingMutations(mutations); + if (reason === "markupmutation" && !hasInterestingMutations) { + return; + } + + if (!this.selection.isConnected()) { + // remove all the crumbs + this.cutAfter(-1); + return; + } + + // If this was an interesting deletion; then trim the breadcrumb trail + let trimmed = false; + if (reason === "markupmutation") { + for (const { type, removed } of mutations) { + if (type !== "childList") { + continue; + } + + for (const node of removed) { + const removedIndex = this.indexOf(node); + if (removedIndex > -1) { + this.cutAfter(removedIndex - 1); + trimmed = true; + } + } + } + } + + if (!this.selection.isElementNode() && !this.selection.isShadowRootNode()) { + // no selection + this.setCursor(-1); + if (trimmed) { + // Since something changed, notify the interested parties. + this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); + } + return; + } + + let idx = this.indexOf(this.selection.nodeFront); + + // Is the node already displayed in the breadcrumbs? + // (and there are no mutations that need re-display of the crumbs) + if (idx > -1 && !hasInterestingMutations) { + // Yes. We select it. + this.setCursor(idx); + } else { + // No. Is the breadcrumbs display empty? + if (this.nodeHierarchy.length) { + // No. We drop all the element that are not direct ancestors + // of the selection + const parent = this.selection.nodeFront.parentNode(); + const ancestorIdx = this.getCommonAncestor(parent); + this.cutAfter(ancestorIdx); + } + // we append the missing button between the end of the breadcrumbs display + // and the current node. + this.expand(this.selection.nodeFront); + + // we select the current node button + idx = this.indexOf(this.selection.nodeFront); + this.setCursor(idx); + } + + const doneUpdating = this.inspector.updating("breadcrumbs"); + + this.updateSelectors(); + + // Make sure the selected node and its neighbours are visible. + setTimeout(() => { + try { + this.scroll(); + this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); + doneUpdating(); + } catch (e) { + // Only log this as an error if we haven't been destroyed in the meantime. + if (!this.isDestroyed) { + console.error(e); + } + } + }, 0); + }, +}; diff --git a/devtools/client/inspector/changes/ChangesContextMenu.js b/devtools/client/inspector/changes/ChangesContextMenu.js new file mode 100644 index 0000000000..40257ec898 --- /dev/null +++ b/devtools/client/inspector/changes/ChangesContextMenu.js @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Menu = require("resource://devtools/client/framework/menu.js"); +loader.lazyRequireGetter( + this, + "MenuItem", + "resource://devtools/client/framework/menu-item.js" +); + +const { + getStr, +} = require("resource://devtools/client/inspector/changes/utils/l10n.js"); + +/** + * Context menu for the Changes panel with options to select, copy and export CSS changes. + */ +class ChangesContextMenu extends Menu { + constructor(config = {}) { + super(config); + this.onCopy = config.onCopy; + this.onCopyAllChanges = config.onCopyAllChanges; + this.onCopyDeclaration = config.onCopyDeclaration; + this.onCopyRule = config.onCopyRule; + this.onSelectAll = config.onSelectAll; + this.toolboxDocument = config.toolboxDocument; + this.window = config.window; + } + + show(event) { + this._openMenu({ + target: event.target, + screenX: event.screenX, + screenY: event.screenY, + }); + } + + _openMenu({ target, screenX = 0, screenY = 0 } = {}) { + this.window.focus(); + // Remove existing menu items. + this.clear(); + + // Copy option + const menuitemCopy = new MenuItem({ + id: "changes-contextmenu-copy", + label: getStr("changes.contextmenu.copy"), + accesskey: getStr("changes.contextmenu.copy.accessKey"), + click: this.onCopy, + disabled: !this._hasTextSelected(), + }); + this.append(menuitemCopy); + + const declEl = target.closest(".changes__declaration"); + const ruleEl = target.closest("[data-rule-id]"); + const ruleId = ruleEl ? ruleEl.dataset.ruleId : null; + + if (ruleId || declEl) { + // Copy Rule option + this.append( + new MenuItem({ + id: "changes-contextmenu-copy-rule", + label: getStr("changes.contextmenu.copyRule"), + click: () => this.onCopyRule(ruleId, true), + }) + ); + + // Copy Declaration option. Visible only if there is a declaration element target. + this.append( + new MenuItem({ + id: "changes-contextmenu-copy-declaration", + label: getStr("changes.contextmenu.copyDeclaration"), + click: () => this.onCopyDeclaration(declEl), + visible: !!declEl, + }) + ); + + this.append( + new MenuItem({ + type: "separator", + }) + ); + } + + // Select All option + const menuitemSelectAll = new MenuItem({ + id: "changes-contextmenu-select-all", + label: getStr("changes.contextmenu.selectAll"), + accesskey: getStr("changes.contextmenu.selectAll.accessKey"), + click: this.onSelectAll, + }); + this.append(menuitemSelectAll); + + this.popup(screenX, screenY, this.toolboxDocument); + } + + _hasTextSelected() { + const selection = this.window.getSelection(); + return selection.toString() && !selection.isCollapsed; + } + + destroy() { + this.window = null; + this.toolboxDocument = null; + } +} + +module.exports = ChangesContextMenu; diff --git a/devtools/client/inspector/changes/ChangesView.js b/devtools/client/inspector/changes/ChangesView.js new file mode 100644 index 0000000000..20c70137a7 --- /dev/null +++ b/devtools/client/inspector/changes/ChangesView.js @@ -0,0 +1,284 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + createElement, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + Provider, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +loader.lazyRequireGetter( + this, + "ChangesContextMenu", + "resource://devtools/client/inspector/changes/ChangesContextMenu.js" +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); + +const changesReducer = require("resource://devtools/client/inspector/changes/reducers/changes.js"); +const { + getChangesStylesheet, +} = require("resource://devtools/client/inspector/changes/selectors/changes.js"); +const { + resetChanges, + trackChange, +} = require("resource://devtools/client/inspector/changes/actions/changes.js"); + +const ChangesApp = createFactory( + require("resource://devtools/client/inspector/changes/components/ChangesApp.js") +); + +class ChangesView { + constructor(inspector, window) { + this.document = window.document; + this.inspector = inspector; + this.store = this.inspector.store; + this.telemetry = this.inspector.telemetry; + this.window = window; + + this.store.injectReducer("changes", changesReducer); + + this.onAddChange = this.onAddChange.bind(this); + this.onContextMenu = this.onContextMenu.bind(this); + this.onCopy = this.onCopy.bind(this); + this.onCopyAllChanges = this.copyAllChanges.bind(this); + this.onCopyDeclaration = this.copyDeclaration.bind(this); + this.onCopyRule = this.copyRule.bind(this); + this.onClearChanges = this.onClearChanges.bind(this); + this.onSelectAll = this.onSelectAll.bind(this); + this.onResourceAvailable = this.onResourceAvailable.bind(this); + + this.destroy = this.destroy.bind(this); + + this.init(); + } + + get contextMenu() { + if (!this._contextMenu) { + this._contextMenu = new ChangesContextMenu({ + onCopy: this.onCopy, + onCopyAllChanges: this.onCopyAllChanges, + onCopyDeclaration: this.onCopyDeclaration, + onCopyRule: this.onCopyRule, + onSelectAll: this.onSelectAll, + toolboxDocument: this.inspector.toolbox.doc, + window: this.window, + }); + } + + return this._contextMenu; + } + + get resourceCommand() { + return this.inspector.toolbox.resourceCommand; + } + + init() { + const changesApp = ChangesApp({ + onContextMenu: this.onContextMenu, + onCopyAllChanges: this.onCopyAllChanges, + onCopyRule: this.onCopyRule, + }); + + // Expose the provider to let inspector.js use it in setupSidebar. + this.provider = createElement( + Provider, + { + id: "changesview", + key: "changesview", + store: this.store, + }, + changesApp + ); + + this.watchResources(); + } + + async watchResources() { + await this.resourceCommand.watchResources( + [this.resourceCommand.TYPES.DOCUMENT_EVENT], + { + onAvailable: this.onResourceAvailable, + // Ignore any DOCUMENT_EVENT resources that have occured in the past + // and are cached by the resource command, otherwise the Changes panel will + // react to them erroneously and interpret that the document is reloading *now* + // which leads to clearing all stored changes. + ignoreExistingResources: true, + } + ); + + await this.resourceCommand.watchResources( + [this.resourceCommand.TYPES.CSS_CHANGE], + { onAvailable: this.onResourceAvailable } + ); + } + + onResourceAvailable(resources) { + for (const resource of resources) { + if (resource.resourceType === this.resourceCommand.TYPES.CSS_CHANGE) { + this.onAddChange(resource); + continue; + } + + if (resource.name === "dom-loading" && resource.targetFront.isTopLevel) { + // will-navigate doesn't work when we navigate to a new process, + // and for now, onTargetAvailable/onTargetDestroyed doesn't fire on navigation and + // only when navigating to another process. + // So we fallback on DOCUMENT_EVENTS to be notified when we navigate. When we + // navigate within the same process as well as when we navigate to a new process. + // (We would probably revisit that in bug 1632141) + this.onClearChanges(); + } + } + } + + /** + * Handler for the "Copy All Changes" button. Simple wrapper that just calls + * |this.copyChanges()| with no filters in order to trigger default operation. + */ + copyAllChanges() { + this.copyChanges(); + } + + /** + * Handler for the "Copy Changes" option from the context menu. + * Builds a CSS text with the aggregated changes and copies it to the clipboard. + * + * Optional rule and source ids can be used to filter the scope of the operation: + * - if both a rule id and source id are provided, copy only the changes to the + * matching rule within the matching source. + * - if only a source id is provided, copy the changes to all rules within the + * matching source. + * - if neither rule id nor source id are provided, copy the changes too all rules + * within all sources. + * + * @param {String|null} ruleId + * Optional rule id. + * @param {String|null} sourceId + * Optional source id. + */ + copyChanges(ruleId, sourceId) { + const state = this.store.getState().changes || {}; + const filter = {}; + if (ruleId) { + filter.ruleIds = [ruleId]; + } + if (sourceId) { + filter.sourceIds = [sourceId]; + } + + const text = getChangesStylesheet(state, filter); + clipboardHelper.copyString(text); + } + + /** + * Handler for the "Copy Declaration" option from the context menu. + * Builds a CSS declaration string with the property name and value, and copies it + * to the clipboard. The declaration is commented out if it is marked as removed. + * + * @param {DOMElement} element + * Host element of a CSS declaration rendered the Changes panel. + */ + copyDeclaration(element) { + const name = element.querySelector( + ".changes__declaration-name" + ).textContent; + const value = element.querySelector( + ".changes__declaration-value" + ).textContent; + const isRemoved = element.classList.contains("diff-remove"); + const text = isRemoved ? `/* ${name}: ${value}; */` : `${name}: ${value};`; + clipboardHelper.copyString(text); + } + + /** + * Handler for the "Copy Rule" option from the context menu and "Copy Rule" button. + * Gets the full content of the target CSS rule (including any changes applied) + * and copies it to the clipboard. + * + * @param {String} ruleId + * Rule id of the target CSS rule. + */ + async copyRule(ruleId) { + const inspectorFronts = await this.inspector.getAllInspectorFronts(); + + for (const inspectorFront of inspectorFronts) { + const rule = await inspectorFront.pageStyle.getRule(ruleId); + + if (rule) { + const text = await rule.getRuleText(); + clipboardHelper.copyString(text); + break; + } + } + } + + /** + * Handler for the "Copy" option from the context menu. + * Copies the current text selection to the clipboard. + */ + onCopy() { + clipboardHelper.copyString(this.window.getSelection().toString()); + } + + onAddChange(change) { + // Turn data into a suitable change to send to the store. + this.store.dispatch(trackChange(change)); + } + + onClearChanges() { + this.store.dispatch(resetChanges()); + } + + /** + * Select all text. + */ + onSelectAll() { + const selection = this.window.getSelection(); + selection.selectAllChildren( + this.document.getElementById("sidebar-panel-changes") + ); + } + + /** + * Event handler for the "contextmenu" event fired when the context menu is requested. + * @param {Event} e + */ + onContextMenu(e) { + this.contextMenu.show(e); + } + + /** + * Destruction function called when the inspector is destroyed. + */ + destroy() { + this.resourceCommand.unwatchResources( + [ + this.resourceCommand.TYPES.CSS_CHANGE, + this.resourceCommand.TYPES.DOCUMENT_EVENT, + ], + { onAvailable: this.onResourceAvailable } + ); + + this.store.dispatch(resetChanges()); + + this.document = null; + this.inspector = null; + this.store = null; + + if (this._contextMenu) { + this._contextMenu.destroy(); + this._contextMenu = null; + } + } +} + +module.exports = ChangesView; diff --git a/devtools/client/inspector/changes/actions/changes.js b/devtools/client/inspector/changes/actions/changes.js new file mode 100644 index 0000000000..b3b02cfa2a --- /dev/null +++ b/devtools/client/inspector/changes/actions/changes.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + RESET_CHANGES, + TRACK_CHANGE, +} = require("resource://devtools/client/inspector/changes/actions/index.js"); + +module.exports = { + resetChanges() { + return { + type: RESET_CHANGES, + }; + }, + + trackChange(change) { + return { + type: TRACK_CHANGE, + change, + }; + }, +}; diff --git a/devtools/client/inspector/changes/actions/index.js b/devtools/client/inspector/changes/actions/index.js new file mode 100644 index 0000000000..3ae33883f2 --- /dev/null +++ b/devtools/client/inspector/changes/actions/index.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Remove all changes + "RESET_CHANGES", + + // Track a style change + "TRACK_CHANGE", + ], + module.exports +); diff --git a/devtools/client/inspector/changes/actions/moz.build b/devtools/client/inspector/changes/actions/moz.build new file mode 100644 index 0000000000..06c5314a9e --- /dev/null +++ b/devtools/client/inspector/changes/actions/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "changes.js", + "index.js", +) diff --git a/devtools/client/inspector/changes/components/CSSDeclaration.js b/devtools/client/inspector/changes/components/CSSDeclaration.js new file mode 100644 index 0000000000..c25bf6833f --- /dev/null +++ b/devtools/client/inspector/changes/components/CSSDeclaration.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class CSSDeclaration extends PureComponent { + static get propTypes() { + return { + className: PropTypes.string, + property: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }; + } + + static get defaultProps() { + return { + className: "", + }; + } + + render() { + const { className, property, value } = this.props; + + return dom.div( + { className: `changes__declaration ${className}` }, + dom.span( + { className: "changes__declaration-name theme-fg-color3" }, + property + ), + ": ", + dom.span( + { className: "changes__declaration-value theme-fg-color1" }, + value + ), + ";" + ); + } +} + +module.exports = CSSDeclaration; diff --git a/devtools/client/inspector/changes/components/ChangesApp.js b/devtools/client/inspector/changes/components/ChangesApp.js new file mode 100644 index 0000000000..9953109e19 --- /dev/null +++ b/devtools/client/inspector/changes/components/ChangesApp.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 { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const CSSDeclaration = createFactory( + require("resource://devtools/client/inspector/changes/components/CSSDeclaration.js") +); +const { + getChangesTree, +} = require("resource://devtools/client/inspector/changes/selectors/changes.js"); +const { + getSourceForDisplay, +} = require("resource://devtools/client/inspector/changes/utils/changes-utils.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/changes/utils/l10n.js"); + +class ChangesApp extends PureComponent { + static get propTypes() { + return { + // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet) + changesTree: PropTypes.object.isRequired, + // Event handler for "contextmenu" event + onContextMenu: PropTypes.func.isRequired, + // Event handler for click on "Copy All Changes" button + onCopyAllChanges: PropTypes.func.isRequired, + // Event handler for click on "Copy Rule" button + onCopyRule: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + } + + renderCopyAllChangesButton() { + const button = dom.button( + { + className: "changes__copy-all-changes-button", + onClick: e => { + e.stopPropagation(); + this.props.onCopyAllChanges(); + }, + title: getStr("changes.contextmenu.copyAllChangesDescription"), + }, + getStr("changes.contextmenu.copyAllChanges") + ); + + return dom.div({ className: "changes__toolbar" }, button); + } + + renderCopyButton(ruleId) { + return dom.button( + { + className: "changes__copy-rule-button", + onClick: e => { + e.stopPropagation(); + this.props.onCopyRule(ruleId); + }, + title: getStr("changes.contextmenu.copyRuleDescription"), + }, + getStr("changes.contextmenu.copyRule") + ); + } + + renderDeclarations(remove = [], add = []) { + const removals = remove + // Sorting changed declarations in the order they appear in the Rules view. + .sort((a, b) => a.index > b.index) + .map(({ property, value, index }) => { + return CSSDeclaration({ + key: "remove-" + property + index, + className: "level diff-remove", + property, + value, + }); + }); + + const additions = add + // Sorting changed declarations in the order they appear in the Rules view. + .sort((a, b) => a.index > b.index) + .map(({ property, value, index }) => { + return CSSDeclaration({ + key: "add-" + property + index, + className: "level diff-add", + property, + value, + }); + }); + + return [removals, additions]; + } + + renderRule(ruleId, rule, level = 0) { + const diffClass = rule.isNew ? "diff-add" : ""; + return dom.div( + { + key: ruleId, + className: "changes__rule devtools-monospace", + "data-rule-id": ruleId, + style: { + "--diff-level": level, + }, + }, + this.renderSelectors(rule.selectors, rule.isNew), + this.renderCopyButton(ruleId), + // Render any nested child rules if they exist. + rule.children.map(childRule => { + return this.renderRule(childRule.ruleId, childRule, level + 1); + }), + // Render any changed CSS declarations. + this.renderDeclarations(rule.remove, rule.add), + // Render the closing bracket with a diff marker if necessary. + dom.div({ className: `level ${diffClass}` }, "}") + ); + } + + /** + * Return an array of React elements for the rule's selector. + * + * @param {Array} selectors + * List of strings as versions of this rule's selector over time. + * @param {Boolean} isNewRule + * Whether the rule was created at runtime. + * @return {Array} + */ + renderSelectors(selectors, isNewRule) { + const selectorDiffClassMap = new Map(); + + // The selectors array has just one item if it hasn't changed. Render it as-is. + // If the rule was created at runtime, mark the single selector as added. + // If it has two or more items, the first item was the original selector (mark as + // removed) and the last item is the current selector (mark as added). + if (selectors.length === 1) { + selectorDiffClassMap.set(selectors[0], isNewRule ? "diff-add" : ""); + } else if (selectors.length >= 2) { + selectorDiffClassMap.set(selectors[0], "diff-remove"); + selectorDiffClassMap.set(selectors[selectors.length - 1], "diff-add"); + } + + const elements = []; + + for (const [selector, diffClass] of selectorDiffClassMap) { + elements.push( + dom.div( + { + key: selector, + className: `level changes__selector ${diffClass}`, + title: selector, + }, + selector, + dom.span({}, " {") + ) + ); + } + + return elements; + } + + renderDiff(changes = {}) { + // Render groups of style sources: stylesheets and element style attributes. + return Object.entries(changes).map(([sourceId, source]) => { + const path = getSourceForDisplay(source); + const { href, rules, isFramed } = source; + + return dom.div( + { + key: sourceId, + "data-source-id": sourceId, + className: "source", + }, + dom.div( + { + className: "href", + title: href, + }, + dom.span({}, path), + isFramed && this.renderFrameBadge(href) + ), + // Render changed rules within this source. + Object.entries(rules).map(([ruleId, rule]) => { + return this.renderRule(ruleId, rule); + }) + ); + }); + } + + renderFrameBadge(href = "") { + return dom.span( + { + className: "inspector-badge", + title: href, + }, + getStr("changes.iframeLabel") + ); + } + + renderEmptyState() { + return dom.div( + { className: "devtools-sidepanel-no-result" }, + dom.p({}, getStr("changes.noChanges")), + dom.p({}, getStr("changes.noChangesDescription")) + ); + } + + render() { + const hasChanges = !!Object.keys(this.props.changesTree).length; + return dom.div( + { + className: "theme-sidebar inspector-tabpanel", + id: "sidebar-panel-changes", + role: "document", + tabIndex: "0", + onContextMenu: this.props.onContextMenu, + }, + !hasChanges && this.renderEmptyState(), + hasChanges && this.renderCopyAllChangesButton(), + hasChanges && this.renderDiff(this.props.changesTree) + ); + } +} + +const mapStateToProps = state => { + return { + changesTree: getChangesTree(state.changes), + }; +}; + +module.exports = connect(mapStateToProps)(ChangesApp); diff --git a/devtools/client/inspector/changes/components/moz.build b/devtools/client/inspector/changes/components/moz.build new file mode 100644 index 0000000000..e8fba36fb8 --- /dev/null +++ b/devtools/client/inspector/changes/components/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "ChangesApp.js", + "CSSDeclaration.js", +) diff --git a/devtools/client/inspector/changes/moz.build b/devtools/client/inspector/changes/moz.build new file mode 100644 index 0000000000..7d504f13f5 --- /dev/null +++ b/devtools/client/inspector/changes/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "actions", + "components", + "reducers", + "selectors", + "utils", +] + +DevToolsModules( + "ChangesContextMenu.js", + "ChangesView.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] + +with Files("**"): + BUG_COMPONENT = ("DevTools", "Inspector: Changes") diff --git a/devtools/client/inspector/changes/reducers/changes.js b/devtools/client/inspector/changes/reducers/changes.js new file mode 100644 index 0000000000..23e82a3ba7 --- /dev/null +++ b/devtools/client/inspector/changes/reducers/changes.js @@ -0,0 +1,385 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + RESET_CHANGES, + TRACK_CHANGE, +} = require("resource://devtools/client/inspector/changes/actions/index.js"); + +/** + * Return a deep clone of the given state object. + * + * @param {Object} state + * @return {Object} + */ +function cloneState(state = {}) { + return Object.entries(state).reduce((sources, [sourceId, source]) => { + sources[sourceId] = { + ...source, + rules: Object.entries(source.rules).reduce((rules, [ruleId, rule]) => { + rules[ruleId] = { + ...rule, + selectors: rule.selectors.slice(0), + children: rule.children.slice(0), + add: rule.add.slice(0), + remove: rule.remove.slice(0), + }; + + return rules; + }, {}), + }; + + return sources; + }, {}); +} + +/** + * Given information about a CSS rule and its ancestor rules (@media, @supports, etc), + * create entries in the given rules collection for each rule and assign parent/child + * dependencies. + * + * @param {Object} ruleData + * Information about a CSS rule: + * { + * id: {String} + * Unique rule id. + * selectors: {Array} + * Array of CSS selector text + * ancestors: {Array} + * Flattened CSS rule tree of the rule's ancestors with the root rule + * at the beginning of the array and the leaf rule at the end. + * ruleIndex: {Array} + * Indexes of each ancestor rule within its parent rule. + * } + * + * @param {Object} rules + * Collection of rules to be mutated. + * This is a reference to the corresponding `rules` object from the state. + * + * @return {Object} + * Entry for the CSS rule created the given collection of rules. + */ +function createRule(ruleData, rules) { + // Append the rule data to the flattened CSS rule tree with its ancestors. + const ruleAncestry = [...ruleData.ancestors, { ...ruleData }]; + + return ( + ruleAncestry + .map((rule, index) => { + // Ensure each rule has ancestors excluding itself (expand the flattened rule tree). + rule.ancestors = ruleAncestry.slice(0, index); + // Ensure each rule has a selector text. + // For the purpose of displaying in the UI, we treat at-rules as selectors. + if (!rule.selectors || !rule.selectors.length) { + // Display the @type label if there's one + let selector = rule.typeName ? rule.typeName + " " : ""; + selector += + rule.conditionText || + rule.name || + rule.keyText || + rule.selectorText; + + rule.selectors = [selector]; + } + + return rule.id; + }) + // Then, create new entries in the rules collection and assign dependencies. + .map((ruleId, index, array) => { + const { selectors } = ruleAncestry[index]; + const prevRuleId = array[index - 1]; + const nextRuleId = array[index + 1]; + + // Create an entry for this ruleId if one does not exist. + if (!rules[ruleId]) { + rules[ruleId] = { + ruleId, + isNew: false, + selectors, + add: [], + remove: [], + children: [], + parent: null, + }; + } + + // The next ruleId is lower in the rule tree, therefore it's a child of this rule. + if (nextRuleId && !rules[ruleId].children.includes(nextRuleId)) { + rules[ruleId].children.push(nextRuleId); + } + + // The previous ruleId is higher in the rule tree, therefore it's the parent. + if (prevRuleId) { + rules[ruleId].parent = prevRuleId; + } + + return rules[ruleId]; + }) + // Finally, return the last rule in the array which is the rule we set out to create. + .pop() + ); +} + +function removeRule(ruleId, rules) { + const rule = rules[ruleId]; + + // First, remove this rule's id from its parent's list of children + if (rule.parent && rules[rule.parent]) { + rules[rule.parent].children = rules[rule.parent].children.filter( + childRuleId => { + return childRuleId !== ruleId; + } + ); + + // Remove the parent rule if it has no children left. + if (!rules[rule.parent].children.length) { + removeRule(rule.parent, rules); + } + } + + delete rules[ruleId]; +} + +/** + * Aggregated changes grouped by sources (stylesheet/element), which contain rules, + * which contain collections of added and removed CSS declarations. + * + * Structure: + * <sourceId>: { + * type: // {String} One of: "stylesheet", "inline" or "element" + * href: // {String|null} Stylesheet or document URL; null for inline stylesheets + * rules: { + * <ruleId>: { + * ruleId: // {String} <ruleId> of this rule + * isNew: // {Boolean} Whether the tracked rule was created at runtime, + * // meaning it didn't originally exist in the source. + * selectors: // {Array} of CSS selectors or CSS at-rule text. + * // The array has just one item if the selector is never + * // changed. When the rule's selector is changed, the new + * // selector is pushed onto this array. + * children: [] // {Array} of <ruleId> for child rules of this rule + * parent: // {String} <ruleId> of the parent rule + * add: [ // {Array} of objects with CSS declarations + * { + * property: // {String} CSS property name + * value: // {String} CSS property value + * index: // {Number} Position of the declaration within its CSS rule + * } + * ... // more declarations + * ], + * remove: [] // {Array} of objects with CSS declarations + * } + * ... // more rules + * } + * } + * ... // more sources + */ +const INITIAL_STATE = {}; + +const reducers = { + /** + * CSS changes are collected on the server by the ChangesActor which dispatches them to + * the client as atomic operations: a rule/declaration updated, added or removed. + * + * By design, the ChangesActor has no big-picture context of all the collected changes. + * It only holds the stack of atomic changes. This makes it roboust for many use cases: + * building a diff-view, supporting undo/redo, offline persistence, etc. Consumers, + * like the Changes panel, get to massage the data for their particular purposes. + * + * Here in the reducer, we aggregate incoming changes to build a human-readable diff + * shown in the Changes panel. + * - added / removed declarations are grouped by CSS rule. Rules are grouped by their + * parent rules (@media, @supports, @keyframes, etc.); Rules belong to sources + * (stylesheets, inline styles) + * - declarations have an index corresponding to their position in the CSS rule. This + * allows tracking of multiple declarations with the same property name. + * - repeated changes a declaration will show only the original removal and the latest + * addition; + * - when a declaration is removed, we update the indices of other tracked declarations + * in the same rule which may have changed position in the rule as a result; + * - changes which cancel each other out (i.e. return to original) are both removed + * from the store; + * - when changes cancel each other out leaving the rule unchanged, the rule is removed + * from the store. Its parent rule is removed as well if it too ends up unchanged. + */ + // eslint-disable-next-line complexity + [TRACK_CHANGE](state, { change }) { + const defaults = { + selector: null, + source: {}, + ancestors: [], + add: [], + remove: [], + }; + + change = { ...defaults, ...change }; + state = cloneState(state); + + const { selector, ancestors, ruleIndex } = change; + const sourceId = change.source.id; + const ruleId = change.id; + + // Copy or create object identifying the source (styelsheet/element) for this change. + const source = Object.assign({}, state[sourceId], change.source); + // Copy or create collection of all rules ever changed in this source. + const rules = Object.assign({}, source.rules); + // Reference or create object identifying the rule for this change. + const rule = rules[ruleId] + ? rules[ruleId] + : createRule( + { id: change.id, selectors: [selector], ancestors, ruleIndex }, + rules + ); + + // Mark the rule if it was created at runtime as a result of an "Add Rule" action. + if (change.type === "rule-add") { + rule.isNew = true; + } + + // If the first selector tracked for this rule is identical to the incoming selector, + // reduce the selectors array to a single one. This handles the case for renaming a + // selector back to its original name. It has no side effects for other changes which + // preserve the selector. + // If the rule was created at runtime, always reduce the selectors array to one item. + // Changes to the new rule's selector always overwrite the original selector. + // If the selectors are different, push the incoming one to the end of the array to + // signify that the rule has changed selector. The last item is the current selector. + if (rule.selectors[0] === selector || rule.isNew) { + rule.selectors = [selector]; + } else { + rule.selectors.push(selector); + } + + if (change.remove?.length) { + for (const decl of change.remove) { + // Find the position of any added declaration which matches the incoming + // declaration to be removed. + const addIndex = rule.add.findIndex(addDecl => { + return ( + addDecl.index === decl.index && + addDecl.property === decl.property && + addDecl.value === decl.value + ); + }); + + // Find the position of any removed declaration which matches the incoming + // declaration to be removed. It's possible to get duplicate remove operations + // when, for example, disabling a declaration then deleting it. + const removeIndex = rule.remove.findIndex(removeDecl => { + return ( + removeDecl.index === decl.index && + removeDecl.property === decl.property && + removeDecl.value === decl.value + ); + }); + + // Track the remove operation only if the property was not previously introduced + // by an add operation. This ensures repeated changes of the same property + // register as a single remove operation of its original value. Avoid tracking the + // remove declaration if already tracked (happens on disable followed by delete). + if (addIndex < 0 && removeIndex < 0) { + rule.remove.push(decl); + } + + // Delete any previous add operation which would be canceled out by this remove. + if (rule.add[addIndex]) { + rule.add.splice(addIndex, 1); + } + + // Update the indexes of previously tracked declarations which follow this removed + // one so future tracking continues to point to the right declarations. + if (change.type === "declaration-remove") { + rule.add = rule.add.map(addDecl => { + if (addDecl.index > decl.index) { + addDecl.index--; + } + + return addDecl; + }); + + rule.remove = rule.remove.map(removeDecl => { + if (removeDecl.index > decl.index) { + removeDecl.index--; + } + + return removeDecl; + }); + } + } + } + + if (change.add?.length) { + for (const decl of change.add) { + // Find the position of any removed declaration which matches the incoming + // declaration to be added. + const removeIndex = rule.remove.findIndex(removeDecl => { + return ( + removeDecl.index === decl.index && + removeDecl.value === decl.value && + removeDecl.property === decl.property + ); + }); + + // Find the position of any added declaration which matches the incoming + // declaration to be added in case we need to replace it. + const addIndex = rule.add.findIndex(addDecl => { + return ( + addDecl.index === decl.index && addDecl.property === decl.property + ); + }); + + if (rule.remove[removeIndex]) { + // Delete any previous remove operation which would be canceled out by this add. + rule.remove.splice(removeIndex, 1); + } else if (rule.add[addIndex]) { + // Replace previous add operation for declaration at this index. + rule.add.splice(addIndex, 1, decl); + } else { + // Track new add operation. + rule.add.push(decl); + } + } + } + + // Remove the rule if none of its declarations or selector have changed, + // but skip cleanup if the selector is in process of being renamed (there are two + // changes happening in quick succession: selector-remove + selector-add) or if the + // rule was created at runtime (allow empty new rules to persist). + if ( + !rule.add.length && + !rule.remove.length && + rule.selectors.length === 1 && + !change.type.startsWith("selector-") && + !rule.isNew + ) { + removeRule(ruleId, rules); + source.rules = { ...rules }; + } else { + source.rules = { ...rules, [ruleId]: rule }; + } + + // Remove information about the source if none of its rules changed. + if (!Object.keys(source.rules).length) { + delete state[sourceId]; + } else { + state[sourceId] = source; + } + + return state; + }, + + [RESET_CHANGES](state) { + return INITIAL_STATE; + }, +}; + +module.exports = function (state = INITIAL_STATE, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return state; + } + return reducer(state, action); +}; diff --git a/devtools/client/inspector/changes/reducers/moz.build b/devtools/client/inspector/changes/reducers/moz.build new file mode 100644 index 0000000000..f3ea9a1bfc --- /dev/null +++ b/devtools/client/inspector/changes/reducers/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "changes.js", +) diff --git a/devtools/client/inspector/changes/selectors/changes.js b/devtools/client/inspector/changes/selectors/changes.js new file mode 100644 index 0000000000..a6b99e4579 --- /dev/null +++ b/devtools/client/inspector/changes/selectors/changes.js @@ -0,0 +1,261 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +loader.lazyRequireGetter( + this, + "getTabPrefs", + "resource://devtools/shared/indentation.js", + true +); + +const { + getSourceForDisplay, +} = require("resource://devtools/client/inspector/changes/utils/changes-utils.js"); + +/** + * In the Redux state, changed CSS rules are grouped by source (stylesheet) and stored in + * a single level array, regardless of nesting. + * This method returns a nested tree structure of the changed CSS rules so the React + * consumer components can traverse it easier when rendering the nested CSS rules view. + * Keeping this interface updated allows the Redux state structure to change without + * affecting the consumer components. + * + * @param {Object} state + * Redux slice for tracked changes. + * @param {Object} filter + * Object with optional filters to use. Has the following properties: + * - sourceIds: {Array} + * Use only subtrees of sources matching source ids from this array. + * - ruleIds: {Array} + * Use only rules matching rule ids from this array. If the array includes ids + * of ancestor rules (@media, @supports), their nested rules will be included. + * @return {Object} + */ +function getChangesTree(state, filter = {}) { + // Use or assign defaults of sourceId and ruleId arrays by which to filter the tree. + const { sourceIds: sourceIdsFilter = [], ruleIds: rulesIdsFilter = [] } = + filter; + /** + * Recursively replace a rule's array of child rule ids with the referenced child rules. + * Mark visited rules so as not to handle them (and their children) again. + * + * Returns the rule object with expanded children or null if previously visited. + * + * @param {String} ruleId + * @param {Object} rule + * @param {Array} rules + * @param {Set} visitedRules + * @return {Object|null} + */ + function expandRuleChildren(ruleId, rule, rules, visitedRules) { + if (visitedRules.has(ruleId)) { + return null; + } + + visitedRules.add(ruleId); + + return { + ...rule, + children: rule.children.map(childRuleId => + expandRuleChildren(childRuleId, rules[childRuleId], rules, visitedRules) + ), + }; + } + + return Object.entries(state) + .filter(([sourceId, source]) => { + // Use only matching sources if an array to filter by was provided. + if (sourceIdsFilter.length) { + return sourceIdsFilter.includes(sourceId); + } + + return true; + }) + .reduce((sourcesObj, [sourceId, source]) => { + const { rules } = source; + // Log of visited rules in this source. Helps avoid duplication when traversing the + // descendant rule tree. This Set is unique per source. It will be passed down to + // be populated with ids of rules once visited. This ensures that only visited rules + // unique to this source will be skipped and prevents skipping identical rules from + // other sources (ex: rules with the same selector and the same index). + const visitedRules = new Set(); + + // Build a new collection of sources keyed by source id. + sourcesObj[sourceId] = { + ...source, + // Build a new collection of rules keyed by rule id. + rules: Object.entries(rules) + .filter(([ruleId, rule]) => { + // Use only matching rules if an array to filter by was provided. + if (rulesIdsFilter.length) { + return rulesIdsFilter.includes(ruleId); + } + + return true; + }) + .reduce((rulesObj, [ruleId, rule]) => { + // Expand the rule's array of child rule ids with the referenced child rules. + // Skip exposing null values which mean the rule was previously visited + // as part of an ancestor descendant tree. + const expandedRule = expandRuleChildren( + ruleId, + rule, + rules, + visitedRules + ); + if (expandedRule !== null) { + rulesObj[ruleId] = expandedRule; + } + + return rulesObj; + }, {}), + }; + + return sourcesObj; + }, {}); +} + +/** + * Build the CSS text of a stylesheet with the changes aggregated in the Redux state. + * If filters for rule id or source id are provided, restrict the changes to the matching + * sources and rules. + * + * Code comments with the source origin are put above of the CSS rule (or group of + * rules). Removed CSS declarations are written commented out. Added CSS declarations are + * written as-is. + * + * @param {Object} state + * Redux slice for tracked changes. + * @param {Object} filter + * Object with optional source and rule filters. See getChangesTree() + * @return {String} + * CSS stylesheet text. + */ + +// For stylesheet sources, the stylesheet filename and full path are used: +// +// /* styles.css | https://example.com/styles.css */ +// +// .selector { +// /* property: oldvalue; */ +// property: value; +// } + +// For inline stylesheet sources, the stylesheet index and host document URL are used: +// +// /* Inline #1 | https://example.com */ +// +// .selector { +// /* property: oldvalue; */ +// property: value; +// } + +// For element style attribute sources, the unique selector generated for the element +// and the host document URL are used: +// +// /* Element (div) | https://example.com */ +// +// div:nth-child(1) { +// /* property: oldvalue; */ +// property: value; +// } +function getChangesStylesheet(state, filter) { + const changeTree = getChangesTree(state, filter); + // Get user prefs about indentation style. + const { indentUnit, indentWithTabs } = getTabPrefs(); + const indentChar = indentWithTabs + ? "\t".repeat(indentUnit) + : " ".repeat(indentUnit); + + /** + * If the rule has just one item in its array of selector versions, return it as-is. + * If it has more than one, build a string using the first selector commented-out + * and the last selector as-is. This indicates that a rule's selector has changed. + * + * @param {Array} selectors + * History of selector versions if changed over time. + * Array with a single item (the original selector) if never changed. + * @param {Number} level + * Level of nesting within a CSS rule tree. + * @return {String} + */ + function writeSelector(selectors = [], level) { + const indent = indentChar.repeat(level); + let selectorText; + switch (selectors.length) { + case 0: + selectorText = ""; + break; + case 1: + selectorText = `${indent}${selectors[0]}`; + break; + default: + selectorText = + `${indent}/* ${selectors[0]} { */\n` + + `${indent}${selectors[selectors.length - 1]}`; + } + + return selectorText; + } + + function writeRule(ruleId, rule, level) { + // Write nested rules, if any. + let ruleBody = rule.children.reduce((str, childRule) => { + str += writeRule(childRule.ruleId, childRule, level + 1); + return str; + }, ""); + + // Write changed CSS declarations. + ruleBody += writeDeclarations(rule.remove, rule.add, level + 1); + + const indent = indentChar.repeat(level); + const selectorText = writeSelector(rule.selectors, level); + return `\n${selectorText} {${ruleBody}\n${indent}}`; + } + + function writeDeclarations(remove = [], add = [], level) { + const indent = indentChar.repeat(level); + const removals = remove + // Sort declarations in the order in which they exist in the original CSS rule. + .sort((a, b) => a.index > b.index) + .reduce((str, { property, value }) => { + str += `\n${indent}/* ${property}: ${value}; */`; + return str; + }, ""); + + const additions = add + // Sort declarations in the order in which they exist in the original CSS rule. + .sort((a, b) => a.index > b.index) + .reduce((str, { property, value }) => { + str += `\n${indent}${property}: ${value};`; + return str; + }, ""); + + return removals + additions; + } + + // Iterate through all sources in the change tree and build a CSS stylesheet string. + return Object.entries(changeTree).reduce( + (stylesheetText, [sourceId, source]) => { + const { href, rules } = source; + // Write code comment with source origin + stylesheetText += `\n/* ${getSourceForDisplay(source)} | ${href} */\n`; + // Write CSS rules + stylesheetText += Object.entries(rules).reduce((str, [ruleId, rule]) => { + // Add a new like only after top-level rules (level == 0) + str += writeRule(ruleId, rule, 0) + "\n"; + return str; + }, ""); + + return stylesheetText; + }, + "" + ); +} + +module.exports = { + getChangesTree, + getChangesStylesheet, +}; diff --git a/devtools/client/inspector/changes/selectors/moz.build b/devtools/client/inspector/changes/selectors/moz.build new file mode 100644 index 0000000000..f3ea9a1bfc --- /dev/null +++ b/devtools/client/inspector/changes/selectors/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "changes.js", +) diff --git a/devtools/client/inspector/changes/test/browser.toml b/devtools/client/inspector/changes/test/browser.toml new file mode 100644 index 0000000000..407a960e92 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser.toml @@ -0,0 +1,45 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/inspector/rules/test/head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_changes_background_tracking.js"] + +["browser_changes_copy_all_changes.js"] + +["browser_changes_copy_declaration.js"] + +["browser_changes_copy_rule.js"] + +["browser_changes_declaration_add_special_character.js"] + +["browser_changes_declaration_disable.js"] + +["browser_changes_declaration_duplicate.js"] + +["browser_changes_declaration_edit_value.js"] + +["browser_changes_declaration_identical_rules.js"] +skip-if = ["a11y_checks"] # Bugs 1858041 and 1849028 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try) + +["browser_changes_declaration_remove.js"] + +["browser_changes_declaration_remove_ahead.js"] + +["browser_changes_declaration_remove_disabled.js"] + +["browser_changes_declaration_rename.js"] + +["browser_changes_nested_rules.js"] + +["browser_changes_rule_add.js"] + +["browser_changes_rule_selector.js"] diff --git a/devtools/client/inspector/changes/test/browser_changes_background_tracking.js b/devtools/client/inspector/changes/test/browser_changes_background_tracking.js new file mode 100644 index 0000000000..8ebac2a23a --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_background_tracking.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that CSS changes are collected in the background without the Changes panel visible + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div></div> +`; + +add_task(async function () { + info("Ensure Changes panel is NOT the default panel; use Computed panel"); + await pushPref("devtools.inspector.activeSidebar", "computedview"); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + + await selectNode("div", inspector); + const prop = getTextProperty(ruleView, 1, { color: "red" }); + + info("Disable the first CSS declaration"); + await togglePropStatus(ruleView, prop); + + info("Select the Changes panel"); + const { document: doc, store } = selectChangesView(inspector); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + const onResetChanges = waitForDispatch(store, "RESET_CHANGES"); + + info("Wait for change to be tracked"); + await onTrackChange; + const removedDeclarations = getRemovedDeclarations(doc); + is(removedDeclarations.length, 1, "One declaration was tracked as removed"); + + // Test for Bug 1656477. Check that changes are not cleared immediately afterwards. + info("Wait to see if the RESET_CHANGES action is dispatched unexpecteldy"); + const sym = Symbol(); + const onTimeout = wait(500).then(() => sym); + const raceResult = await Promise.any([onResetChanges, onTimeout]); + Assert.strictEqual(raceResult, sym, "RESET_CHANGES has not been dispatched"); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_copy_all_changes.js b/devtools/client/inspector/changes/test/browser_changes_copy_all_changes.js new file mode 100644 index 0000000000..119fe22585 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_copy_all_changes.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Changes panel Copy All Changes button will populate the +// clipboard with a summary of just the changed declarations. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + margin: 0; + } + </style> + <div></div> +`; + +// Indentation is important. A strict check will be done against the clipboard content. +const EXPECTED_CLIPBOARD = ` +/* Inline #0 | data:text/html;charset=utf-8,${TEST_URI} */ + +div { + /* color: red; */ + color: green; +} +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const changesView = selectChangesView(inspector); + const { document: panelDoc, store } = changesView; + + await selectNode("div", inspector); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration(ruleView, 1, { color: "red" }, { color: "green" }); + await onTrackChange; + + info( + "Check that clicking the Copy All Changes button copies all changes to the clipboard." + ); + const button = panelDoc.querySelector(".changes__copy-all-changes-button"); + await waitForClipboardPromise( + () => button.click(), + () => checkClipboardData(EXPECTED_CLIPBOARD) + ); +}); + +function checkClipboardData(expected) { + const actual = SpecialPowers.getClipboardData("text/plain"); + return decodeURIComponent(actual).trim() === expected.trim(); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_copy_declaration.js b/devtools/client/inspector/changes/test/browser_changes_copy_declaration.js new file mode 100644 index 0000000000..0f2c7f68e6 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_copy_declaration.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Changes panel Copy Declaration context menu item will populate the +// clipboard with the changed declaration. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + margin: 0; + } + </style> + <div></div> +`; + +const EXPECTED_CLIPBOARD_REMOVED = `/* color: red; */`; +const EXPECTED_CLIPBOARD_ADDED = `color: green;`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const changesView = selectChangesView(inspector); + const { document: panelDoc, store } = changesView; + + await selectNode("div", inspector); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration(ruleView, 1, { color: "red" }, { color: "green" }); + await onTrackChange; + + info( + "Click the Copy Declaration context menu item for the removed declaration" + ); + const removeDecl = getRemovedDeclarations(panelDoc); + const addDecl = getAddedDeclarations(panelDoc); + + let menu = await getChangesContextMenu(changesView, removeDecl[0].element); + let menuItem = menu.items.find( + item => item.id === "changes-contextmenu-copy-declaration" + ); + await waitForClipboardPromise( + () => menuItem.click(), + () => checkClipboardData(EXPECTED_CLIPBOARD_REMOVED) + ); + + info("Hiding menu"); + menu.hide(document); + + info( + "Click the Copy Declaration context menu item for the added declaration" + ); + menu = await getChangesContextMenu(changesView, addDecl[0].element); + menuItem = menu.items.find( + item => item.id === "changes-contextmenu-copy-declaration" + ); + await waitForClipboardPromise( + () => menuItem.click(), + () => checkClipboardData(EXPECTED_CLIPBOARD_ADDED) + ); +}); + +function checkClipboardData(expected) { + const actual = SpecialPowers.getClipboardData("text/plain"); + return actual.trim() === expected.trim(); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_copy_rule.js b/devtools/client/inspector/changes/test/browser_changes_copy_rule.js new file mode 100644 index 0000000000..4c2a347e8e --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_copy_rule.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Changes panel Copy Rule button and context menu will populate the +// clipboard with the entire contents of the changed rule, including unchanged properties. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + margin: 0; + } + </style> + <div></div> +`; + +// Indentation is important. A strict check will be done against the clipboard content. +const EXPECTED_CLIPBOARD = ` + div { + color: green; + margin: 0; + } +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const changesView = selectChangesView(inspector); + const { document: panelDoc, store } = changesView; + + await selectNode("div", inspector); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration(ruleView, 1, { color: "red" }, { color: "green" }); + await onTrackChange; + + info("Click the Copy Rule button and expect the changed rule on clipboard"); + const button = panelDoc.querySelector(".changes__copy-rule-button"); + await waitForClipboardPromise( + () => button.click(), + () => checkClipboardData(EXPECTED_CLIPBOARD) + ); + + emptyClipboard(); + + info( + "Click the Copy Rule context menu item and expect the changed rule on the clipboard" + ); + const addDecl = getAddedDeclarations(panelDoc); + const menu = await getChangesContextMenu(changesView, addDecl[0].element); + const menuItem = menu.items.find( + item => item.id === "changes-contextmenu-copy-rule" + ); + await waitForClipboardPromise( + () => menuItem.click(), + () => checkClipboardData(EXPECTED_CLIPBOARD) + ); +}); + +function checkClipboardData(expected) { + const actual = SpecialPowers.getClipboardData("text/plain"); + return actual.trim() === expected.trim(); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_add_special_character.js b/devtools/client/inspector/changes/test/browser_changes_declaration_add_special_character.js new file mode 100644 index 0000000000..65b0092b9c --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_add_special_character.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that adding new CSS properties with special characters in the property +// name does note create duplicate entries. + +const PROPERTY_NAME = '"abc"'; +const INITIAL_VALUE = "foo"; +// For assertions the quotes in the property will be escaped. +const EXPECTED_PROPERTY_NAME = '\\"abc\\"'; + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div>test</div> +`; + +add_task(async function addWithSpecialCharacter() { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + + const ruleEditor = getRuleViewRuleEditor(ruleView, 1); + const editor = await focusEditableField(ruleView, ruleEditor.closeBrace); + + const input = editor.input; + input.value = `${PROPERTY_NAME}: ${INITIAL_VALUE};`; + + let onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Pressing return to commit and focus the new value field"); + const onModifications = ruleView.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, ruleView.styleWindow); + await onModifications; + await onTrackChange; + await assertAddedDeclaration(doc, EXPECTED_PROPERTY_NAME, INITIAL_VALUE); + + let newValue = "def"; + info(`Change the CSS declaration value to ${newValue}`); + const prop = getTextProperty(ruleView, 1, { [PROPERTY_NAME]: INITIAL_VALUE }); + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + // flushCount needs to be set to 2 once when quotes are involved. + await setProperty(ruleView, prop, newValue, { flushCount: 2 }); + await onTrackChange; + await assertAddedDeclaration(doc, EXPECTED_PROPERTY_NAME, newValue); + + newValue = "123"; + info(`Change the CSS declaration value to ${newValue}`); + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + // 2 preview requests to flush: one for the new value and one for the + // autocomplete popup suggestion (even if no suggestion is displayed here). + await setProperty(ruleView, prop, newValue, { flushCount: 2 }); + await onTrackChange; + await assertAddedDeclaration(doc, EXPECTED_PROPERTY_NAME, newValue); +}); + +/** + * Check that we only received a single added declaration with the expected + * value. + */ +async function assertAddedDeclaration(doc, expectedName, expectedValue) { + await waitFor(() => { + const addDecl = getAddedDeclarations(doc); + return ( + addDecl.length == 1 && + addDecl[0].value == expectedValue && + addDecl[0].property == expectedName + ); + }, "Got the expected declaration"); + is(getAddedDeclarations(doc).length, 1, "Only one added declaration"); + is(getRemovedDeclarations(doc).length, 0, "No removed declaration"); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_disable.js b/devtools/client/inspector/changes/test/browser_changes_declaration_disable.js new file mode 100644 index 0000000000..4c7141cdc6 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_disable.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that toggling a CSS declaration in the Rule view is tracked. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop = getTextProperty(ruleView, 1, { color: "red" }); + + let onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the first declaration"); + await togglePropStatus(ruleView, prop); + info("Wait for change to be tracked"); + await onTrackChange; + + let removedDeclarations = getRemovedDeclarations(doc); + is( + removedDeclarations.length, + 1, + "Only one declaration was tracked as removed" + ); + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Re-enable the first declaration"); + await togglePropStatus(ruleView, prop); + info("Wait for change to be tracked"); + await onTrackChange; + + const addedDeclarations = getAddedDeclarations(doc); + removedDeclarations = getRemovedDeclarations(doc); + is(addedDeclarations.length, 0, "No declarations were tracked as added"); + is(removedDeclarations.length, 0, "No declarations were tracked as removed"); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_duplicate.js b/devtools/client/inspector/changes/test/browser_changes_declaration_duplicate.js new file mode 100644 index 0000000000..1d3423992e --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_duplicate.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that adding duplicate declarations to the Rule view is shown in the Changes panel. + +const TEST_URI = ` + <style type='text/css'> + div { + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + await testAddDuplicateDeclarations(ruleView, store, doc); + await testChangeDuplicateDeclarations(ruleView, store, doc); + await testRemoveDuplicateDeclarations(ruleView, store, doc); +}); + +async function testAddDuplicateDeclarations(ruleView, store, doc) { + info(`Test that adding declarations with the same property name and value + are both tracked.`); + + let onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Add CSS declaration"); + await addProperty(ruleView, 1, "color", "red"); + info("Wait for the change to be tracked"); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Add duplicate CSS declaration"); + await addProperty(ruleView, 1, "color", "red"); + info("Wait for the change to be tracked"); + await onTrackChange; + + await waitFor(() => { + const decls = getAddedDeclarations(doc); + return decls.length == 2 && decls[1].value == "red"; + }, "Two declarations were tracked as added"); + const addDecl = getAddedDeclarations(doc); + is(addDecl[0].value, "red", "First declaration has correct property value"); + is( + addDecl[0].value, + addDecl[1].value, + "First and second declarations have identical property values" + ); +} + +async function testChangeDuplicateDeclarations(ruleView, store, doc) { + info( + "Test that changing one of the duplicate declarations won't change the other" + ); + const prop = getTextProperty(ruleView, 1, { color: "red" }); + + info("Change the value of the first of the duplicate declarations"); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await setProperty(ruleView, prop, "black"); + info("Wait for the change to be tracked"); + await onTrackChange; + + await waitFor( + () => getAddedDeclarations(doc).length == 2, + "Two declarations were tracked as added" + ); + const addDecl = getAddedDeclarations(doc); + is(addDecl[0].value, "black", "First declaration has changed property value"); + is( + addDecl[1].value, + "red", + "Second declaration has not changed property value" + ); +} + +async function testRemoveDuplicateDeclarations(ruleView, store, doc) { + info(`Test that removing the first of the duplicate declarations + will not remove the second.`); + + const prop = getTextProperty(ruleView, 1, { color: "black" }); + + info("Remove first declaration"); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await removeProperty(ruleView, prop); + info("Wait for the change to be tracked"); + await onTrackChange; + + await waitFor( + () => getAddedDeclarations(doc).length == 1, + "One declaration was tracked as added" + ); + const addDecl = getAddedDeclarations(doc); + const removeDecl = getRemovedDeclarations(doc); + // Expect no remove operation tracked because it cancels out the original add operation. + is(removeDecl.length, 0, "No declaration was tracked as removed"); + is(addDecl.length, 1, "Just one declaration left tracked as added"); + is( + addDecl[0].value, + "red", + "Leftover declaration has property value of the former second declaration" + ); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_edit_value.js b/devtools/client/inspector/changes/test/browser_changes_declaration_edit_value.js new file mode 100644 index 0000000000..588513a274 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_edit_value.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing the value of a CSS declaration in the Rule view is tracked. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + font-family: "courier"; + } + </style> + <div>test</div> +`; + +/* + This object contains iteration steps to modify various CSS properties of the + test element, keyed by property name,. + Each value is an array which will be iterated over in order and the `value` + property will be used to update the value of the property. + The `add` and `remove` objects hold the expected values of the tracked declarations + shown in the Changes panel. If `add` or `remove` are null, it means we don't expect + any corresponding tracked declaration to show up in the Changes panel. + */ +const ITERATIONS = { + color: [ + // No changes should be tracked if the value did not actually change. + { + value: "red", + add: null, + remove: null, + }, + // Changing the priority flag "!important" should be tracked. + { + value: "red !important", + add: { value: "red !important" }, + remove: { value: "red" }, + }, + // Repeated changes should still show the original value as the one removed. + { + value: "blue", + add: { value: "blue" }, + remove: { value: "red" }, + }, + // Restoring the original value should clear tracked changes. + { + value: "red", + add: null, + remove: null, + }, + ], + "font-family": [ + // Set a value with an opening quote, missing the closing one. + // The closing quote should still appear in the "add" value. + { + value: '"ar', + add: { value: '"ar"' }, + remove: { value: '"courier"' }, + // For some reason we need an additional flush the first time we set a + // value with a quote. Since the ruleview is manually flushed when opened + // openRuleView, we need to pass this information all the way down to the + // setProperty helper. + needsExtraFlush: true, + }, + // Add an escaped character + { + value: '"ar\\i', + add: { value: '"ar\\i"' }, + remove: { value: '"courier"' }, + }, + // Add some more text + { + value: '"ar\\ia', + add: { value: '"ar\\ia"' }, + remove: { value: '"courier"' }, + }, + // Remove the backslash + { + value: '"aria', + add: { value: '"aria"' }, + remove: { value: '"courier"' }, + }, + // Add the rest of the text, still no closing quote + { + value: '"arial', + add: { value: '"arial"' }, + remove: { value: '"courier"' }, + }, + // Restoring the original value should clear tracked changes. + { + value: '"courier"', + add: null, + remove: null, + }, + ], +}; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + + const colorProp = getTextProperty(ruleView, 1, { color: "red" }); + await assertEditValue(ruleView, doc, store, colorProp, ITERATIONS.color); + + const fontFamilyProp = getTextProperty(ruleView, 1, { + "font-family": '"courier"', + }); + await assertEditValue( + ruleView, + doc, + store, + fontFamilyProp, + ITERATIONS["font-family"] + ); +}); + +async function assertEditValue(ruleView, doc, store, prop, iterations) { + let onTrackChange; + for (const { value, add, needsExtraFlush, remove } of iterations) { + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + + info(`Change the CSS declaration value to ${value}`); + await setProperty(ruleView, prop, value, { + flushCount: needsExtraFlush ? 2 : 1, + }); + info("Wait for the change to be tracked"); + await onTrackChange; + + if (add) { + await waitFor(() => { + const decl = getAddedDeclarations(doc); + return decl.length == 1 && decl[0].value == add.value; + }, "Only one declaration was tracked as added."); + const addDecl = getAddedDeclarations(doc); + is( + addDecl[0].value, + add.value, + `Added declaration has expected value: ${add.value}` + ); + } else { + await waitFor( + () => !getAddedDeclarations(doc).length, + "Added declaration was cleared" + ); + } + + if (remove) { + await waitFor( + () => getRemovedDeclarations(doc).length == 1, + "Only one declaration was tracked as removed." + ); + const removeDecl = getRemovedDeclarations(doc); + is( + removeDecl[0].value, + remove.value, + `Removed declaration has expected value: ${remove.value}` + ); + } else { + await waitFor( + () => !getRemovedDeclarations(doc).length, + "Removed declaration was cleared" + ); + } + } +} diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_identical_rules.js b/devtools/client/inspector/changes/test/browser_changes_declaration_identical_rules.js new file mode 100644 index 0000000000..08ac6d173d --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_identical_rules.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test tracking changes to CSS declarations in different stylesheets but in rules +// with identical selectors. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <style type='text/css'> + div { + font-size: 1em; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop1 = getTextProperty(ruleView, 1, { "font-size": "1em" }); + const prop2 = getTextProperty(ruleView, 2, { color: "red" }); + + let onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the declaration in the first rule"); + await togglePropStatus(ruleView, prop1); + info("Wait for change to be tracked"); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the declaration in the second rule"); + await togglePropStatus(ruleView, prop2); + info("Wait for change to be tracked"); + await onTrackChange; + + const removeDecl = getRemovedDeclarations(doc); + is(removeDecl.length, 2, "Two declarations tracked as removed"); + // The last of the two matching rules shows up first in Rule view given that the + // specificity is the same. This is correct. If the properties were the same, the latest + // declaration would overwrite the first and thus show up on top. + is( + removeDecl[0].property, + "font-size", + "Correct property name for second declaration" + ); + is( + removeDecl[0].value, + "1em", + "Correct property value for second declaration" + ); + is( + removeDecl[1].property, + "color", + "Correct property name for first declaration" + ); + is( + removeDecl[1].value, + "red", + "Correct property value for first declaration" + ); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_remove.js b/devtools/client/inspector/changes/test/browser_changes_declaration_remove.js new file mode 100644 index 0000000000..60b61c3196 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_remove.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that removing a CSS declaration from a rule in the Rule view is tracked. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop = getTextProperty(ruleView, 1, { color: "red" }); + + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Remove the first declaration"); + await removeProperty(ruleView, prop); + info("Wait for change to be tracked"); + await onTrackChange; + + const removeDecl = getRemovedDeclarations(doc); + is(removeDecl.length, 1, "One declaration was tracked as removed"); + is( + removeDecl[0].property, + "color", + "Correct declaration name was tracked as removed" + ); + is( + removeDecl[0].value, + "red", + "Correct declaration value was tracked as removed" + ); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_remove_ahead.js b/devtools/client/inspector/changes/test/browser_changes_declaration_remove_ahead.js new file mode 100644 index 0000000000..b249fc8198 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_remove_ahead.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the correct declaration is identified and changed after removing a +// declaration positioned ahead of it in the same CSS rule. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + display: block; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop1 = getTextProperty(ruleView, 1, { color: "red" }); + const prop2 = getTextProperty(ruleView, 1, { display: "block" }); + + let onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Change the second declaration"); + await setProperty(ruleView, prop2, "grid"); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Remove the first declaration"); + await removeProperty(ruleView, prop1); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Change the second declaration again"); + await setProperty(ruleView, prop2, "flex"); + info("Wait for change to be tracked"); + await onTrackChange; + + // Ensure changes to the second declaration were tracked after removing the first one. + await waitFor( + () => getRemovedDeclarations(doc).length == 2, + "Two declarations should have been tracked as removed" + ); + await waitFor(() => { + const addDecl = getAddedDeclarations(doc); + return addDecl.length == 1 && addDecl[0].value == "flex"; + }, "One declaration should have been tracked as added, and the added declaration to have updated property value"); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_remove_disabled.js b/devtools/client/inspector/changes/test/browser_changes_declaration_remove_disabled.js new file mode 100644 index 0000000000..421d22cbae --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_remove_disabled.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that disabling a CSS declaration and then removing it from the Rule view +// is tracked as removed only once. Toggling leftover declarations should not introduce +// duplicate changes. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + background: black; + } + </style> + <div></div> +`; + +add_task(async function () { + await pushPref("devtools.inspector.rule-view.focusNextOnEnter", false); + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop1 = getTextProperty(ruleView, 1, { color: "red" }); + const prop2 = getTextProperty(ruleView, 1, { background: "black" }); + + info("Using the second declaration"); + await testRemoveValue(ruleView, store, doc, prop2); + info("Using the first declaration"); + await testToggle(ruleView, store, doc, prop1); + info("Using the first declaration"); + await testRemoveName(ruleView, store, doc, prop1); +}); + +async function testRemoveValue(ruleView, store, doc, prop) { + info("Test removing disabled declaration by clearing its property value."); + let onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the declaration"); + await togglePropStatus(ruleView, prop); + info("Wait for change to be tracked"); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Remove the disabled declaration by clearing its value"); + await setProperty(ruleView, prop, null); + await onTrackChange; + + const removeDecl = getRemovedDeclarations(doc); + is(removeDecl.length, 1, "Only one declaration tracked as removed"); +} + +async function testToggle(ruleView, store, doc, prop) { + info( + "Test toggling leftover declaration off and on will not track extra changes." + ); + let onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the declaration"); + await togglePropStatus(ruleView, prop); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Re-enable the declaration"); + await togglePropStatus(ruleView, prop); + await onTrackChange; + + await waitFor( + () => getRemovedDeclarations(doc).length == 1, + "Still just one declaration tracked as removed" + ); +} + +async function testRemoveName(ruleView, store, doc, prop) { + info("Test removing disabled declaration by clearing its property name."); + let onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Disable the declaration"); + await togglePropStatus(ruleView, prop); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Remove the disabled declaration by clearing its name"); + await removeProperty(ruleView, prop); + await onTrackChange; + + info(`Expecting two declarations removed: + - one removed by its value in the other test + - one removed by its name from this test + `); + + await waitFor( + () => getRemovedDeclarations(doc).length == 2, + "Two declarations tracked as removed" + ); + const removeDecl = getRemovedDeclarations(doc); + is(removeDecl[0].property, "background", "First declaration name correct"); + is(removeDecl[0].value, "black", "First declaration value correct"); + is(removeDecl[1].property, "color", "Second declaration name correct"); + is(removeDecl[1].value, "red", "Second declaration value correct"); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_declaration_rename.js b/devtools/client/inspector/changes/test/browser_changes_declaration_rename.js new file mode 100644 index 0000000000..245ce50121 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_rename.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that renaming the property of a CSS declaration in the Rule view is tracked. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + + await selectNode("div", inspector); + const prop = getTextProperty(ruleView, 1, { color: "red" }); + + let onTrackChange; + + const oldPropertyName = "color"; + const newPropertyName = "background-color"; + + info(`Rename the CSS declaration name to ${newPropertyName}`); + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await renameProperty(ruleView, prop, newPropertyName); + info("Wait for the change to be tracked"); + await onTrackChange; + + const removeDecl = getRemovedDeclarations(doc); + const addDecl = getAddedDeclarations(doc); + + is(removeDecl.length, 1, "One declaration tracked as removed"); + is( + removeDecl[0].property, + oldPropertyName, + `Removed declaration has old property name: ${oldPropertyName}` + ); + is(addDecl.length, 1, "One declaration tracked as added"); + is( + addDecl[0].property, + newPropertyName, + `Added declaration has new property name: ${newPropertyName}` + ); + + info( + `Reverting the CSS declaration name to ${oldPropertyName} should clear changes.` + ); + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await renameProperty(ruleView, prop, oldPropertyName); + info("Wait for the change to be tracked"); + await onTrackChange; + + await waitFor( + () => !getRemovedDeclarations(doc).length, + "No declaration tracked as removed" + ); + await waitFor( + () => !getAddedDeclarations(doc).length, + "No declaration tracked as added" + ); +}); diff --git a/devtools/client/inspector/changes/test/browser_changes_nested_rules.js b/devtools/client/inspector/changes/test/browser_changes_nested_rules.js new file mode 100644 index 0000000000..789d88fdda --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_nested_rules.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Changes panel works with nested rules. + +// Declare rule individually so we can use them for the assertions as well +// In the end, we should have nested rule looking like: +// - @media screen and (height > 5px) { +// -- @layer myLayer { +// --- @container myContainer (width > 10px) { +// ----- div { +// ------- & > span { … } +// ------- .mySpan { +// --------- &:not(:focus) { + +const spanNotFocusedRule = `&:not(:focus) { + text-decoration: underline; +}`; + +const spanClassRule = `.mySpan { + font-weight: bold; + ${spanNotFocusedRule} +}`; + +const spanRule = `& > span { + outline: 1px solid gold; +}`; + +const divRule = `div { + color: tomato; + ${spanRule} + ${spanClassRule} +}`; + +const containerRule = `@container myContainer (width > 10px) { + /* in container */ + ${divRule} +}`; +const layerRule = `@layer myLayer { + /* in layer */ + ${containerRule} +}`; +const mediaRule = `@media screen and (height > 5px) { + /* in media */ + ${layerRule} +}`; + +const TEST_URI = ` + <style> + body { + container: myContainer / inline-size + } + ${mediaRule} + </style> + <div>hello <span class="mySpan">world</span></div> +`; + +const applyModificationAfterDivPropertyChange = ruleText => + ruleText.replace("tomato", "cyan"); + +const EXPECTED_AFTER_DIV_PROP_CHANGE = [ + { + text: "@media screen and (height > 5px) {", + copyRuleClipboard: applyModificationAfterDivPropertyChange(mediaRule), + }, + { + text: "@layer myLayer {", + copyRuleClipboard: applyModificationAfterDivPropertyChange(layerRule), + }, + { + text: "@container myContainer (width > 10px) {", + copyRuleClipboard: applyModificationAfterDivPropertyChange(containerRule), + }, + { + text: "div {", + copyRuleClipboard: applyModificationAfterDivPropertyChange(divRule), + }, +]; + +const applyModificationAfterSpanPropertiesChange = ruleText => + ruleText + .replace("1px solid gold", "4px solid gold") + .replace("bold", "bolder") + .replace("underline", "underline dotted"); + +const EXPECTED_AFTER_SPAN_PROP_CHANGES = EXPECTED_AFTER_DIV_PROP_CHANGE.map( + expected => ({ + ...expected, + copyRuleClipboard: applyModificationAfterSpanPropertiesChange( + expected.copyRuleClipboard + ), + }) +).concat([ + { + text: ".mySpan {", + copyRuleClipboard: + applyModificationAfterSpanPropertiesChange(spanClassRule), + }, + { + text: "&:not(:focus) {", + copyRuleClipboard: + applyModificationAfterSpanPropertiesChange(spanNotFocusedRule), + }, + { + text: "& > span {", + copyRuleClipboard: applyModificationAfterSpanPropertiesChange(spanRule), + }, +]); + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const changesView = selectChangesView(inspector); + const { document: panelDoc, store } = changesView; + const panel = panelDoc.querySelector("#sidebar-panel-changes"); + + await selectNode("div", inspector); + let onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration(ruleView, 1, { color: "tomato" }, { color: "cyan" }); + await onTrackChange; + + await assertSelectors(panel, EXPECTED_AFTER_DIV_PROP_CHANGE); + + await selectNode(".mySpan", inspector); + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration( + ruleView, + 1, + { "text-decoration": "underline" }, + { "text-decoration": "underline dotted" } + ); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration( + ruleView, + 2, + { "font-weight": "bold" }, + { "font-weight": "bolder" } + ); + await onTrackChange; + + onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration( + ruleView, + 3, + { outline: "1px solid gold" }, + { outline: "4px solid gold" } + ); + await onTrackChange; + + await assertSelectors(panel, EXPECTED_AFTER_SPAN_PROP_CHANGES); +}); + +async function assertSelectors(panel, expected) { + const selectorsEl = getSelectors(panel); + + is( + selectorsEl.length, + expected.length, + "Got the expected number of selectors item" + ); + + for (let i = 0; i < expected.length; i++) { + const selectorEl = selectorsEl[i]; + const expectedItem = expected[i]; + + is( + selectorEl.innerText, + expectedItem.text, + `Got expected selector text at index ${i}` + ); + info(`Click the Copy Rule button for the "${expectedItem.text}" rule`); + const button = selectorEl + .closest(".changes__rule") + .querySelector(".changes__copy-rule-button"); + await waitForClipboardPromise( + () => button.click(), + () => checkClipboardData(expectedItem.copyRuleClipboard) + ); + } +} + +function checkClipboardData(expected) { + const actual = SpecialPowers.getClipboardData("text/plain"); + return actual.trim() === expected.trim(); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_rule_add.js b/devtools/client/inspector/changes/test/browser_changes_rule_add.js new file mode 100644 index 0000000000..215f1f3605 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_rule_add.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that adding a new CSS rule in the Rules view is tracked in the Changes panel. +// Renaming the selector of the new rule should overwrite the tracked selector. + +const TEST_URI = ` + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + const panel = doc.querySelector("#sidebar-panel-changes"); + + await selectNode("div", inspector); + await testTrackAddNewRule(store, inspector, ruleView, panel); + await testTrackRenameNewRule(store, inspector, ruleView, panel); +}); + +async function testTrackAddNewRule(store, inspector, ruleView, panel) { + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + info("Adding a new CSS rule in the Rule view"); + await addNewRule(inspector, ruleView); + info("Pressing escape to leave the editor"); + EventUtils.synthesizeKey("KEY_Escape"); + info("Waiting for changes to be tracked"); + await onTrackChange; + + const addedSelectors = getAddedSelectors(panel); + const removedSelectors = getRemovedSelectors(panel); + is(addedSelectors.length, 1, "One selector was tracked as added"); + is(addedSelectors.item(0).title, "div", "New rule's has DIV selector"); + is(removedSelectors.length, 0, "No selectors tracked as removed"); +} + +async function testTrackRenameNewRule(store, inspector, ruleView, panel) { + info("Focusing the first rule's selector name in the Rule view"); + const ruleEditor = getRuleViewRuleEditor(ruleView, 1); + const editor = await focusEditableField(ruleView, ruleEditor.selectorText); + info("Entering a new selector name"); + editor.input.value = ".test"; + + // Expect two "TRACK_CHANGE" actions: one for removal, one for addition. + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE", 2); + const onRuleViewChanged = once(ruleView, "ruleview-changed"); + EventUtils.synthesizeKey("KEY_Enter"); + await onRuleViewChanged; + info("Waiting for changes to be tracked"); + await onTrackChange; + + const addedSelectors = getAddedSelectors(panel); + const removedSelectors = getRemovedSelectors(panel); + is(addedSelectors.length, 1, "One selector was tracked as added"); + is( + addedSelectors.item(0).title, + ".test", + "New rule's selector was updated in place." + ); + is(removedSelectors.length, 0, "No selectors tracked as removed"); +} diff --git a/devtools/client/inspector/changes/test/browser_changes_rule_selector.js b/devtools/client/inspector/changes/test/browser_changes_rule_selector.js new file mode 100644 index 0000000000..20d3fba654 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_rule_selector.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that renaming the selector of a CSS rule is tracked. +// Expect a selector removal followed by a selector addition and no changed declarations + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + </style> + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view: ruleView } = await openRuleView(); + const { document: doc, store } = selectChangesView(inspector); + const panel = doc.querySelector("#sidebar-panel-changes"); + + await selectNode("div", inspector); + const ruleEditor = getRuleViewRuleEditor(ruleView, 1); + + info("Focusing the first rule's selector name in the Rule view"); + const editor = await focusEditableField(ruleView, ruleEditor.selectorText); + info("Entering a new selector name"); + editor.input.value = ".test"; + + // Expect two "TRACK_CHANGE" actions: one for removal, one for addition. + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE", 2); + const onRuleViewChanged = once(ruleView, "ruleview-changed"); + info("Pressing Enter key to commit the change"); + EventUtils.synthesizeKey("KEY_Enter"); + info("Waiting for rule view to update"); + await onRuleViewChanged; + info("Wait for the change to be tracked"); + await onTrackChange; + + const rules = panel.querySelectorAll(".changes__rule"); + is(rules.length, 1, "One rule was tracked as changed"); + + info("Expect old selector to be removed and new selector to be added"); + const addedSelectors = getAddedSelectors(panel); + const removedSelectors = getRemovedSelectors(panel); + is(addedSelectors.length, 1, "One new selector was tracked as added"); + is(addedSelectors.item(0).title, ".test", "New selector is correct"); + is(removedSelectors.length, 1, "One old selector was tracked as removed"); + is(removedSelectors.item(0).title, "div", "Old selector is correct"); + + info( + "Expect no declarations to have been added or removed during selector change" + ); + const removeDecl = getRemovedDeclarations(doc, rules.item(0)); + is(removeDecl.length, 0, "No declarations removed"); + const addDecl = getAddedDeclarations(doc, rules.item(0)); + is(addDecl.length, 0, "No declarations added"); +}); diff --git a/devtools/client/inspector/changes/test/head.js b/devtools/client/inspector/changes/test/head.js new file mode 100644 index 0000000000..b45af3ba47 --- /dev/null +++ b/devtools/client/inspector/changes/test/head.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Load the Rule view's test/head.js to make use of its helpers. +// It loads inspector/test/head.js which itself loads inspector/test/shared-head.js +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/rules/test/head.js", + this +); + +// Ensure the three-pane mode is enabled before running the tests. +Services.prefs.setBoolPref("devtools.inspector.three-pane-enabled", true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.three-pane-enabled"); +}); + +/** + * Get an array of objects with property/value pairs of the CSS declarations rendered + * in the Changes panel. + * + * @param {Document} panelDoc + * Host document of the Changes panel. + * @param {String} selector + * Optional selector to filter rendered declaration DOM elements. + * One of ".diff-remove" or ".diff-add". + * If omitted, all declarations will be returned. + * @param {DOMNode} containerNode + * Optional element to restrict results to declaration DOM elements which are + * descendants of this container node. + * If omitted, all declarations will be returned + * @return {Array} + */ +function getDeclarations(panelDoc, selector = "", containerNode = null) { + const els = panelDoc.querySelectorAll(`.changes__declaration${selector}`); + + return [...els] + .filter(el => { + return containerNode ? containerNode.contains(el) : true; + }) + .map(el => { + return { + property: el.querySelector(".changes__declaration-name").textContent, + value: el.querySelector(".changes__declaration-value").textContent, + element: el, + }; + }); +} + +function getAddedDeclarations(panelDoc, containerNode) { + return getDeclarations(panelDoc, ".diff-add", containerNode); +} + +function getRemovedDeclarations(panelDoc, containerNode) { + return getDeclarations(panelDoc, ".diff-remove", containerNode); +} + +/** + * Get an array of DOM elements for the CSS selectors rendered in the Changes panel. + * + * @param {Document} panelDoc + * Host document of the Changes panel. + * @param {String} selector + * Optional selector to filter rendered selector DOM elements. + * One of ".diff-remove" or ".diff-add". + * If omitted, all selectors will be returned. + * @return {Array} + */ +function getSelectors(panelDoc, selector = "") { + return panelDoc.querySelectorAll(`.changes__selector${selector}`); +} + +function getAddedSelectors(panelDoc) { + return getSelectors(panelDoc, ".diff-add"); +} + +function getRemovedSelectors(panelDoc) { + return getSelectors(panelDoc, ".diff-remove"); +} + +async function getChangesContextMenu(changesView, element) { + const onContextMenu = changesView.contextMenu.once("open"); + info(`Trigger context menu for element: ${element}`); + synthesizeContextMenuEvent(element); + info(`Wait for context menu to show`); + await onContextMenu; + + return changesView.contextMenu; +} diff --git a/devtools/client/inspector/changes/test/xpcshell/.eslintrc.js b/devtools/client/inspector/changes/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..86bd54c245 --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/client/inspector/changes/test/xpcshell/head.js b/devtools/client/inspector/changes/test/xpcshell/head.js new file mode 100644 index 0000000000..f08a79dd71 --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/head.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); diff --git a/devtools/client/inspector/changes/test/xpcshell/mocks.js b/devtools/client/inspector/changes/test/xpcshell/mocks.js new file mode 100644 index 0000000000..52f175beb8 --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/mocks.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable comma-dangle */ + +"use strict"; + +/** + * Snapshot of the Redux state for the Changes panel. + * + * It corresponds to the tracking of a single property value change (background-color) + * within a deeply nested CSS at-rule structure from an inline stylesheet: + * + * @media (min-width: 50em) { + * @supports (display: grid) { + * body { + * - background-color: royalblue; + * + background-color: red; + * } + * } + * } + */ +module.exports.CHANGES_STATE = { + source1: { + type: "inline", + href: "http://localhost:5000/at-rules-nested.html", + id: "source1", + index: 0, + isFramed: false, + rules: { + rule1: { + selectors: ["@media (min-width: 50em)"], + ruleId: "rule1", + add: [], + remove: [], + children: ["rule2"], + }, + rule2: { + selectors: ["@supports (display: grid)"], + ruleId: "rule2", + add: [], + remove: [], + children: ["rule3"], + parent: "rule1", + }, + rule3: { + selectors: ["body"], + ruleId: "rule3", + add: [ + { + property: "background-color", + value: "red", + index: 0, + }, + ], + remove: [ + { + property: "background-color", + value: "royalblue", + index: 0, + }, + ], + children: [], + parent: "rule2", + }, + }, + }, +}; diff --git a/devtools/client/inspector/changes/test/xpcshell/test_changes_stylesheet.js b/devtools/client/inspector/changes/test/xpcshell/test_changes_stylesheet.js new file mode 100644 index 0000000000..33a64cfbcb --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/test_changes_stylesheet.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that getChangesStylesheet() serializes tracked changes from nested CSS rules +// into the expected stylesheet format. + +const { + getChangesStylesheet, +} = require("resource://devtools/client/inspector/changes/selectors/changes.js"); + +const { CHANGES_STATE } = require("resource://test/mocks"); + +// Wrap multi-line string in backticks to ensure exact check in test, including new lines. +const STYLESHEET_FOR_ANCESTOR = ` +/* Inline #0 | http://localhost:5000/at-rules-nested.html */ + +@media (min-width: 50em) { + @supports (display: grid) { + body { + /* background-color: royalblue; */ + background-color: red; + } + } +} +`; + +// Wrap multi-line string in backticks to ensure exact check in test, including new lines. +const STYLESHEET_FOR_DESCENDANT = ` +/* Inline #0 | http://localhost:5000/at-rules-nested.html */ + +body { + /* background-color: royalblue; */ + background-color: red; +} +`; + +add_test(() => { + info( + "Check stylesheet generated for the first ancestor in the CSS rule tree." + ); + equal( + getChangesStylesheet(CHANGES_STATE), + STYLESHEET_FOR_ANCESTOR, + "Stylesheet includes all ancestors." + ); + + info( + "Check stylesheet generated for the last descendant in the CSS rule tree." + ); + const filter = { sourceIds: ["source1"], ruleIds: ["rule3"] }; + equal( + getChangesStylesheet(CHANGES_STATE, filter), + STYLESHEET_FOR_DESCENDANT, + "Stylesheet includes just descendant." + ); + + run_next_test(); +}); diff --git a/devtools/client/inspector/changes/test/xpcshell/xpcshell.toml b/devtools/client/inspector/changes/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..2edc192d20 --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/xpcshell.toml @@ -0,0 +1,7 @@ +[DEFAULT] +tags = "devtools" +firefox-appdir = "browser" +head = "head.js" +support-files = ["./mocks.js"] + +["test_changes_stylesheet.js"] diff --git a/devtools/client/inspector/changes/utils/changes-utils.js b/devtools/client/inspector/changes/utils/changes-utils.js new file mode 100644 index 0000000000..3e8505dc17 --- /dev/null +++ b/devtools/client/inspector/changes/utils/changes-utils.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + getFormatStr, + getStr, +} = require("resource://devtools/client/inspector/changes/utils/l10n.js"); + +/** + * Get a human-friendly style source path to display in the Changes panel. + * For element inline styles, return a string indicating that. + * For inline stylesheets, return a string indicating that plus the stylesheet's index. + * For URLs, return just the stylesheet filename. + * + * @param {Object} source + * Information about the style source. Contains: + * - type: {String} One of "element" or "stylesheet" + * - href: {String|null} Stylesheet URL or document URL for elmeent inline styles + * - index: {Number} Position of the stylesheet in its document's stylesheet list. + * @return {String} + */ +function getSourceForDisplay(source) { + let href; + + switch (source.type) { + case "element": + href = getStr("changes.elementStyleLabel"); + break; + case "inline": + href = getFormatStr("changes.inlineStyleSheetLabel", `#${source.index}`); + break; + case "stylesheet": + const url = new URL(source.href); + href = url.pathname.substring(url.pathname.lastIndexOf("/") + 1); + break; + } + + return href; +} + +module.exports.getSourceForDisplay = getSourceForDisplay; diff --git a/devtools/client/inspector/changes/utils/l10n.js b/devtools/client/inspector/changes/utils/l10n.js new file mode 100644 index 0000000000..693a1ec3cf --- /dev/null +++ b/devtools/client/inspector/changes/utils/l10n.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const L10N = new LocalizationHelper( + "devtools/client/locales/changes.properties" +); + +module.exports = { + getStr: (...args) => L10N.getStr(...args), + getFormatStr: (...args) => L10N.getFormatStr(...args), +}; diff --git a/devtools/client/inspector/changes/utils/moz.build b/devtools/client/inspector/changes/utils/moz.build new file mode 100644 index 0000000000..155752e0d8 --- /dev/null +++ b/devtools/client/inspector/changes/utils/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "changes-utils.js", + "l10n.js", +) diff --git a/devtools/client/inspector/compatibility/CompatibilityView.js b/devtools/client/inspector/compatibility/CompatibilityView.js new file mode 100644 index 0000000000..19c3263f00 --- /dev/null +++ b/devtools/client/inspector/compatibility/CompatibilityView.js @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + createElement, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + Provider, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const LocalizationProvider = createFactory(FluentReact.LocalizationProvider); + +const compatibilityReducer = require("resource://devtools/client/inspector/compatibility/reducers/compatibility.js"); +const { + appendNode, + clearDestroyedNodes, + initUserSettings, + removeNode, + updateNodes, + updateSelectedNode, + updateTopLevelTarget, + updateNode, +} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); + +const CompatibilityApp = createFactory( + require("resource://devtools/client/inspector/compatibility/components/CompatibilityApp.js") +); + +class CompatibilityView { + constructor(inspector, window) { + this.inspector = inspector; + + this.inspector.store.injectReducer("compatibility", compatibilityReducer); + + this._parseMarkup = this._parseMarkup.bind(this); + this._onChangeAdded = this._onChangeAdded.bind(this); + this._onPanelSelected = this._onPanelSelected.bind(this); + this._onSelectedNodeChanged = this._onSelectedNodeChanged.bind(this); + this._onTopLevelTargetChanged = this._onTopLevelTargetChanged.bind(this); + this._onResourceAvailable = this._onResourceAvailable.bind(this); + this._onMarkupMutation = this._onMarkupMutation.bind(this); + + this._init(); + } + + destroy() { + try { + this.resourceCommand.unwatchResources( + [this.resourceCommand.TYPES.CSS_CHANGE], + { + onAvailable: this._onResourceAvailable, + } + ); + } catch (e) { + // If unwatchResources is called before finishing process of watchResources, + // unwatchResources throws an error during stopping listener. + } + + this.inspector.off("new-root", this._onTopLevelTargetChanged); + this.inspector.off("markupmutation", this._onMarkupMutation); + this.inspector.selection.off("new-node-front", this._onSelectedNodeChanged); + this.inspector.sidebar.off( + "compatibilityview-selected", + this._onPanelSelected + ); + this.inspector = null; + } + + get resourceCommand() { + return this.inspector.toolbox.resourceCommand; + } + + async _init() { + const { setSelectedNode } = this.inspector.getCommonComponentProps(); + const compatibilityApp = new CompatibilityApp({ + setSelectedNode, + }); + + this.provider = createElement( + Provider, + { + id: "compatibilityview", + store: this.inspector.store, + }, + LocalizationProvider( + { + bundles: this.inspector.fluentL10n.getBundles(), + parseMarkup: this._parseMarkup, + }, + compatibilityApp + ) + ); + + await this.inspector.store.dispatch(initUserSettings()); + // awaiting for `initUserSettings` makes us miss the initial "compatibilityview-selected" + // event, so we need to manually call _onPanelSelected to fetch compatibility issues + // for the selected node (and the whole page). + this._onPanelSelected(); + + this.inspector.on("new-root", this._onTopLevelTargetChanged); + this.inspector.on("markupmutation", this._onMarkupMutation); + this.inspector.selection.on("new-node-front", this._onSelectedNodeChanged); + this.inspector.sidebar.on( + "compatibilityview-selected", + this._onPanelSelected + ); + + await this.resourceCommand.watchResources( + [this.resourceCommand.TYPES.CSS_CHANGE], + { + onAvailable: this._onResourceAvailable, + // CSS changes made before opening Compatibility View are already applied to + // corresponding DOM at this point, so existing resources can be ignored here. + ignoreExistingResources: true, + } + ); + + this.inspector.emitForTests("compatibilityview-initialized"); + } + + _isAvailable() { + return ( + this.inspector && + this.inspector.sidebar && + this.inspector.sidebar.getCurrentTabID() === "compatibilityview" && + this.inspector.selection && + this.inspector.selection.isConnected() + ); + } + + _parseMarkup(str) { + // Using a BrowserLoader for the inspector is currently blocked on performance regressions, + // see Bug 1471853. + throw new Error( + "The inspector cannot use tags in ftl strings because it does not run in a BrowserLoader" + ); + } + + _onChangeAdded({ selector }) { + if (!this._isAvailable()) { + // In order to update this panel if a change is added while hiding this panel. + this._isChangeAddedWhileHidden = true; + return; + } + + this._isChangeAddedWhileHidden = false; + + // We need to debounce updating nodes since "add-change" event on changes actor is + // fired for every typed character until fixing bug 1503036. + if (this._previousChangedSelector === selector) { + clearTimeout(this._updateNodesTimeoutId); + } + this._previousChangedSelector = selector; + + this._updateNodesTimeoutId = setTimeout(() => { + // TODO: In case of keyframes changes, the selector given from changes actor is + // keyframe-selector such as "from" and "100%", not selector for node. Thus, + // we need to address this case. + this.inspector.store.dispatch(updateNodes(selector)); + }, 500); + } + + _onMarkupMutation(mutations) { + const attributeMutation = mutations.filter( + mutation => + mutation.type === "attributes" && + (mutation.attributeName === "style" || + mutation.attributeName === "class") + ); + const childListMutation = mutations.filter( + mutation => mutation.type === "childList" + ); + + if (attributeMutation.length === 0 && childListMutation.length === 0) { + return; + } + + if (!this._isAvailable()) { + // In order to update this panel if a change is added while hiding this panel. + this._isChangeAddedWhileHidden = true; + return; + } + + this._isChangeAddedWhileHidden = false; + + // Resource Watcher doesn't respond to programmatic inline CSS + // change. This check can be removed once the following bug is resolved + // https://bugzilla.mozilla.org/show_bug.cgi?id=1506160 + for (const { target } of attributeMutation) { + this.inspector.store.dispatch(updateNode(target)); + } + + // Destroyed nodes can be cleaned up + // once at the end if necessary + let cleanupDestroyedNodes = false; + for (const { removed, target } of childListMutation) { + if (!removed.length) { + this.inspector.store.dispatch(appendNode(target)); + continue; + } + + const retainedNodes = removed.filter(node => node && !node.isDestroyed()); + cleanupDestroyedNodes = + cleanupDestroyedNodes || retainedNodes.length !== removed.length; + + for (const retainedNode of retainedNodes) { + this.inspector.store.dispatch(removeNode(retainedNode)); + } + } + + if (cleanupDestroyedNodes) { + this.inspector.store.dispatch(clearDestroyedNodes()); + } + } + + _onPanelSelected() { + const { selectedNode, topLevelTarget } = + this.inspector.store.getState().compatibility; + + // Update if the selected node is changed or new change is added while the panel was hidden. + if ( + this.inspector.selection.nodeFront !== selectedNode || + this._isChangeAddedWhileHidden + ) { + this._onSelectedNodeChanged(); + } + + // Update if the top target has changed or new change is added while the panel was hidden. + if ( + this.inspector.toolbox.target !== topLevelTarget || + this._isChangeAddedWhileHidden + ) { + this._onTopLevelTargetChanged(); + } + + this._isChangeAddedWhileHidden = false; + } + + _onSelectedNodeChanged() { + if (!this._isAvailable()) { + return; + } + + this.inspector.store.dispatch( + updateSelectedNode(this.inspector.selection.nodeFront) + ); + } + + _onResourceAvailable(resources) { + for (const resource of resources) { + // Style changes applied inline directly to + // the element and its changes are monitored by + // _onMarkupMutation via markupmutation events. + // Hence those changes can be ignored here + if (resource.source?.type !== "element") { + this._onChangeAdded(resource); + } + } + } + + _onTopLevelTargetChanged() { + if (!this._isAvailable()) { + return; + } + + this.inspector.store.dispatch( + updateTopLevelTarget(this.inspector.toolbox.target) + ); + } +} + +module.exports = CompatibilityView; diff --git a/devtools/client/inspector/compatibility/README.md b/devtools/client/inspector/compatibility/README.md new file mode 100644 index 0000000000..40b23f8be0 --- /dev/null +++ b/devtools/client/inspector/compatibility/README.md @@ -0,0 +1,25 @@ +# Compatibility Panel + +## Related files +The compatibility panel consists of the following files: +* Client: + * Main: `devtools/client/inspector/compatibility/` + * Style: `devtools/client/themes/compatibility.css` +* Shared: + * MDN compatibility dataset: `devtools/shared/compatibility/dataset/` + * MDN compatibility library: `devtools/server/actors/compatibility/lib/MDNCompatibility.js` + * User setting file - `devtools/shared/compatibility/compatibility-user-settings.js` +* Server: + * Actor: `devtools/server/actors/compatibility.js` + * Front: `devtools/client/fronts/compatibility.js` + * Spec: `devtools/shared/specs/compatibility.js` + +## MDN Compatibility Data +The Compatibility panel detects issues by comparing against official [MDN compatibility data](https://github.com/mdn/browser-compat-data). It uses a local snapshot of the dataset. This dataset needs to be manually synchronized periodically to `devtools/shared/compatibility/dataset` (ideally with every Firefox release). + +To update this dataset, please refer to the guidelines in `devtools/shared/compatibility/README.md` + +## Tests +* Client: `devtools/client/inspector/compatibility/test` +* MDN compatibility library: `devtools/server/actors/compatibility/lib/test` +* Server: `devtools/server/tests/browser/browser_compatibility_cssIssues.js` diff --git a/devtools/client/inspector/compatibility/actions/compatibility.js b/devtools/client/inspector/compatibility/actions/compatibility.js new file mode 100644 index 0000000000..fa9f410e0d --- /dev/null +++ b/devtools/client/inspector/compatibility/actions/compatibility.js @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const nodeConstants = require("resource://devtools/shared/dom-node-constants.js"); + +const UserSettings = require("resource://devtools/shared/compatibility/compatibility-user-settings.js"); + +const { + COMPATIBILITY_APPEND_NODE_START, + COMPATIBILITY_APPEND_NODE_SUCCESS, + COMPATIBILITY_APPEND_NODE_FAILURE, + COMPATIBILITY_APPEND_NODE_COMPLETE, + COMPATIBILITY_CLEAR_DESTROYED_NODES, + COMPATIBILITY_INIT_USER_SETTINGS_START, + COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS, + COMPATIBILITY_INIT_USER_SETTINGS_FAILURE, + COMPATIBILITY_INIT_USER_SETTINGS_COMPLETE, + COMPATIBILITY_INTERNAL_APPEND_NODE, + COMPATIBILITY_INTERNAL_NODE_UPDATE, + COMPATIBILITY_INTERNAL_REMOVE_NODE, + COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES, + COMPATIBILITY_REMOVE_NODE_START, + COMPATIBILITY_REMOVE_NODE_SUCCESS, + COMPATIBILITY_REMOVE_NODE_FAILURE, + COMPATIBILITY_REMOVE_NODE_COMPLETE, + COMPATIBILITY_UPDATE_NODE_START, + COMPATIBILITY_UPDATE_NODE_SUCCESS, + COMPATIBILITY_UPDATE_NODE_FAILURE, + COMPATIBILITY_UPDATE_NODE_COMPLETE, + COMPATIBILITY_UPDATE_NODES_START, + COMPATIBILITY_UPDATE_NODES_SUCCESS, + COMPATIBILITY_UPDATE_NODES_FAILURE, + COMPATIBILITY_UPDATE_NODES_COMPLETE, + COMPATIBILITY_UPDATE_SELECTED_NODE_START, + COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS, + COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, + COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE, + COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_START, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +function appendNode(node) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_APPEND_NODE_START }); + + try { + const { targetBrowsers, topLevelTarget } = getState().compatibility; + const { walker } = await topLevelTarget.getFront("inspector"); + await _inspectNode(node, targetBrowsers, walker, dispatch); + dispatch({ type: COMPATIBILITY_APPEND_NODE_SUCCESS }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_APPEND_NODE_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_APPEND_NODE_COMPLETE }); + }; +} + +function clearDestroyedNodes() { + return { type: COMPATIBILITY_CLEAR_DESTROYED_NODES }; +} + +function initUserSettings() { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_INIT_USER_SETTINGS_START }); + + try { + const [defaultTargetBrowsers, targetBrowsers] = await Promise.all([ + UserSettings.getBrowsersList(), + UserSettings.getTargetBrowsers(), + ]); + + dispatch({ + type: COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS, + defaultTargetBrowsers, + targetBrowsers, + }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_INIT_USER_SETTINGS_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_INIT_USER_SETTINGS_COMPLETE }); + }; +} + +function removeNode(node) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_REMOVE_NODE_START }); + + try { + const { topLevelTarget } = getState().compatibility; + const { walker } = await topLevelTarget.getFront("inspector"); + await _removeNode(node, walker, dispatch); + dispatch({ type: COMPATIBILITY_REMOVE_NODE_SUCCESS }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_REMOVE_NODE_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_REMOVE_NODE_COMPLETE }); + }; +} + +function updateNodes(selector) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_UPDATE_NODES_START }); + + try { + const { selectedNode, topLevelTarget, targetBrowsers } = + getState().compatibility; + const { walker } = await topLevelTarget.getFront("inspector"); + const nodeList = await walker.querySelectorAll(walker.rootNode, selector); + + for (const node of await nodeList.items()) { + await _updateNode(node, selectedNode, targetBrowsers, dispatch); + } + dispatch({ type: COMPATIBILITY_UPDATE_NODES_SUCCESS }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_UPDATE_NODES_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_UPDATE_NODES_COMPLETE }); + }; +} + +function updateSelectedNode(node) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_UPDATE_SELECTED_NODE_START }); + + try { + const { targetBrowsers } = getState().compatibility; + await _updateSelectedNodeIssues(node, targetBrowsers, dispatch); + + dispatch({ + type: COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS, + node, + }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE }); + }; +} + +function updateSettingsVisibility(visibility) { + return { + type: COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY, + visibility, + }; +} + +function updateTargetBrowsers(targetBrowsers) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_UPDATE_TARGET_BROWSERS_START }); + + try { + UserSettings.setTargetBrowsers(targetBrowsers); + + const { selectedNode, topLevelTarget } = getState().compatibility; + + if (selectedNode) { + await _updateSelectedNodeIssues(selectedNode, targetBrowsers, dispatch); + } + + if (topLevelTarget) { + await _updateTopLevelTargetIssues( + topLevelTarget, + targetBrowsers, + dispatch + ); + } + + dispatch({ + type: COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS, + targetBrowsers, + }); + } catch (error) { + dispatch({ type: COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE, error }); + } + + dispatch({ type: COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE }); + }; +} + +function updateTopLevelTarget(target) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START }); + + try { + const { targetBrowsers } = getState().compatibility; + await _updateTopLevelTargetIssues(target, targetBrowsers, dispatch); + + dispatch({ type: COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS, target }); + } catch (error) { + dispatch({ type: COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, error }); + } + + dispatch({ type: COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE }); + }; +} + +function updateNode(node) { + return async ({ dispatch, getState }) => { + dispatch({ type: COMPATIBILITY_UPDATE_NODE_START }); + + try { + const { selectedNode, targetBrowsers } = getState().compatibility; + await _updateNode(node, selectedNode, targetBrowsers, dispatch); + dispatch({ type: COMPATIBILITY_UPDATE_NODE_SUCCESS }); + } catch (error) { + dispatch({ + type: COMPATIBILITY_UPDATE_NODE_FAILURE, + error, + }); + } + + dispatch({ type: COMPATIBILITY_UPDATE_NODE_COMPLETE }); + }; +} + +async function _getNodeIssues(node, targetBrowsers) { + const compatibility = await node.inspectorFront.getCompatibilityFront(); + const declarationBlocksIssues = await compatibility.getNodeCssIssues( + node, + targetBrowsers + ); + + return declarationBlocksIssues; +} + +async function _inspectNode(node, targetBrowsers, walker, dispatch) { + if (node.nodeType !== nodeConstants.ELEMENT_NODE) { + return; + } + + const issues = await _getNodeIssues(node, targetBrowsers); + + if (issues.length) { + dispatch({ + type: COMPATIBILITY_INTERNAL_APPEND_NODE, + node, + issues, + }); + } + + const { nodes: children } = await walker.children(node); + for (const child of children) { + await _inspectNode(child, targetBrowsers, walker, dispatch); + } +} + +async function _removeNode(node, walker, dispatch) { + if (node.nodeType !== nodeConstants.ELEMENT_NODE) { + return; + } + + dispatch({ + type: COMPATIBILITY_INTERNAL_REMOVE_NODE, + node, + }); + + const { nodes: children } = await walker.children(node); + for (const child of children) { + await _removeNode(child, walker, dispatch); + } +} + +async function _updateNode(node, selectedNode, targetBrowsers, dispatch) { + if (selectedNode.actorID === node.actorID) { + await _updateSelectedNodeIssues(node, targetBrowsers, dispatch); + } + + const issues = await _getNodeIssues(node, targetBrowsers); + dispatch({ + type: COMPATIBILITY_INTERNAL_NODE_UPDATE, + node, + issues, + }); +} + +async function _updateSelectedNodeIssues(node, targetBrowsers, dispatch) { + const issues = await _getNodeIssues(node, targetBrowsers); + + dispatch({ + type: COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES, + issues, + }); +} + +async function _updateTopLevelTargetIssues(target, targetBrowsers, dispatch) { + const { walker } = await target.getFront("inspector"); + const documentElement = await walker.documentElement(); + await _inspectNode(documentElement, targetBrowsers, walker, dispatch); +} + +module.exports = { + appendNode, + clearDestroyedNodes, + initUserSettings, + removeNode, + updateNodes, + updateSelectedNode, + updateSettingsVisibility, + updateTargetBrowsers, + updateTopLevelTarget, + updateNode, +}; diff --git a/devtools/client/inspector/compatibility/actions/index.js b/devtools/client/inspector/compatibility/actions/index.js new file mode 100644 index 0000000000..4362177213 --- /dev/null +++ b/devtools/client/inspector/compatibility/actions/index.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Append node and their children on DOM mutation + "COMPATIBILITY_APPEND_NODE_START", + "COMPATIBILITY_APPEND_NODE_SUCCESS", + "COMPATIBILITY_APPEND_NODE_FAILURE", + "COMPATIBILITY_APPEND_NODE_COMPLETE", + + // Remove references to node that is removed + // programmatically whose fronts are destroyed. + "COMPATIBILITY_CLEAR_DESTROYED_NODES", + + // Init user settings. + "COMPATIBILITY_INIT_USER_SETTINGS_START", + "COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS", + "COMPATIBILITY_INIT_USER_SETTINGS_FAILURE", + "COMPATIBILITY_INIT_USER_SETTINGS_COMPLETE", + + // Append node using internal helper that caused issues. + "COMPATIBILITY_INTERNAL_APPEND_NODE", + + // Updates a node via the internal helper + "COMPATIBILITY_INTERNAL_NODE_UPDATE", + + // Remove references to node that is removed + // in Markup Inspector but retained by DevTools + // using the internal helper. + "COMPATIBILITY_INTERNAL_REMOVE_NODE", + + // Updates the selected node issues using internal helper. + "COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES", + + // Clean up removed node from node list + "COMPATIBILITY_REMOVE_NODE_START", + "COMPATIBILITY_REMOVE_NODE_SUCCESS", + "COMPATIBILITY_REMOVE_NODE_FAILURE", + "COMPATIBILITY_REMOVE_NODE_COMPLETE", + + // Update node on attribute mutation + "COMPATIBILITY_UPDATE_NODE_START", + "COMPATIBILITY_UPDATE_NODE_SUCCESS", + "COMPATIBILITY_UPDATE_NODE_FAILURE", + "COMPATIBILITY_UPDATE_NODE_COMPLETE", + + // Updates nodes. + "COMPATIBILITY_UPDATE_NODES_START", + "COMPATIBILITY_UPDATE_NODES_SUCCESS", + "COMPATIBILITY_UPDATE_NODES_FAILURE", + "COMPATIBILITY_UPDATE_NODES_COMPLETE", + + // Updates the selected node. + "COMPATIBILITY_UPDATE_SELECTED_NODE_START", + "COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS", + "COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE", + "COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE", + + // Updates the settings panel visibility. + "COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY", + + // Updates the target browsers. + "COMPATIBILITY_UPDATE_TARGET_BROWSERS_START", + "COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS", + "COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE", + "COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE", + + // Updates the top level target. + "COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START", + "COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS", + "COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE", + "COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE", + ], + module.exports +); diff --git a/devtools/client/inspector/compatibility/actions/moz.build b/devtools/client/inspector/compatibility/actions/moz.build new file mode 100644 index 0000000000..1b2b96950f --- /dev/null +++ b/devtools/client/inspector/compatibility/actions/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "compatibility.js", + "index.js", +) diff --git a/devtools/client/inspector/compatibility/components/BrowserIcon.js b/devtools/client/inspector/compatibility/components/BrowserIcon.js new file mode 100644 index 0000000000..f452ba608a --- /dev/null +++ b/devtools/client/inspector/compatibility/components/BrowserIcon.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const ICONS = { + firefox: { + src: "chrome://devtools/skin/images/browsers/firefox.svg", + isMobileIconNeeded: false, + }, + firefox_android: { + src: "chrome://devtools/skin/images/browsers/firefox.svg", + isMobileIconNeeded: true, + }, + chrome: { + src: "chrome://devtools/skin/images/browsers/chrome.svg", + isMobileIconNeeded: false, + }, + chrome_android: { + src: "chrome://devtools/skin/images/browsers/chrome.svg", + isMobileIconNeeded: true, + }, + safari: { + src: "chrome://devtools/skin/images/browsers/safari.svg", + isMobileIconNeeded: false, + }, + safari_ios: { + src: "chrome://devtools/skin/images/browsers/safari.svg", + isMobileIconNeeded: true, + }, + edge: { + src: "chrome://devtools/skin/images/browsers/edge.svg", + isMobileIconNeeded: false, + }, + ie: { + src: "chrome://devtools/skin/images/browsers/ie.svg", + isMobileIconNeeded: false, + }, +}; + +class BrowserIcon extends PureComponent { + static get propTypes() { + return { + id: Types.browser.id, + title: PropTypes.string, + name: PropTypes.string, + }; + } + + render() { + const { id, name, title } = this.props; + + const icon = ICONS[id]; + + return dom.span( + { + className: + "compatibility-browser-icon" + + (icon.isMobileIconNeeded + ? " compatibility-browser-icon--mobile" + : ""), + }, + dom.img({ + className: "compatibility-browser-icon__image", + alt: name || title, + title, + src: icon.src, + }) + ); + } +} + +module.exports = BrowserIcon; diff --git a/devtools/client/inspector/compatibility/components/CompatibilityApp.js b/devtools/client/inspector/compatibility/components/CompatibilityApp.js new file mode 100644 index 0000000000..39614b15e0 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/CompatibilityApp.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const Accordion = createFactory( + require("resource://devtools/client/shared/components/Accordion.js") +); +const Footer = createFactory( + require("resource://devtools/client/inspector/compatibility/components/Footer.js") +); +const IssuePane = createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssuePane.js") +); +const Settings = createFactory( + require("resource://devtools/client/inspector/compatibility/components/Settings.js") +); + +class CompatibilityApp extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + // getString prop is injected by the withLocalization wrapper + getString: PropTypes.func.isRequired, + isSettingsVisible: PropTypes.bool.isRequired, + isTopLevelTargetProcessing: PropTypes.bool.isRequired, + selectedNodeIssues: PropTypes.arrayOf(PropTypes.shape(Types.issue)) + .isRequired, + topLevelTargetIssues: PropTypes.arrayOf(PropTypes.shape(Types.issue)) + .isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { + dispatch, + getString, + isSettingsVisible, + isTopLevelTargetProcessing, + selectedNodeIssues, + topLevelTargetIssues, + setSelectedNode, + } = this.props; + + const selectedNodeIssuePane = IssuePane({ + issues: selectedNodeIssues, + }); + + const topLevelTargetIssuePane = + topLevelTargetIssues.length || !isTopLevelTargetProcessing + ? IssuePane({ + dispatch, + issues: topLevelTargetIssues, + setSelectedNode, + }) + : null; + + const throbber = isTopLevelTargetProcessing + ? dom.div({ + className: "compatibility-app__throbber devtools-throbber", + }) + : null; + + return dom.section( + { + className: "compatibility-app theme-sidebar inspector-tabpanel", + }, + dom.div( + { + className: + "compatibility-app__container" + + (isSettingsVisible ? " compatibility-app__container-hidden" : ""), + }, + Accordion({ + className: "compatibility-app__main", + items: [ + { + id: "compatibility-app--selected-element-pane", + header: getString("compatibility-selected-element-header"), + component: selectedNodeIssuePane, + opened: true, + }, + { + id: "compatibility-app--all-elements-pane", + header: getString("compatibility-all-elements-header"), + component: [topLevelTargetIssuePane, throbber], + opened: true, + }, + ], + }), + Footer({ + className: "compatibility-app__footer", + }) + ), + isSettingsVisible ? Settings() : null + ); + } +} + +const mapStateToProps = state => { + return { + isSettingsVisible: state.compatibility.isSettingsVisible, + isTopLevelTargetProcessing: state.compatibility.isTopLevelTargetProcessing, + selectedNodeIssues: state.compatibility.selectedNodeIssues, + topLevelTargetIssues: state.compatibility.topLevelTargetIssues, + }; +}; +module.exports = FluentReact.withLocalization( + connect(mapStateToProps)(CompatibilityApp) +); diff --git a/devtools/client/inspector/compatibility/components/Footer.js b/devtools/client/inspector/compatibility/components/Footer.js new file mode 100644 index 0000000000..c48484b1c4 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/Footer.js @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const { + updateSettingsVisibility, +} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); + +const SETTINGS_ICON = "chrome://devtools/skin/images/settings.svg"; + +class Footer extends PureComponent { + static get propTypes() { + return { + updateSettingsVisibility: PropTypes.func.isRequired, + }; + } + + _renderButton(icon, labelId, titleId, onClick) { + return Localized( + { + id: titleId, + attrs: { title: true }, + }, + dom.button( + { + className: "compatibility-footer__button", + title: titleId, + onClick, + }, + dom.img({ + className: "compatibility-footer__icon", + src: icon, + }), + Localized( + { + id: labelId, + }, + dom.label( + { + className: "compatibility-footer__label", + }, + labelId + ) + ) + ) + ); + } + + render() { + return dom.footer( + { + className: "compatibility-footer", + }, + this._renderButton( + SETTINGS_ICON, + "compatibility-settings-button-label", + "compatibility-settings-button-title", + this.props.updateSettingsVisibility + ) + ); + } +} + +const mapDispatchToProps = dispatch => { + return { + updateSettingsVisibility: () => dispatch(updateSettingsVisibility(true)), + }; +}; + +module.exports = connect(null, mapDispatchToProps)(Footer); diff --git a/devtools/client/inspector/compatibility/components/IssueItem.js b/devtools/client/inspector/compatibility/components/IssueItem.js new file mode 100644 index 0000000000..c576e58223 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/IssueItem.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 { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +loader.lazyRequireGetter( + this, + "openDocLink", + "resource://devtools/client/shared/link.js", + true +); + +const UnsupportedBrowserList = createFactory( + require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js") +); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const NodePane = createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodePane.js") +); + +// For test +loader.lazyRequireGetter( + this, + "toSnakeCase", + "resource://devtools/client/inspector/compatibility/utils/cases.js", + true +); + +const MDN_LINK_PARAMS = new URLSearchParams({ + utm_source: "devtools", + utm_medium: "inspector-compatibility", + utm_campaign: "default", +}); + +class IssueItem extends PureComponent { + static get propTypes() { + return { + ...Types.issue, + dispatch: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this._onLinkClicked = this._onLinkClicked.bind(this); + } + + _onLinkClicked(e) { + e.preventDefault(); + e.stopPropagation(); + + const isMacOS = Services.appinfo.OS === "Darwin"; + + openDocLink(e.target.href, { + relatedToCurrent: true, + inBackground: isMacOS ? e.metaKey : e.ctrlKey, + }); + } + + _getTestDataAttributes() { + const testDataSet = {}; + + if (Services.prefs.getBoolPref("devtools.testing", false)) { + for (const [key, value] of Object.entries(this.props)) { + if (key === "nodes") { + continue; + } + const datasetKey = `data-qa-${toSnakeCase(key)}`; + testDataSet[datasetKey] = JSON.stringify(value); + } + } + + return testDataSet; + } + + _renderAliases() { + const { property } = this.props; + let { aliases } = this.props; + + if (!aliases) { + return null; + } + + aliases = aliases.filter(alias => alias !== property); + + if (aliases.length === 0) { + return null; + } + + return dom.ul( + { + className: "compatibility-issue-item__aliases", + }, + aliases.map(alias => + dom.li( + { + key: alias, + className: "compatibility-issue-item__alias", + }, + alias + ) + ) + ); + } + + _renderCauses() { + const { deprecated, experimental, prefixNeeded } = this.props; + + if (!deprecated && !experimental && !prefixNeeded) { + return null; + } + + let localizationId = ""; + + if (deprecated && experimental && prefixNeeded) { + localizationId = + "compatibility-issue-deprecated-experimental-prefixneeded"; + } else if (deprecated && experimental) { + localizationId = "compatibility-issue-deprecated-experimental"; + } else if (deprecated && prefixNeeded) { + localizationId = "compatibility-issue-deprecated-prefixneeded"; + } else if (experimental && prefixNeeded) { + localizationId = "compatibility-issue-experimental-prefixneeded"; + } else if (deprecated) { + localizationId = "compatibility-issue-deprecated"; + } else if (experimental) { + localizationId = "compatibility-issue-experimental"; + } else if (prefixNeeded) { + localizationId = "compatibility-issue-prefixneeded"; + } + + return Localized( + { + id: localizationId, + }, + dom.span( + { className: "compatibility-issue-item__causes" }, + localizationId + ) + ); + } + + _renderPropertyEl() { + const { property, url, specUrl } = this.props; + const baseCls = "compatibility-issue-item__property devtools-monospace"; + if (!url && !specUrl) { + return dom.span({ className: baseCls }, property); + } + + const href = url ? `${url}?${MDN_LINK_PARAMS}` : specUrl; + + return dom.a( + { + className: `${baseCls} ${ + url + ? "compatibility-issue-item__mdn-link" + : "compatibility-issue-item__spec-link" + }`, + href, + title: href, + onClick: e => this._onLinkClicked(e), + }, + property + ); + } + + _renderDescription() { + return dom.div( + { + className: "compatibility-issue-item__description", + }, + this._renderPropertyEl(), + this._renderCauses(), + this._renderUnsupportedBrowserList() + ); + } + + _renderNodeList() { + const { dispatch, nodes, setSelectedNode } = this.props; + + if (!nodes) { + return null; + } + + return NodePane({ + dispatch, + nodes, + setSelectedNode, + }); + } + + _renderUnsupportedBrowserList() { + const { unsupportedBrowsers } = this.props; + + return unsupportedBrowsers.length + ? UnsupportedBrowserList({ browsers: unsupportedBrowsers }) + : null; + } + + render() { + const { deprecated, experimental, property, unsupportedBrowsers } = + this.props; + + const classes = ["compatibility-issue-item"]; + + if (deprecated) { + classes.push("compatibility-issue-item--deprecated"); + } + + if (experimental) { + classes.push("compatibility-issue-item--experimental"); + } + + if (unsupportedBrowsers.length) { + classes.push("compatibility-issue-item--unsupported"); + } + + return dom.li( + { + className: classes.join(" "), + key: property, + ...this._getTestDataAttributes(), + }, + this._renderDescription(), + this._renderAliases(), + this._renderNodeList() + ); + } +} + +module.exports = IssueItem; diff --git a/devtools/client/inspector/compatibility/components/IssueList.js b/devtools/client/inspector/compatibility/components/IssueList.js new file mode 100644 index 0000000000..f334276cb3 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/IssueList.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const IssueItem = createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssueItem.js") +); + +class IssueList extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + issues: PropTypes.arrayOf(PropTypes.shape(Types.issue)).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, issues, setSelectedNode } = this.props; + + return dom.ul( + { className: "compatibility-issue-list" }, + issues.map(issue => + IssueItem({ + ...issue, + dispatch, + setSelectedNode, + }) + ) + ); + } +} + +module.exports = IssueList; diff --git a/devtools/client/inspector/compatibility/components/IssuePane.js b/devtools/client/inspector/compatibility/components/IssuePane.js new file mode 100644 index 0000000000..b313274d9a --- /dev/null +++ b/devtools/client/inspector/compatibility/components/IssuePane.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const IssueList = createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssueList.js") +); + +class IssuePane extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + issues: PropTypes.arrayOf(PropTypes.shape(Types.issue)).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + _renderNoIssues() { + return Localized( + { id: "compatibility-no-issues-found" }, + dom.p( + { className: "devtools-sidepanel-no-result" }, + "compatibility-no-issues-found" + ) + ); + } + + render() { + const { dispatch, issues, setSelectedNode } = this.props; + + return issues.length + ? IssueList({ + dispatch, + issues, + setSelectedNode, + }) + : this._renderNoIssues(); + } +} + +module.exports = IssuePane; diff --git a/devtools/client/inspector/compatibility/components/NodeItem.js b/devtools/client/inspector/compatibility/components/NodeItem.js new file mode 100644 index 0000000000..078b04a3b4 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/NodeItem.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + translateNodeFrontToGrip, +} = require("resource://devtools/client/inspector/shared/utils.js"); +const { + REPS, + MODE, +} = require("resource://devtools/client/shared/components/reps/index.js"); +const { Rep } = REPS; +const ElementNode = REPS.ElementNode; + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class NodeItem extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + node: Types.node.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, node, setSelectedNode } = this.props; + + return dom.li( + { className: "compatibility-node-item" }, + Rep({ + defaultRep: ElementNode, + mode: MODE.TINY, + object: translateNodeFrontToGrip(node), + onDOMNodeClick: () => { + setSelectedNode(node); + dispatch(unhighlightNode()); + }, + onDOMNodeMouseOut: () => dispatch(unhighlightNode()), + onDOMNodeMouseOver: () => dispatch(highlightNode(node)), + }) + ); + } +} + +module.exports = NodeItem; diff --git a/devtools/client/inspector/compatibility/components/NodeList.js b/devtools/client/inspector/compatibility/components/NodeList.js new file mode 100644 index 0000000000..7dc93c8b06 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/NodeList.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const NodeItem = createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodeItem.js") +); + +class NodeList extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + nodes: PropTypes.arrayOf(Types.node).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, nodes, setSelectedNode } = this.props; + + return dom.ul( + { className: "compatibility-node-list" }, + nodes.map(node => + NodeItem({ + dispatch, + node, + setSelectedNode, + }) + ) + ); + } +} + +module.exports = NodeList; diff --git a/devtools/client/inspector/compatibility/components/NodePane.js b/devtools/client/inspector/compatibility/components/NodePane.js new file mode 100644 index 0000000000..06c844d012 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/NodePane.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const NodeList = createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodeList.js") +); + +class NodePane extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + nodes: PropTypes.arrayOf(Types.node).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { nodes } = this.props; + + return dom.details( + { + className: "compatibility-node-pane", + open: nodes.length <= 1, + }, + Localized( + { + id: "compatibility-issue-occurrences", + $number: nodes.length, + }, + dom.summary( + { className: "compatibility-node-pane__summary" }, + "compatibility-issue-occurrences" + ) + ), + NodeList(this.props) + ); + } +} + +module.exports = NodePane; diff --git a/devtools/client/inspector/compatibility/components/Settings.js b/devtools/client/inspector/compatibility/components/Settings.js new file mode 100644 index 0000000000..6f55353aa6 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/Settings.js @@ -0,0 +1,197 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +const BrowserIcon = createFactory( + require("resource://devtools/client/inspector/compatibility/components/BrowserIcon.js") +); + +const { + updateSettingsVisibility, + updateTargetBrowsers, +} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); + +const CLOSE_ICON = "chrome://devtools/skin/images/close.svg"; + +class Settings extends PureComponent { + static get propTypes() { + return { + defaultTargetBrowsers: PropTypes.arrayOf(PropTypes.shape(Types.browser)) + .isRequired, + targetBrowsers: PropTypes.arrayOf(PropTypes.shape(Types.browser)) + .isRequired, + updateTargetBrowsers: PropTypes.func.isRequired, + updateSettingsVisibility: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this._onTargetBrowserChanged = this._onTargetBrowserChanged.bind(this); + + this.state = { + targetBrowsers: props.targetBrowsers, + }; + } + + _onTargetBrowserChanged({ target }) { + const { id, status } = target.dataset; + let { targetBrowsers } = this.state; + + if (target.checked) { + targetBrowsers = [...targetBrowsers, { id, status }]; + } else { + targetBrowsers = targetBrowsers.filter( + b => !(b.id === id && b.status === status) + ); + } + + this.setState({ targetBrowsers }); + } + + _renderTargetBrowsers() { + const { defaultTargetBrowsers } = this.props; + const { targetBrowsers } = this.state; + + return dom.section( + { + className: "compatibility-settings__target-browsers", + }, + Localized( + { id: "compatibility-target-browsers-header" }, + dom.header( + { + className: "compatibility-settings__target-browsers-header", + }, + "compatibility-target-browsers-header" + ) + ), + dom.ul( + { + className: "compatibility-settings__target-browsers-list", + }, + defaultTargetBrowsers.map(({ id, name, status, version }) => { + const inputId = `${id}-${status}`; + const isTargetBrowser = !!targetBrowsers.find( + b => b.id === id && b.status === status + ); + return dom.li( + { + className: "compatibility-settings__target-browsers-item", + }, + dom.input({ + id: inputId, + type: "checkbox", + checked: isTargetBrowser, + onChange: this._onTargetBrowserChanged, + "data-id": id, + "data-status": status, + }), + dom.label( + { + className: "compatibility-settings__target-browsers-item-label", + htmlFor: inputId, + }, + BrowserIcon({ id, title: `${name} ${status}` }), + `${name} ${status} (${version})` + ) + ); + }) + ) + ); + } + + _renderHeader() { + return dom.header( + { + className: "compatibility-settings__header", + }, + Localized( + { id: "compatibility-settings-header" }, + dom.label( + { + className: "compatibility-settings__header-label", + }, + "compatibility-settings-header" + ) + ), + Localized( + { + id: "compatibility-close-settings-button", + attrs: { title: true }, + }, + dom.button( + { + className: "compatibility-settings__header-button", + title: "compatibility-close-settings-button", + onClick: () => { + const { defaultTargetBrowsers } = this.props; + const { targetBrowsers } = this.state; + + // Sort by ordering of default browsers. + const browsers = defaultTargetBrowsers.filter(b => + targetBrowsers.find(t => t.id === b.id && t.status === b.status) + ); + + if ( + this.props.targetBrowsers.toString() !== browsers.toString() + ) { + this.props.updateTargetBrowsers(browsers); + } + + this.props.updateSettingsVisibility(); + }, + }, + dom.img({ + className: "compatibility-settings__header-icon", + src: CLOSE_ICON, + }) + ) + ) + ); + } + + render() { + return dom.section( + { + className: "compatibility-settings", + }, + this._renderHeader(), + this._renderTargetBrowsers() + ); + } +} + +const mapStateToProps = state => { + return { + defaultTargetBrowsers: state.compatibility.defaultTargetBrowsers, + targetBrowsers: state.compatibility.targetBrowsers, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + updateTargetBrowsers: browsers => dispatch(updateTargetBrowsers(browsers)), + updateSettingsVisibility: () => dispatch(updateSettingsVisibility(false)), + }; +}; + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js b/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js new file mode 100644 index 0000000000..ae9806d206 --- /dev/null +++ b/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js @@ -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/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const BrowserIcon = createFactory( + require("resource://devtools/client/inspector/compatibility/components/BrowserIcon.js") +); +const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); +const Localized = createFactory(FluentReact.Localized); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +class UnsupportedBrowserItem extends PureComponent { + static get propTypes() { + return { + id: Types.browser.id, + name: Types.browser.name, + unsupportedVersions: PropTypes.array.isRequired, + version: Types.browser.version, + }; + } + + render() { + const { unsupportedVersions, id, name, version } = this.props; + + return Localized( + { + id: "compatibility-issue-browsers-list", + $browsers: unsupportedVersions + .map( + ({ version: v, status }) => + `${name} ${v}${status ? ` (${status})` : ""}` + ) + .join("\n"), + attrs: { title: true }, + }, + dom.li( + { className: "compatibility-browser", "data-browser-id": id }, + BrowserIcon({ id, name }), + dom.span( + { + className: "compatibility-browser-version", + }, + version + ) + ) + ); + } +} + +module.exports = UnsupportedBrowserItem; diff --git a/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js b/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js new file mode 100644 index 0000000000..51b513253f --- /dev/null +++ b/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const UnsupportedBrowserItem = createFactory( + require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js") +); + +const Types = require("resource://devtools/client/inspector/compatibility/types.js"); + +class UnsupportedBrowserList extends PureComponent { + static get propTypes() { + return { + browsers: PropTypes.arrayOf(PropTypes.shape(Types.browser)).isRequired, + }; + } + + render() { + const { browsers } = this.props; + + const unsupportedBrowserItems = {}; + + const unsupportedVersionsListByBrowser = new Map(); + + for (const { name, version, status } of browsers) { + if (!unsupportedVersionsListByBrowser.has(name)) { + unsupportedVersionsListByBrowser.set(name, []); + } + unsupportedVersionsListByBrowser.get(name).push({ version, status }); + } + + for (const { id, name, version, status } of browsers) { + // Only display one icon per browser + if (!unsupportedBrowserItems[id]) { + if (status === "esr") { + // The data is ordered by version number, so we'll show the first unsupported + // browser version. This might be confusing for Firefox as we'll show ESR + // version first, and so the user wouldn't be able to tell if there's an issue + // only on ESR, or also on release. + // So only show ESR if there's no newer unsupported version + const newerVersionIsUnsupported = browsers.find( + browser => browser.id == id && browser.status !== status + ); + if (newerVersionIsUnsupported) { + continue; + } + } + + unsupportedBrowserItems[id] = UnsupportedBrowserItem({ + key: id, + id, + name, + version, + unsupportedVersions: unsupportedVersionsListByBrowser.get(name), + }); + } + } + return dom.ul( + { + className: "compatibility-unsupported-browser-list", + }, + Object.values(unsupportedBrowserItems) + ); + } +} + +module.exports = UnsupportedBrowserList; diff --git a/devtools/client/inspector/compatibility/components/moz.build b/devtools/client/inspector/compatibility/components/moz.build new file mode 100644 index 0000000000..b4a1c7cf7c --- /dev/null +++ b/devtools/client/inspector/compatibility/components/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "BrowserIcon.js", + "CompatibilityApp.js", + "Footer.js", + "IssueItem.js", + "IssueList.js", + "IssuePane.js", + "NodeItem.js", + "NodeList.js", + "NodePane.js", + "Settings.js", + "UnsupportedBrowserItem.js", + "UnsupportedBrowserList.js", +) diff --git a/devtools/client/inspector/compatibility/moz.build b/devtools/client/inspector/compatibility/moz.build new file mode 100644 index 0000000000..92b29743cc --- /dev/null +++ b/devtools/client/inspector/compatibility/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "actions", + "components", + "reducers", + "utils", +] + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"] +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] + +DevToolsModules( + "CompatibilityView.js", + "types.js", +) + +with Files("**"): + BUG_COMPONENT = ("DevTools", "Inspector: Compatibility") diff --git a/devtools/client/inspector/compatibility/reducers/compatibility.js b/devtools/client/inspector/compatibility/reducers/compatibility.js new file mode 100644 index 0000000000..9256167601 --- /dev/null +++ b/devtools/client/inspector/compatibility/reducers/compatibility.js @@ -0,0 +1,262 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + COMPATIBILITY_APPEND_NODE_FAILURE, + COMPATIBILITY_CLEAR_DESTROYED_NODES, + COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS, + COMPATIBILITY_INIT_USER_SETTINGS_FAILURE, + COMPATIBILITY_INTERNAL_APPEND_NODE, + COMPATIBILITY_INTERNAL_NODE_UPDATE, + COMPATIBILITY_INTERNAL_REMOVE_NODE, + COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES, + COMPATIBILITY_REMOVE_NODE_FAILURE, + COMPATIBILITY_UPDATE_NODE_FAILURE, + COMPATIBILITY_UPDATE_NODES_FAILURE, + COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS, + COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, + COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_START, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +const INITIAL_STATE = { + defaultTargetBrowsers: [], + isSettingsVisible: false, + isTopLevelTargetProcessing: false, + selectedNode: null, + selectedNodeIssues: [], + topLevelTarget: null, + topLevelTargetIssues: [], + targetBrowsers: [], +}; + +const reducers = { + [COMPATIBILITY_APPEND_NODE_FAILURE](state, { error }) { + _showError(COMPATIBILITY_APPEND_NODE_FAILURE, error); + return state; + }, + [COMPATIBILITY_CLEAR_DESTROYED_NODES](state) { + const topLevelTargetIssues = _clearDestroyedNodes( + state.topLevelTargetIssues + ); + return Object.assign({}, state, { topLevelTargetIssues }); + }, + [COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS]( + state, + { defaultTargetBrowsers, targetBrowsers } + ) { + return Object.assign({}, state, { defaultTargetBrowsers, targetBrowsers }); + }, + [COMPATIBILITY_INIT_USER_SETTINGS_FAILURE](state, { error }) { + _showError(COMPATIBILITY_INIT_USER_SETTINGS_FAILURE, error); + return state; + }, + [COMPATIBILITY_INTERNAL_APPEND_NODE](state, { node, issues }) { + const topLevelTargetIssues = _appendTopLevelTargetIssues( + state.topLevelTargetIssues, + node, + issues + ); + return Object.assign({}, state, { topLevelTargetIssues }); + }, + [COMPATIBILITY_INTERNAL_NODE_UPDATE](state, { node, issues }) { + const topLevelTargetIssues = _updateTopLevelTargetIssues( + state.topLevelTargetIssues, + node, + issues + ); + return Object.assign({}, state, { topLevelTargetIssues }); + }, + [COMPATIBILITY_INTERNAL_REMOVE_NODE](state, { node }) { + const topLevelTargetIssues = _removeNodeOrIssues( + state.topLevelTargetIssues, + node, + [] + ); + return Object.assign({}, state, { topLevelTargetIssues }); + }, + [COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES](state, { issues }) { + return Object.assign({}, state, { selectedNodeIssues: issues }); + }, + [COMPATIBILITY_REMOVE_NODE_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_NODE_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_NODES_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS](state, { node }) { + return Object.assign({}, state, { selectedNode: node }); + }, + [COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY](state, { visibility }) { + return Object.assign({}, state, { isSettingsVisible: visibility }); + }, + [COMPATIBILITY_UPDATE_TARGET_BROWSERS_START](state) { + return Object.assign({}, state, { + isTopLevelTargetProcessing: true, + topLevelTargetIssues: [], + }); + }, + [COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS](state, { targetBrowsers }) { + return Object.assign({}, state, { targetBrowsers }); + }, + [COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE](state) { + return Object.assign({}, state, { isTopLevelTargetProcessing: false }); + }, + [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START](state) { + return Object.assign({}, state, { + isTopLevelTargetProcessing: true, + topLevelTargetIssues: [], + }); + }, + [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS](state, { target }) { + return Object.assign({}, state, { topLevelTarget: target }); + }, + [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE](state, { error }) { + _showError(COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, error); + return state; + }, + [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE](state, { target }) { + return Object.assign({}, state, { isTopLevelTargetProcessing: false }); + }, +}; + +function _appendTopLevelTargetIssues(targetIssues, node, issues) { + targetIssues = [...targetIssues]; + for (const issue of issues) { + const index = _indexOfIssue(targetIssues, issue); + if (index < 0) { + issue.nodes = [node]; + targetIssues.push(issue); + continue; + } + + const targetIssue = targetIssues[index]; + const nodeIndex = targetIssue.nodes.findIndex( + issueNode => issueNode.actorID === node.actorID + ); + + if (nodeIndex < 0) { + targetIssue.nodes = [...targetIssue.nodes, node]; + } + } + return targetIssues; +} + +function _clearDestroyedNodes(targetIssues) { + return targetIssues.reduce((newIssues, targetIssue) => { + const retainedNodes = targetIssue.nodes.filter( + n => n.targetFront && !n.targetFront.isDestroyed() + ); + + // All the nodes for a given issue are destroyed + if (retainedNodes.length === 0) { + // Remove issue. + return newIssues; + } + + targetIssue.nodes = retainedNodes; + return [...newIssues, targetIssue]; + }, []); +} + +function _indexOfIssue(issues, issue) { + return issues.findIndex( + i => i.type === issue.type && i.property === issue.property + ); +} + +function _indexOfNode(issue, node) { + return issue.nodes.findIndex(n => n.actorID === node.actorID); +} + +function _removeNodeOrIssues(targetIssues, node, issues) { + return targetIssues.reduce((newIssues, targetIssue) => { + if (issues.length && _indexOfIssue(issues, targetIssue) >= 0) { + // The targetIssue is still in the node. + return [...newIssues, targetIssue]; + } + + const indexOfNodeInTarget = _indexOfNode(targetIssue, node); + if (indexOfNodeInTarget < 0) { + // The targetIssue does not have the node to remove. + return [...newIssues, targetIssue]; + } + + // This issue on the updated node is gone. + if (targetIssue.nodes.length === 1) { + // Remove issue. + return newIssues; + } + + // Remove node from the nodes. + targetIssue.nodes = [ + ...targetIssue.nodes.slice(0, indexOfNodeInTarget), + ...targetIssue.nodes.slice(indexOfNodeInTarget + 1), + ]; + return [...newIssues, targetIssue]; + }, []); +} + +function _updateTopLevelTargetIssues(targetIssues, node, issues) { + // Remove issues or node. + targetIssues = _removeNodeOrIssues(targetIssues, node, issues); + + // Append issues or node. + const appendables = issues.filter(issue => { + const indexOfIssue = _indexOfIssue(targetIssues, issue); + return ( + indexOfIssue < 0 || _indexOfNode(targetIssues[indexOfIssue], node) < 0 + ); + }); + targetIssues = _appendTopLevelTargetIssues(targetIssues, node, appendables); + + // Update fields. + for (const issue of issues) { + const indexOfIssue = _indexOfIssue(targetIssues, issue); + + if (indexOfIssue < 0) { + continue; + } + + const targetIssue = targetIssues[indexOfIssue]; + targetIssues[indexOfIssue] = Object.assign(issue, { + nodes: targetIssue.nodes, + }); + } + + return targetIssues; +} + +function _showError(action, error) { + console.error(`[${action}] ${error.message}`); + console.error(error.stack); +} + +module.exports = function (state = INITIAL_STATE, action) { + const reducer = reducers[action.type]; + return reducer ? reducer(state, action) : state; +}; diff --git a/devtools/client/inspector/compatibility/reducers/moz.build b/devtools/client/inspector/compatibility/reducers/moz.build new file mode 100644 index 0000000000..b939919567 --- /dev/null +++ b/devtools/client/inspector/compatibility/reducers/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "compatibility.js", +) diff --git a/devtools/client/inspector/compatibility/test/browser/browser.toml b/devtools/client/inspector/compatibility/test/browser/browser.toml new file mode 100644 index 0000000000..b57f9d3837 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser.toml @@ -0,0 +1,41 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_compatibility_css-property_issue.js"] + +["browser_compatibility_dynamic_js-attribute-change.js"] + +["browser_compatibility_dynamic_js-dom-change.js"] + +["browser_compatibility_dynamic_markup-dom-change.js"] + +["browser_compatibility_dynamic_ruleview-attribute-change.js"] + +["browser_compatibility_event_document-reload.js"] + +["browser_compatibility_event_panel-select.js"] + +["browser_compatibility_event_rule-change.js"] + +["browser_compatibility_event_selected-node-change.js"] + +["browser_compatibility_event_top-level-target-change.js"] + +["browser_compatibility_issue-node.js"] + +["browser_compatibility_settings.js"] + +["browser_compatibility_throbber.js"] + +["browser_compatibility_unsupported-browsers_all.js"] + +["browser_compatibility_unsupported-browsers_some.js"] diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js new file mode 100644 index 0000000000..d026283b4d --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that unsupported CSS properties are correctly reported as issues. + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); + +const TEST_URI = ` + <style> + body { + color: blue; + scrollbar-width: thin; + user-modify: read-only; + hyphenate-limit-chars: auto; + overflow-clip-box: padding-box; + } + div { + ruby-align: center; + } + </style> + <body> + <div>test</div> + </body> +`; + +const TEST_DATA_SELECTED = [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + deprecated: false, + experimental: false, + }, + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES, + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + aliases: ["user-modify"], + deprecated: true, + experimental: false, + }, + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "hyphenate-limit-chars", + // No MDN url but a spec one + specUrl: + "https://drafts.csswg.org/css-text-4/#propdef-hyphenate-limit-chars", + deprecated: false, + experimental: false, + }, + // TODO: Re-enable it when we have another property with no MDN url nor spec url Bug 1840910 + /*{ + // No MDN url nor spec url + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "overflow-clip-box", + deprecated: false, + experimental: false, + },*/ +]; + +const TEST_DATA_ALL = [ + ...TEST_DATA_SELECTED, + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + deprecated: false, + experimental: true, + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const { allElementsPane, selectedElementPane } = + await openCompatibilityView(); + + // If the test fail because the properties used are no longer in the dataset, or they + // now have mdn/spec url although we expected them not to, uncomment the next line + // to get all the properties in the dataset that don't have a MDN url. + // logCssCompatDataPropertiesWithoutMDNUrl() + + info("Check the content of the issue list on the selected element"); + await assertIssueList(selectedElementPane, TEST_DATA_SELECTED); + + info("Check the content of the issue list on all elements"); + await assertIssueList(allElementsPane, TEST_DATA_ALL); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-attribute-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-attribute-change.js new file mode 100644 index 0000000000..99845457aa --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-attribute-change.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); + +const { + COMPATIBILITY_UPDATE_NODE_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +// Test the behavior rules are dynamically added + +const ISSUE_OUTLINE_RADIUS = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-input", + url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input", + deprecated: true, + experimental: false, +}; + +const ISSUE_SCROLLBAR_WIDTH = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + deprecated: false, + experimental: false, +}; + +const TEST_URI = ` + <style> + .issue { + -moz-user-input: none; + } + </style> + <body> + <div class="test"></div> + </body> +`; + +add_task(async function () { + info("Testing dynamic style change using JavaScript"); + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Testing inline style change due to JavaScript execution"); + const onPanelUpdate = waitForUpdateSelectedNodeAction(inspector.store); + info("Select the div to undergo mutation"); + await selectNode(".test", inspector); + await onPanelUpdate; + + info("Check initial issues"); + await assertIssueList(selectedElementPane, []); + await assertIssueList(allElementsPane, []); + + info("Adding inline style with compatibility issue"); + await testAttributeMutation( + tab, + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_SCROLLBAR_WIDTH], + [ISSUE_SCROLLBAR_WIDTH], + async function () { + content.document.querySelector(".test").style["scrollbar-width"] = "none"; + } + ); + + info("Adding a class with declarations having compatibility issue"); + await testAttributeMutation( + tab, + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_SCROLLBAR_WIDTH, ISSUE_OUTLINE_RADIUS], + [ISSUE_SCROLLBAR_WIDTH, ISSUE_OUTLINE_RADIUS], + async function () { + content.document.querySelector(".test").classList.add("issue"); + } + ); + + info("Removing a class with declarations having compatibility issue"); + await testAttributeMutation( + tab, + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_SCROLLBAR_WIDTH], + [ISSUE_SCROLLBAR_WIDTH], + async function () { + content.document.querySelector(".test").classList.remove("issue"); + } + ); + + await removeTab(tab); +}); + +async function testAttributeMutation( + tab, + inspector, + selectedElementPane, + allElementsPane, + expectedSelectedElementIssues, + expectedAllElementsIssues, + contentTaskFunction +) { + const onPanelUpdate = Promise.all([ + inspector.once("markupmutation"), + waitForDispatch(inspector.store, COMPATIBILITY_UPDATE_NODE_COMPLETE), + ]); + info("Run the task in webpage context"); + await ContentTask.spawn(tab.linkedBrowser, {}, contentTaskFunction); + info("Wait for changes to reflect"); + await onPanelUpdate; + + info("Check issues listed in selected element pane"); + await assertIssueList(selectedElementPane, expectedSelectedElementIssues); + info("Check issues listed in all issues pane"); + await assertIssueList(allElementsPane, expectedAllElementsIssues); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-dom-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-dom-change.js new file mode 100644 index 0000000000..bf148b988a --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_js-dom-change.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); + +const { + COMPATIBILITY_APPEND_NODE_COMPLETE, + COMPATIBILITY_CLEAR_DESTROYED_NODES, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +// Test the behavior rules are dynamically added + +const ISSUE_OUTLINE_RADIUS = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-input", + url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input", + deprecated: true, + experimental: false, +}; + +const ISSUE_SCROLLBAR_WIDTH = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + deprecated: false, + experimental: false, +}; + +const TEST_URI = ` + <style> + .child { + -moz-user-input: none; + } + </style> + <body></body> +`; + +add_task(async function () { + info("Testing dynamic DOM mutation using JavaScript"); + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Check initial issues"); + await assertIssueList(selectedElementPane, []); + await assertIssueList(allElementsPane, []); + + info("Append nodes dynamically using JavaScript"); + await testNodeMutation( + ".child", + COMPATIBILITY_APPEND_NODE_COMPLETE, + tab, + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_OUTLINE_RADIUS], + [ISSUE_SCROLLBAR_WIDTH, ISSUE_OUTLINE_RADIUS], + async function () { + const doc = content.document; + const parent = doc.querySelector("body"); + + const newElementWithIssue = doc.createElement("div"); + newElementWithIssue.style["scrollbar-width"] = "none"; + + const parentOfIssueElement = doc.createElement("div"); + parentOfIssueElement.classList.add("parent"); + const child = doc.createElement("div"); + child.classList.add("child"); + parentOfIssueElement.appendChild(child); + + parent.appendChild(newElementWithIssue); + parent.appendChild(parentOfIssueElement); + } + ); + + info("Remove node whose child has compatibility issue"); + await testNodeMutation( + "div", + COMPATIBILITY_CLEAR_DESTROYED_NODES, + tab, + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_SCROLLBAR_WIDTH], + [ISSUE_SCROLLBAR_WIDTH], + async function () { + const doc = content.document; + const parent = doc.querySelector(".parent"); + parent.remove(); + } + ); + + info("Remove node which has compatibility issue"); + await testNodeMutation( + "body", + COMPATIBILITY_CLEAR_DESTROYED_NODES, + tab, + inspector, + selectedElementPane, + allElementsPane, + [], + [], + async function () { + const doc = content.document; + const issueElement = doc.querySelector("div"); + issueElement.remove(); + } + ); + + await removeTab(tab); +}); + +async function testNodeMutation( + selector, + action, + tab, + inspector, + selectedElementPane, + allElementsPane, + expectedSelectedElementIssues, + expectedAllElementsIssues, + contentTaskFunction +) { + let onPanelUpdate = Promise.all([ + inspector.once("markupmutation"), + waitForDispatch(inspector.store, action), + ]); + info("Add a new node with issue and another node whose child has the issue"); + await ContentTask.spawn(tab.linkedBrowser, {}, contentTaskFunction); + info("Wait for changes"); + await onPanelUpdate; + + onPanelUpdate = waitForUpdateSelectedNodeAction(inspector.store); + await selectNode(selector, inspector); + await onPanelUpdate; + + info("Check element issues"); + await assertIssueList(selectedElementPane, expectedSelectedElementIssues); + + info("Check all issues"); + await assertIssueList(allElementsPane, expectedAllElementsIssues); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_markup-dom-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_markup-dom-change.js new file mode 100644 index 0000000000..2d7ddc2e11 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_markup-dom-change.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); + +const { + COMPATIBILITY_APPEND_NODE_COMPLETE, + COMPATIBILITY_REMOVE_NODE_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +// Test the behavior rules are dynamically added + +const ISSUE_OUTLINE_RADIUS = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-input", + url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input", + deprecated: true, + experimental: false, +}; + +const ISSUE_SCROLLBAR_WIDTH = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + deprecated: false, + experimental: false, +}; + +const TEST_URI = ` + <style> + div { + -moz-user-input: none; + } + </style> + <body> + <div></div> + <div class="parent"> + <div style="scrollbar-width: none"></div> + </div> + </body> +`; + +add_task(async function () { + info("Testing dynamic DOM mutation using JavaScript"); + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector } = await openCompatibilityView(); + + info("Check initial issues"); + await assertIssueList(allElementsPane, [ + ISSUE_OUTLINE_RADIUS, + ISSUE_SCROLLBAR_WIDTH, + ]); + + info("Delete node whose child node has CSS compatibility issue"); + await testNodeRemoval(".parent", inspector, allElementsPane, [ + ISSUE_OUTLINE_RADIUS, + ]); + + info("Delete node that has CSS compatibility issue"); + await testNodeRemoval("div", inspector, allElementsPane, []); + + info("Add node that has CSS compatibility issue"); + await testNodeAddition("div", inspector, allElementsPane, [ + ISSUE_OUTLINE_RADIUS, + ]); + + await removeTab(tab); +}); + +/** + * Simulate a click on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * This overrides the definition in inspector/test/head.js which times + * out when the container to be clicked is already the selected node. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the node has been selected. + */ +var clickContainer = async function (selector, inspector) { + info("Clicking on the markup-container for node " + selector); + + const nodeFront = await getNodeFront(selector, inspector); + const container = getContainerForNodeFront(nodeFront, inspector); + + const updated = container.selected + ? Promise.resolve() + : inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mousedown" }, + inspector.markup.doc.defaultView + ); + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mouseup" }, + inspector.markup.doc.defaultView + ); + return updated; +}; + +async function deleteNode(inspector, selector) { + info("Select node " + selector + " and make sure it is focused"); + await selectNode(selector, inspector); + await clickContainer(selector, inspector); + + info("Delete the node"); + const mutated = inspector.once("markupmutation"); + const updated = inspector.once("inspector-updated"); + EventUtils.sendKey("delete", inspector.panelWin); + await mutated; + await updated; +} + +async function testNodeAddition( + selector, + inspector, + allElementsPane, + expectedAllElementsIssues +) { + let onPanelUpdate = Promise.all([ + inspector.once("markupmutation"), + waitForDispatch(inspector.store, COMPATIBILITY_APPEND_NODE_COMPLETE), + ]); + info("Add a new node"); + await inspector.addNode(); + await onPanelUpdate; + + onPanelUpdate = waitForUpdateSelectedNodeAction(inspector.store); + await selectNode(selector, inspector); + await onPanelUpdate; + + info("Check issues list for the webpage"); + await assertIssueList(allElementsPane, expectedAllElementsIssues); +} + +async function testNodeRemoval( + selector, + inspector, + allElementsPane, + expectedAllElementsIssues +) { + const onPanelUpdate = Promise.all([ + inspector.once("markupmutation"), + waitForDispatch(inspector.store, COMPATIBILITY_REMOVE_NODE_COMPLETE), + ]); + info(`Delete the node with selector ${selector}`); + await deleteNode(inspector, selector); + await onPanelUpdate; + + info("Check issues list for the webpage"); + await assertIssueList(allElementsPane, expectedAllElementsIssues); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_ruleview-attribute-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_ruleview-attribute-change.js new file mode 100644 index 0000000000..178edd8cd0 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_dynamic_ruleview-attribute-change.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); + +const { + COMPATIBILITY_UPDATE_NODE_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +// Test the behavior rules are dynamically added + +const ISSUE_OUTLINE_RADIUS = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-input", + url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input", + deprecated: true, + experimental: false, +}; + +const ISSUE_SCROLLBAR_WIDTH = { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + deprecated: false, + experimental: false, +}; + +const TEST_URI = ` + <style> + .issue { + -moz-user-input: none; + } + </style> + <body> + <div class="test issue"></div> + </body> +`; + +add_task(async function () { + info("Testing dynamic style change via the devtools inspector's rule view"); + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Select the div to undergo mutation"); + const waitForCompatibilityListUpdate = waitForUpdateSelectedNodeAction( + inspector.store + ); + await selectNode(".test.issue", inspector); + await waitForCompatibilityListUpdate; + + info("Check initial issues"); + await checkPanelIssues(selectedElementPane, allElementsPane, [ + ISSUE_OUTLINE_RADIUS, + ]); + + await addNewRule( + "scrollbar-width", + "none", + inspector, + selectedElementPane, + allElementsPane, + [ISSUE_OUTLINE_RADIUS, ISSUE_SCROLLBAR_WIDTH] + ); + + info("Toggle the inline issue rendering it disable"); + await togglePropStatusOnRuleView(inspector, 0, 0); + info("Check the issues listed in panel"); + await checkPanelIssues(selectedElementPane, allElementsPane, [ + ISSUE_OUTLINE_RADIUS, + ]); + + info("Toggle the class rule rendering it disabled"); + await togglePropStatusOnRuleView(inspector, 1, 0); + info("Check the panel issues listed in panel"); + await checkPanelIssues(selectedElementPane, allElementsPane, []); + + await removeTab(tab); +}); + +async function addNewRule( + newDeclaration, + value, + inspector, + selectedElementPane, + allElementsPane, + issue +) { + const { view } = await openRuleView(); + const waitForCompatibilityListUpdate = waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_NODE_COMPLETE + ); + + info("Add a new inline property"); + await addProperty(view, 0, newDeclaration, value); + info("Wait for changes"); + await waitForCompatibilityListUpdate; + + info("Check issues list for element and the webpage"); + await checkPanelIssues(selectedElementPane, allElementsPane, issue); +} + +async function checkPanelIssues(selectedElementPane, allElementsPane, issues) { + info("Check selected element issues"); + await assertIssueList(selectedElementPane, issues); + info("Check all panel issues"); + await assertIssueList(allElementsPane, issues); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_document-reload.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_document-reload.js new file mode 100644 index 0000000000..a7f18e0811 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_document-reload.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the issues after reloading the browsing document. + +const TEST_URI = ` + <style> + body { + color: blue; + ruby-align: center; + user-modify: read-only; + } + div { + scrollbar-width: thin; + } + </style> + <body> + <div>test</div> + </body> +`; + +const TEST_DATA_SELECTED = [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + { + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + }, +]; + +const TEST_DATA_ALL = [ + ...TEST_DATA_SELECTED, + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, +]; + +const { + COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +add_task(async function () { + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Check the issues on the selected element"); + await assertIssueList(selectedElementPane, TEST_DATA_SELECTED); + info("Check the issues on all elements"); + await assertIssueList(allElementsPane, TEST_DATA_ALL); + + let isUpdateSelectedNodeFailure = false; + let isUpdateTopLevelTargetFailure = false; + waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE + ).then(() => { + isUpdateSelectedNodeFailure = true; + }); + waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE + ).then(() => { + isUpdateTopLevelTargetFailure = true; + }); + + info("Reload the browsing page"); + const onReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + const onUpdateSelectedNode = waitForUpdateSelectedNodeAction(inspector.store); + const onUpdateTopLevelTarget = waitForUpdateTopLevelTargetAction( + inspector.store + ); + gBrowser.reloadTab(tab); + await Promise.all([onReloaded, onUpdateSelectedNode, onUpdateTopLevelTarget]); + + info("Check whether the failure action will be fired or not"); + ok( + !isUpdateSelectedNodeFailure && !isUpdateTopLevelTargetFailure, + "No error occurred" + ); + + info("Check the issues on the selected element again"); + await assertIssueList(selectedElementPane, TEST_DATA_SELECTED); + info("Check the issues on all elements again"); + await assertIssueList(allElementsPane, TEST_DATA_ALL); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_panel-select.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_panel-select.js new file mode 100644 index 0000000000..d856af3539 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_panel-select.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the behavior when the panel is selected. + +const TEST_URI = "<body style='background-color: lime;'><div>test</div></body>"; +const TEST_ANOTHER_URI = "<body></body>"; + +const { + COMPATIBILITY_UPDATE_SELECTED_NODE_START, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +add_task(async function () { + info( + "Check that the panel does not update when no changes occur while hidden" + ); + + await pushPref("devtools.inspector.activeSidebar", ""); + + const tab = await addTab(_toDataURL(TEST_URI)); + const { inspector } = await openCompatibilityView(); + + info("Select another sidebar panel"); + await _selectSidebarPanel(inspector, "changesview"); + + info("Select the compatibility panel again"); + let isSelectedNodeUpdated = false; + let isTopLevelTargetUpdated = false; + waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_SELECTED_NODE_START + ).then(() => { + isSelectedNodeUpdated = true; + }); + waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START + ).then(() => { + isTopLevelTargetUpdated = true; + }); + + await _selectSidebarPanel(inspector, "compatibilityview"); + + // Check above both flags after taking enough time. + await wait(1000); + + ok(!isSelectedNodeUpdated, "Avoid updating the selected node pane"); + ok(!isTopLevelTargetUpdated, "Avoid updating the top level target pane"); + + await removeTab(tab); +}); + +add_task(async function () { + info( + "Check that the panel only updates for the selected node when the node is changed while the panel is hidden" + ); + + await pushPref("devtools.inspector.activeSidebar", ""); + + const tab = await addTab(_toDataURL(TEST_URI)); + const { inspector } = await openCompatibilityView(); + + info("Select another sidebar panel"); + await _selectSidebarPanel(inspector, "changesview"); + + info("Select another node"); + await selectNode("div", inspector); + + info("Select the compatibility panel again"); + const onSelectedNodePaneUpdated = waitForUpdateSelectedNodeAction( + inspector.store + ); + let isTopLevelTargetUpdated = false; + waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START + ).then(() => { + isTopLevelTargetUpdated = true; + }); + + await _selectSidebarPanel(inspector, "compatibilityview"); + + await onSelectedNodePaneUpdated; + ok(true, "Update the selected node pane"); + ok(!isTopLevelTargetUpdated, "Avoid updating the top level target pane"); + + await removeTab(tab); +}); + +add_task(async function () { + info( + "Check that both panes update when the top-level target changed while the panel is hidden" + ); + + await pushPref("devtools.inspector.activeSidebar", ""); + + const tab = await addTab(_toDataURL(TEST_URI)); + const { inspector } = await openCompatibilityView(); + + info("Select another sidebar panel"); + await _selectSidebarPanel(inspector, "changesview"); + + info("Navigate to another page"); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + _toDataURL(TEST_ANOTHER_URI) + ); + + info("Select the compatibility panel again"); + const onSelectedNodePaneUpdated = waitForUpdateSelectedNodeAction( + inspector.store + ); + const onTopLevelTargetPaneUpdated = waitForUpdateTopLevelTargetAction( + inspector.store + ); + + await _selectSidebarPanel(inspector, "compatibilityview"); + + await onSelectedNodePaneUpdated; + await onTopLevelTargetPaneUpdated; + ok(true, "Update both panes"); + + await removeTab(tab); +}); + +add_task(async function () { + info( + "Check that both panes update when a rule is changed changed while the panel is hidden" + ); + + await pushPref("devtools.inspector.activeSidebar", ""); + + info("Disable 3 pane mode"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const tab = await addTab(_toDataURL(TEST_URI)); + const { inspector } = await openCompatibilityView(); + + info("Select rule view"); + await _selectSidebarPanel(inspector, "ruleview"); + + info("Change a rule"); + await togglePropStatusOnRuleView(inspector, 0, 0); + + info("Select the compatibility panel again"); + const onSelectedNodePaneUpdated = waitForUpdateSelectedNodeAction( + inspector.store + ); + const onTopLevelTargetPaneUpdated = waitForUpdateTopLevelTargetAction( + inspector.store + ); + + await _selectSidebarPanel(inspector, "compatibilityview"); + + await onSelectedNodePaneUpdated; + await onTopLevelTargetPaneUpdated; + ok(true, "Update both panes"); + + await removeTab(tab); +}); + +async function _selectSidebarPanel(inspector, toolId) { + const onSelected = inspector.sidebar.once(`${toolId}-selected`); + inspector.sidebar.select(toolId); + await onSelected; +} + +function _toDataURL(content) { + return "data:text/html;charset=utf-8," + encodeURIComponent(content); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_rule-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_rule-change.js new file mode 100644 index 0000000000..91b29b488b --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_rule-change.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the content of the issue list will be changed when the rules are changed +// on the rule view. + +const TEST_URI = ` + <style> + .test-class { + ruby-align: center; + } + div { + scrollbar-width: thin; + } + </style> + <div class="test-class">test class</div> + <div>test</div> +`; + +const TEST_DATA_SELECTED = { + fullRule: { + expectedProperties: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + ], + expectedNodes: [ + { + property: "ruby-align", + nodes: [], + }, + { + property: "scrollbar-width", + nodes: [], + }, + ], + }, + classRule: { + expectedProperties: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + ], + expectedNodes: [ + { + property: "ruby-align", + nodes: [], + }, + ], + }, + elementRule: { + expectedProperties: [ + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + ], + expectedNodes: [ + { + property: "scrollbar-width", + nodes: [], + }, + ], + }, +}; + +const TEST_DATA_ALL = { + fullRule: { + expectedProperties: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + ], + expectedNodes: [ + { + property: "ruby-align", + nodes: ["div.test-class"], + }, + { + property: "scrollbar-width", + nodes: ["div.test-class", "div"], + }, + ], + }, + classRule: { + expectedProperties: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + ], + expectedNodes: [ + { + property: "ruby-align", + nodes: ["div.test-class"], + }, + ], + }, + elementRule: { + expectedProperties: [ + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + ], + expectedNodes: [ + { + property: "scrollbar-width", + nodes: ["div.test-class", "div"], + }, + ], + }, +}; + +const { + COMPATIBILITY_UPDATE_NODES_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +add_task(async function () { + info("Enable 3 pane mode"); + await pushPref("devtools.inspector.three-pane-enabled", true); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + await selectNode(".test-class", inspector); + + info("Check the initial issue"); + await assertAll(selectedElementPane, TEST_DATA_SELECTED.fullRule); + await assertAll(allElementsPane, TEST_DATA_ALL.fullRule); + + info("Check the issue after unchecking class rule"); + await _togglePropStatus(inspector, 1, 0); + await assertAll(selectedElementPane, TEST_DATA_SELECTED.elementRule); + await assertAll(allElementsPane, TEST_DATA_ALL.elementRule); + + info("Check the issue after unchecking div rule"); + await _togglePropStatus(inspector, 2, 0); + await assertIssueList(selectedElementPane, []); + await assertIssueList(allElementsPane, []); + + info("Check the issue after reverting class rule"); + await _togglePropStatus(inspector, 1, 0); + await assertAll(selectedElementPane, TEST_DATA_SELECTED.classRule); + await assertAll(allElementsPane, TEST_DATA_ALL.classRule); + + info("Check the issue after reverting div rule"); + await _togglePropStatus(inspector, 2, 0); + await assertAll(selectedElementPane, TEST_DATA_SELECTED.fullRule); + await assertAll(allElementsPane, TEST_DATA_ALL.fullRule); +}); + +async function assertAll(pane, { expectedProperties, expectedNodes }) { + await assertIssueList(pane, expectedProperties); + await assertNodeList(pane, expectedNodes); +} + +async function _togglePropStatus(inspector, ruleIndex, propIndex) { + const onNodesUpdated = waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_NODES_COMPLETE + ); + await togglePropStatusOnRuleView(inspector, ruleIndex, propIndex); + await onNodesUpdated; +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_selected-node-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_selected-node-change.js new file mode 100644 index 0000000000..d24f450968 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_selected-node-change.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the content of the issue list will be changed when the new node is selected. + +const TEST_URI = ` + <style> + body { + ruby-align: center; + } + + .has-issue { + scrollbar-width: thin; + user-modify: read-only; + } + + .no-issue { + color: black; + } + </style> + <body> + <div class="has-issue">has issue</div> + <div class="no-issue">no issue</div> + </body> +`; + +const TEST_DATA_SELECTED = [ + { + selector: ".has-issue", + expectedIssues: [ + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + { + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + }, + ], + }, + { + selector: ".no-issue", + expectedIssues: [], + }, + { + selector: "body", + expectedIssues: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + ], + }, +]; + +const TEST_DATA_ALL = [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + { + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + for (const { selector, expectedIssues } of TEST_DATA_SELECTED) { + info(`Check the issue list for ${selector} node`); + await selectNode(selector, inspector); + await assertIssueList(selectedElementPane, expectedIssues); + info("Check whether the issues on all elements pane are not changed"); + await assertIssueList(allElementsPane, TEST_DATA_ALL); + } +}); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_top-level-target-change.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_top-level-target-change.js new file mode 100644 index 0000000000..84d5131f8d --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_event_top-level-target-change.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the issues after navigating to another page. + +const TEST_DATA_ISSUES = { + uri: ` + <style> + body { + ruby-align: center; + } + div { + scrollbar-width: thin; + } + </style> + <body> + <div>test</div> + </body> + `, + expectedIssuesOnSelected: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + ], + expectedIssuesOnAll: [ + { + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + }, + { + property: "scrollbar-width", + url: "https://developer.mozilla.org/docs/Web/CSS/scrollbar-width", + }, + ], +}; + +const TEST_DATA_NO_ISSUES = { + uri: "<body></body>", + expectedIssuesOnSelected: [], + expectedIssuesOnAll: [], +}; + +add_task(async function () { + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_DATA_ISSUES.uri) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Check issues at initial"); + await assertIssues(selectedElementPane, allElementsPane, TEST_DATA_ISSUES); + + info("Navigate to next uri that has no issues"); + await navigateTo(TEST_DATA_NO_ISSUES.uri, tab, inspector); + info("Check issues after navigating"); + await assertIssues(selectedElementPane, allElementsPane, TEST_DATA_NO_ISSUES); + + info("Revert the uri"); + await navigateTo(TEST_DATA_ISSUES.uri, tab, inspector); + info("Check issues after reverting"); + await assertIssues(selectedElementPane, allElementsPane, TEST_DATA_ISSUES); +}); + +async function assertIssues( + selectedElementPane, + allElementsPane, + { expectedIssuesOnSelected, expectedIssuesOnAll } +) { + await assertIssueList(selectedElementPane, expectedIssuesOnSelected); + await assertIssueList(allElementsPane, expectedIssuesOnAll); +} + +async function navigateTo(uri, tab, { store }) { + uri = "data:text/html;charset=utf-8," + encodeURIComponent(uri); + const onSelectedNodeUpdated = waitForUpdateSelectedNodeAction(store); + const onTopLevelTargetUpdated = waitForUpdateTopLevelTargetAction(store); + const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, uri); + await Promise.all([onLoaded, onSelectedNodeUpdated, onTopLevelTargetUpdated]); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_issue-node.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_issue-node.js new file mode 100644 index 0000000000..b99dfd4062 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_issue-node.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test displaying the nodes that caused issues. + +const TEST_URI = ` + <style> + body { + user-modify: read-only; + } + div { + user-modify: read-only; + scrollbar-width: thin; + } + </style> + <body> + <div>div</div> + </body> +`; + +const TEST_DATA_ALL = [ + { + property: "user-modify", + nodes: ["body", "div"], + }, + { + property: "scrollbar-width", + nodes: ["div"], + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const { allElementsPane, selectedElementPane } = + await openCompatibilityView(); + + info("Check nodes that caused issues on the selected element"); + is( + selectedElementPane.querySelectorAll(".compatibility-node-item").length, + 0, + "Nodes are not displayed on the selected element" + ); + + info("Check nodes that caused issues on all elements"); + await assertNodeList(allElementsPane, TEST_DATA_ALL); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_settings.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_settings.js new file mode 100644 index 0000000000..36e4d57735 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_settings.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether settings page works. + +const TEST_URI = ` + <style> + body { + text-size-adjust: none; + } + div { + text-size-adjust: none; + } + </style> + <body><div></div></body> +`; + +const { + COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +add_task(async function () { + registerCleanupFunction(() => { + Services.prefs.clearUserPref( + "devtools.inspector.compatibility.target-browsers" + ); + }); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, panel } = await openCompatibilityView(); + const { store } = inspector; + + info("Check initial state"); + ok( + panel.querySelector(`.compatibility-browser-icon__image[src*="firefox"]`), + "Firefox browsers are the target" + ); + + info("Make Firefox browsers out of target"); + await updateTargetBrowsers(panel, store, id => !id.includes("firefox")); + ok( + !panel.querySelector(`.compatibility-browser-icon__image[src*="firefox"]`), + "Firefox browsers are not the target" + ); + + info("Make all browsers out of target"); + await updateTargetBrowsers(panel, store, () => false); + ok( + !panel.querySelector(".compatibility-browser-icon__image"), + "No browsers are the target" + ); + + info("Make Firefox browsers target"); + await updateTargetBrowsers(panel, store, id => id.includes("firefox")); + ok( + panel.querySelector(`.compatibility-browser-icon__image[src*="firefox"]`), + "Firefox browsers are the target now" + ); +}); + +async function updateTargetBrowsers(panel, store, isTargetBrowserFunc) { + info("Open settings pane"); + const settingsButton = panel.querySelector(".compatibility-footer__button"); + settingsButton.click(); + await waitUntil(() => panel.querySelector(".compatibility-settings")); + + const browsers = [ + ...new Set( + Array.from(panel.querySelectorAll("[data-id]")).map(el => + el.getAttribute("data-id") + ) + ), + ]; + Assert.deepEqual( + // Filter out IE, to be removed in an upcoming browser compat data sync. + // TODO: Remove the filter once D150961 lands. see Bug 1778009 + browsers.filter(browser => browser != "ie"), + [ + "chrome", + "chrome_android", + "edge", + "firefox", + "firefox_android", + "safari", + "safari_ios", + ], + "The expected browsers are displayed" + ); + + info("Change target browsers"); + const settingsPane = panel.querySelector(".compatibility-settings"); + for (const checkbox of settingsPane.querySelectorAll( + ".compatibility-settings__target-browsers-item input" + )) { + if (checkbox.checked !== isTargetBrowserFunc(checkbox.id)) { + // Need to click to toggle since the input is designed as controlled component. + checkbox.click(); + } + } + + info("Close settings pane"); + const onUpdated = waitForDispatch( + store, + COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE + ); + const closeButton = settingsPane.querySelector( + ".compatibility-settings__header-button" + ); + closeButton.click(); + await onUpdated; +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_throbber.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_throbber.js new file mode 100644 index 0000000000..eff0f9ac93 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_throbber.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the throbber is displayed correctly or not. + +const TEST_URI = ` + <style> + body { + color: blue; + border-block-color: lime; + user-modify: read-only; + } + div { + font-variant-alternates: historical-forms; + } + </style> + <body> + <div>test</div> + </body> +`; + +const { + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +add_task(async function () { + const tab = await addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + + const { allElementsPane, inspector, selectedElementPane } = + await openCompatibilityView(); + + info("Check the throbber visibility at the beginning"); + assertThrobber(allElementsPane, false); + assertThrobber(selectedElementPane, false); + + info("Reload the browsing page"); + const onStart = waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START + ); + const onComplete = waitForDispatch( + inspector.store, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE + ); + gBrowser.reloadTab(tab); + + info("Check the throbber visibility of after starting updating action"); + await onStart; + assertThrobber(allElementsPane, true); + assertThrobber(selectedElementPane, false); + + info("Check the throbber visibility of after completing updating action"); + await onComplete; + assertThrobber(allElementsPane, false); + assertThrobber(selectedElementPane, false); +}); + +function assertThrobber(panel, expectedVisibility) { + const isThrobberVisible = !!panel.querySelector(".devtools-throbber"); + is( + isThrobberVisible, + expectedVisibility, + "Visibility of the throbber is correct" + ); +} diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_all.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_all.js new file mode 100644 index 0000000000..c57c805ce1 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_all.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether the all of default browsers are unsupported. + +const TEST_URI = ` + <style> + body { + user-modify: read-only; + } + </style> + <body></body> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, selectedElementPane } = await openCompatibilityView(); + + info("Get the taget browsers we set as default"); + const { targetBrowsers } = inspector.store.getState().compatibility; + + info("Check the content of the issue item"); + const expectedIssues = [ + { + property: "user-modify", + unsupportedBrowsers: targetBrowsers, + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + }, + ]; + await assertIssueList(selectedElementPane, expectedIssues); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_some.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_some.js new file mode 100644 index 0000000000..6359b34e31 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_unsupported-browsers_some.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether some of browsers are unsupported. + +const { + updateTargetBrowsers, +} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); + +const TEST_URI = ` + <style> + body { + border-block-color: lime; + } + </style> + <body></body> +`; + +const TARGET_BROWSERS = [ + { id: "firefox", name: "Firefox", version: "1" }, + { id: "firefox", name: "Firefox", version: "70" }, + { id: "firefox_android", name: "Firefox Android", version: "1" }, + { id: "firefox_android", name: "Firefox Android", version: "70" }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, selectedElementPane } = await openCompatibilityView(); + + info("Update the target browsers for this test"); + await inspector.store.dispatch(updateTargetBrowsers(TARGET_BROWSERS)); + + info("Check the content of the issue item"); + const expectedIssues = [ + { + property: "border-block-color", + unsupportedBrowsers: [ + { id: "firefox", name: "Firefox", version: "1" }, + { id: "firefox_android", name: "Firefox Android", version: "1" }, + ], + url: "https://developer.mozilla.org/docs/Web/CSS/border-block-color", + }, + ]; + await assertIssueList(selectedElementPane, expectedIssues); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/head.js b/devtools/client/inspector/compatibility/test/browser/head.js new file mode 100644 index 0000000000..4cd84b2698 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/head.js @@ -0,0 +1,281 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Import the rule view's head.js first (which itself imports inspector's head.js and shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/rules/test/head.js", + this +); + +const { + COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE, + COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE, +} = require("resource://devtools/client/inspector/compatibility/actions/index.js"); + +const { + toCamelCase, +} = require("resource://devtools/client/inspector/compatibility/utils/cases.js"); + +async function openCompatibilityView() { + info("Open the compatibility view"); + const { inspector } = await openInspectorSidebarTab("compatibilityview"); + await Promise.all([ + waitForUpdateSelectedNodeAction(inspector.store), + waitForUpdateTopLevelTargetAction(inspector.store), + ]); + const panel = inspector.panelDoc.querySelector( + "#compatibilityview-panel .inspector-tabpanel" + ); + + const selectedElementPane = panel.querySelector( + "#compatibility-app--selected-element-pane" + ); + + const allElementsPane = panel.querySelector( + "#compatibility-app--all-elements-pane" + ); + + return { allElementsPane, inspector, panel, selectedElementPane }; +} + +/** + * Check whether the content of issue item element is matched with the expected values. + * + * @param {Element} panel + * @param {Array} expectedIssues + * Array of the issue expected. + * For the structure of issue items, see types.js. + */ +async function assertIssueList(panel, expectedIssues) { + info("Check the number of issues"); + await waitUntil( + () => + panel.querySelectorAll("[data-qa-property]").length === + expectedIssues.length + ); + ok(true, "The number of issues is correct"); + + if (expectedIssues.length === 0) { + // No issue. + return; + } + + const getFluentString = await getFluentStringHelper([ + "devtools/client/compatibility.ftl", + ]); + + for (const expectedIssue of expectedIssues) { + const property = expectedIssue.property; + info(`Check an element for ${property}`); + const issueEl = getIssueItem(property, panel); + ok(issueEl, `Issue element for the ${property} is in the panel`); + + if (expectedIssue.unsupportedBrowsers) { + // We only display a single icon per unsupported browser, so we need to + // group the expected unsupported browsers (versions) by their browser id. + const expectedUnsupportedBrowsersById = new Map(); + for (const unsupportedBrowser of expectedIssue.unsupportedBrowsers) { + if (!expectedUnsupportedBrowsersById.has(unsupportedBrowser.id)) { + expectedUnsupportedBrowsersById.set(unsupportedBrowser.id, []); + } + expectedUnsupportedBrowsersById + .get(unsupportedBrowser.id) + .push(unsupportedBrowser); + } + + const unsupportedBrowserListEl = issueEl.querySelector( + ".compatibility-unsupported-browser-list" + ); + const unsupportedBrowsersEl = + unsupportedBrowserListEl.querySelectorAll("li"); + + is( + unsupportedBrowsersEl.length, + expectedUnsupportedBrowsersById.size, + "The expected number of browser icons are displayed" + ); + + for (const unsupportedBrowserEl of unsupportedBrowsersEl) { + const expectedUnsupportedBrowsers = expectedUnsupportedBrowsersById.get( + unsupportedBrowserEl.getAttribute("data-browser-id") + ); + + ok(expectedUnsupportedBrowsers, "The expected browser is displayed"); + // debugger; + is( + unsupportedBrowserEl.querySelector(".compatibility-browser-version") + .innerText, + // If esr is not supported, but a newest version isn't as well, we don't display + // the esr version number + ( + expectedUnsupportedBrowsers.find( + ({ status }) => status !== "esr" + ) || expectedUnsupportedBrowsers[0] + ).version, + "The expected browser version is displayed" + ); + + is( + unsupportedBrowserEl.getAttribute("title"), + getFluentString("compatibility-issue-browsers-list", "title", { + browsers: expectedUnsupportedBrowsers + .map( + ({ name, status, version }) => + `${name} ${version}${status ? ` (${status})` : ""}` + ) + .join("\n"), + }), + "The brower item has the expected title attribute" + ); + } + } + + for (const [key, value] of Object.entries(expectedIssue)) { + const datasetKey = toCamelCase(`qa-${key}`); + is( + issueEl.dataset[datasetKey], + JSON.stringify(value), + `The value of ${datasetKey} is correct` + ); + } + + const propertyEl = issueEl.querySelector( + ".compatibility-issue-item__property" + ); + const MDN_CLASSNAME = "compatibility-issue-item__mdn-link"; + const SPEC_CLASSNAME = "compatibility-issue-item__spec-link"; + + is( + propertyEl.textContent, + property, + "property name is displayed as expected" + ); + + is( + propertyEl.classList.contains(MDN_CLASSNAME), + !!expectedIssue.url, + `${property} element ${ + expectedIssue.url ? "has" : "does not have" + } mdn link class` + ); + is( + propertyEl.classList.contains(SPEC_CLASSNAME), + !!expectedIssue.specUrl, + `${property} element ${ + expectedIssue.specUrl ? "has" : "does not have" + } spec link class` + ); + + if (expectedIssue.url || expectedIssue.specUrl) { + is( + propertyEl.nodeName.toLowerCase(), + "a", + `Link rendered for ${property}` + ); + + const expectedUrl = expectedIssue.url + ? expectedIssue.url + + "?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + : expectedIssue.specUrl; + const { link } = await simulateLinkClick(propertyEl); + is( + link, + expectedUrl, + `Click on ${property} link navigates user to expected url` + ); + } else { + is( + propertyEl.nodeName.toLowerCase(), + "span", + `No link rendered for ${property}` + ); + + const { link } = await simulateLinkClick(propertyEl); + is(link, null, `Click on ${property} does not navigate`); + } + } +} + +/** + * Check whether the content of node item element is matched with the expected values. + * + * @param {Element} panel + * @param {Array} expectedNodes + * e.g. + * [{ property: "margin-inline-end", nodes: ["body", "div.classname"] },...] + */ +async function assertNodeList(panel, expectedNodes) { + for (const { property, nodes } of expectedNodes) { + info(`Check nodes for ${property}`); + const issueEl = getIssueItem(property, panel); + + await waitUntil( + () => + issueEl.querySelectorAll(".compatibility-node-item").length === + nodes.length + ); + ok(true, "The number of nodes is correct"); + + const nodeEls = [...issueEl.querySelectorAll(".compatibility-node-item")]; + for (const node of nodes) { + const nodeEl = nodeEls.find(el => el.textContent === node); + ok(nodeEl, "The text content of the node element is correct"); + } + } +} + +/** + * Get IssueItem of given property from given element. + * + * @param {String} property + * @param {Element} element + * @return {Element} + */ +function getIssueItem(property, element) { + return element.querySelector(`[data-qa-property=\"\\"${property}\\"\"]`); +} + +/** + * Toggle enable/disable checkbox of a specific property on rule view. + * + * @param {Inspector} inspector + * @param {Number} ruleIndex + * @param {Number} propIndex + */ +async function togglePropStatusOnRuleView(inspector, ruleIndex, propIndex) { + const ruleView = inspector.getPanel("ruleview").view; + const rule = getRuleViewRuleEditor(ruleView, ruleIndex).rule; + // In case of inline style changes, we track the mutations via the + // inspector's markupmutation event to react to dynamic style changes + // which Resource Watcher doesn't cover yet. + // If an inline style is applied to the element, we need to wait on the + // markupmutation event + const onMutation = + ruleIndex === 0 ? inspector.once("markupmutation") : Promise.resolve(); + const textProp = rule.textProps[propIndex]; + const onRuleviewChanged = ruleView.once("ruleview-changed"); + textProp.editor.enable.click(); + await Promise.all([onRuleviewChanged, onMutation]); +} + +/** + * Return a promise which waits for COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE action. + * + * @param {Object} store + * @return {Promise} + */ +function waitForUpdateSelectedNodeAction(store) { + return waitForDispatch(store, COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE); +} + +/** + * Return a promise which waits for COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE action. + * + * @param {Object} store + * @return {Promise} + */ +function waitForUpdateTopLevelTargetAction(store) { + return waitForDispatch(store, COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE); +} diff --git a/devtools/client/inspector/compatibility/test/node/.eslintrc.js b/devtools/client/inspector/compatibility/test/node/.eslintrc.js new file mode 100644 index 0000000000..ffb3e70473 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/.eslintrc.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +module.exports = { + env: { + jest: true, + }, +}; diff --git a/devtools/client/inspector/compatibility/test/node/babel.config.js b/devtools/client/inspector/compatibility/test/node/babel.config.js new file mode 100644 index 0000000000..327f89b06b --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/babel.config.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +module.exports = { + plugins: [ + "@babel/plugin-proposal-async-generator-functions", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-proposal-optional-chaining", + "transform-amd-to-commonjs", + ], +}; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-CompatibilityApp.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-CompatibilityApp.test.js.snap new file mode 100644 index 0000000000..64839057c7 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-CompatibilityApp.test.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CompatibilityApp component renders with settings 1`] = ` +<section + className="compatibility-app theme-sidebar inspector-tabpanel" +> + <div + className="compatibility-app__container compatibility-app__container-hidden" + > + <Accordion + className="compatibility-app__main" + items={ + Array [ + Object { + "component": <IssuePane + issues={Array []} + />, + "header": "compatibility-selected-element-header", + "id": "compatibility-app--selected-element-pane", + "opened": true, + }, + Object { + "component": Array [ + <IssuePane + dispatch={[Function]} + issues={Array []} + />, + null, + ], + "header": "compatibility-all-elements-header", + "id": "compatibility-app--all-elements-pane", + "opened": true, + }, + ] + } + /> + <Connect(Footer) + className="compatibility-app__footer" + /> + </div> + <Connect(Settings) /> +</section> +`; + +exports[`CompatibilityApp component renders zero issues 1`] = ` +<section + className="compatibility-app theme-sidebar inspector-tabpanel" +> + <div + className="compatibility-app__container" + > + <Accordion + className="compatibility-app__main" + items={ + Array [ + Object { + "component": <IssuePane + issues={Array []} + />, + "header": "compatibility-selected-element-header", + "id": "compatibility-app--selected-element-pane", + "opened": true, + }, + Object { + "component": Array [ + <IssuePane + dispatch={[Function]} + issues={Array []} + />, + null, + ], + "header": "compatibility-all-elements-header", + "id": "compatibility-app--all-elements-pane", + "opened": true, + }, + ] + } + /> + <Connect(Footer) + className="compatibility-app__footer" + /> + </div> +</section> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Footer.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Footer.test.js.snap new file mode 100644 index 0000000000..62be938dd7 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Footer.test.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Footer component renders 1`] = ` +<footer + className="compatibility-footer" +> + <Localized + attrs={ + Object { + "title": true, + } + } + id="compatibility-settings-button-title" + > + <button + className="compatibility-footer__button" + onClick={[Function]} + title="compatibility-settings-button-title" + > + <img + className="compatibility-footer__icon" + src="chrome://devtools/skin/images/settings.svg" + /> + <Localized + id="compatibility-settings-button-label" + > + <label + className="compatibility-footer__label" + > + compatibility-settings-button-label + </label> + </Localized> + </button> + </Localized> +</footer> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueItem.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueItem.test.js.snap new file mode 100644 index 0000000000..3d219629e9 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueItem.test.js.snap @@ -0,0 +1,348 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IssueItem component renders a deprecated issue of CSS property 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--deprecated" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-deprecated" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-deprecated + </span> + </Localized> + </div> +</li> +`; + +exports[`IssueItem component renders a prefixNeeded issue of CSS property 1`] = ` +<li + className="compatibility-issue-item" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-prefixneeded" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-prefixneeded + </span> + </Localized> + </div> + <ul + className="compatibility-issue-item__aliases" + > + <li + className="compatibility-issue-item__alias" + key="test-alias-1" + > + test-alias-1 + </li> + <li + className="compatibility-issue-item__alias" + key="test-alias-2" + > + test-alias-2 + </li> + </ul> +</li> +`; + +exports[`IssueItem component renders an experimental issue of CSS property 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--experimental" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-experimental" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-experimental + </span> + </Localized> + </div> +</li> +`; + +exports[`IssueItem component renders an issue which has deprecated and experimental 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--deprecated compatibility-issue-item--experimental" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-deprecated-experimental" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-deprecated-experimental + </span> + </Localized> + </div> + <ul + className="compatibility-issue-item__aliases" + > + <li + className="compatibility-issue-item__alias" + key="test-alias-1" + > + test-alias-1 + </li> + <li + className="compatibility-issue-item__alias" + key="test-alias-2" + > + test-alias-2 + </li> + </ul> +</li> +`; + +exports[`IssueItem component renders an issue which has deprecated and prefixNeeded 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--deprecated" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-deprecated-prefixneeded" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-deprecated-prefixneeded + </span> + </Localized> + </div> + <ul + className="compatibility-issue-item__aliases" + > + <li + className="compatibility-issue-item__alias" + key="test-alias-1" + > + test-alias-1 + </li> + <li + className="compatibility-issue-item__alias" + key="test-alias-2" + > + test-alias-2 + </li> + </ul> +</li> +`; + +exports[`IssueItem component renders an issue which has deprecated, experimental and prefixNeeded 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--deprecated compatibility-issue-item--experimental" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-deprecated-experimental-prefixneeded" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-deprecated-experimental-prefixneeded + </span> + </Localized> + </div> + <ul + className="compatibility-issue-item__aliases" + > + <li + className="compatibility-issue-item__alias" + key="test-alias-1" + > + test-alias-1 + </li> + <li + className="compatibility-issue-item__alias" + key="test-alias-2" + > + test-alias-2 + </li> + </ul> +</li> +`; + +exports[`IssueItem component renders an issue which has experimental and prefixNeeded 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--experimental" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <Localized + id="compatibility-issue-experimental-prefixneeded" + > + <span + className="compatibility-issue-item__causes" + > + compatibility-issue-experimental-prefixneeded + </span> + </Localized> + </div> + <ul + className="compatibility-issue-item__aliases" + > + <li + className="compatibility-issue-item__alias" + key="test-alias-1" + > + test-alias-1 + </li> + <li + className="compatibility-issue-item__alias" + key="test-alias-2" + > + test-alias-2 + </li> + </ul> +</li> +`; + +exports[`IssueItem component renders an issue which has nodes that caused this issue 1`] = ` +<li + className="compatibility-issue-item" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + </div> + <NodePane + nodes={ + Array [ + Object { + "actorID": "test-actor", + "attributes": Array [], + "nodeName": "test-element", + "nodeType": 1, + }, + ] + } + /> +</li> +`; + +exports[`IssueItem component renders an unsupported issue of CSS property 1`] = ` +<li + className="compatibility-issue-item compatibility-issue-item--unsupported" + key="test-property" +> + <div + className="compatibility-issue-item__description" + > + <a + className="compatibility-issue-item__property devtools-monospace compatibility-issue-item__mdn-link" + href="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + onClick={[Function]} + title="test-url?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default" + > + test-property + </a> + <UnsupportedBrowserList + browsers={ + Array [ + Object { + "id": "firefox", + "name": "Firefox", + "status": "nightly", + "version": "70", + }, + ] + } + /> + </div> +</li> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueList.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueList.test.js.snap new file mode 100644 index 0000000000..e89f2e721e --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssueList.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IssueList component renders some issues 1`] = ` +<ul + className="compatibility-issue-list" +> + <IssueItem + deprecated={false} + experimental={true} + property="border-block-color" + type="CSS_PROPERTY" + unsupportedBrowsers={Array []} + url="https://developer.mozilla.org/docs/Web/CSS/border-block-color" + /> + <IssueItem + aliases={ + Array [ + "user-modify", + ] + } + deprecated={true} + experimental={false} + property="user-modify" + type="CSS_PROPERTY_ALIASES" + unsupportedBrowsers={Array []} + url="https://developer.mozilla.org/docs/Web/CSS/user-modify" + /> +</ul> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssuePane.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssuePane.test.js.snap new file mode 100644 index 0000000000..579c1c1429 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-IssuePane.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IssuePane component renders no issues 1`] = ` +<Localized + id="compatibility-no-issues-found" +> + <p + className="devtools-sidepanel-no-result" + > + compatibility-no-issues-found + </p> +</Localized> +`; + +exports[`IssuePane component renders some issues 1`] = ` +<IssueList + issues={ + Array [ + Object { + "deprecated": false, + "experimental": true, + "property": "border-block-color", + "type": "CSS_PROPERTY", + "unsupportedBrowsers": Array [], + "url": "https://developer.mozilla.org/docs/Web/CSS/border-block-color", + }, + Object { + "aliases": Array [ + "user-modify", + ], + "deprecated": true, + "experimental": false, + "property": "user-modify", + "type": "CSS_PROPERTY_ALIASES", + "unsupportedBrowsers": Array [], + "url": "https://developer.mozilla.org/docs/Web/CSS/user-modify", + }, + ] + } +/> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeItem.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeItem.test.js.snap new file mode 100644 index 0000000000..daf80ec227 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeItem.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NodeItem component renders a node 1`] = ` +<li + className="compatibility-node-item" +> + <span + className="objectBox objectBox-node clickable" + data-link-actor-id="test-actor" + onClick={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <span + className="tag-name" + > + test-element + </span> + </span> +</li> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeList.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeList.test.js.snap new file mode 100644 index 0000000000..6e99ac9537 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodeList.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NodeList component renders a node 1`] = ` +<ul + className="compatibility-node-list" +> + <NodeItem + node={ + Object { + "actorID": "test-actor", + "attributes": Array [], + "nodeName": "test-element", + "nodeType": 1, + } + } + /> +</ul> +`; + +exports[`NodeList component renders some nodes 1`] = ` +<ul + className="compatibility-node-list" +> + <NodeItem + node={ + Object { + "actorID": "test-actor-1", + "attributes": Array [], + "nodeName": "test-element-1", + "nodeType": 1, + } + } + /> + <NodeItem + node={ + Object { + "actorID": "test-actor-2", + "attributes": Array [], + "nodeName": "test-element-2", + "nodeType": 1, + } + } + /> +</ul> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodePane.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodePane.test.js.snap new file mode 100644 index 0000000000..2c2508703c --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-NodePane.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NodePane component renders a node 1`] = ` +<details + className="compatibility-node-pane" + open={true} +> + <Localized + $number={1} + id="compatibility-issue-occurrences" + > + <summary + className="compatibility-node-pane__summary" + > + compatibility-issue-occurrences + </summary> + </Localized> + <NodeList + nodes={ + Array [ + Object { + "actorID": "test-actor", + "attributes": Array [], + "nodeName": "test-element", + "nodeType": 1, + }, + ] + } + /> +</details> +`; + +exports[`NodePane component renders some nodes 1`] = ` +<details + className="compatibility-node-pane" + open={false} +> + <Localized + $number={2} + id="compatibility-issue-occurrences" + > + <summary + className="compatibility-node-pane__summary" + > + compatibility-issue-occurrences + </summary> + </Localized> + <NodeList + nodes={ + Array [ + Object { + "actorID": "test-actor-1", + "attributes": Array [], + "nodeName": "test-element-1", + "nodeType": 1, + }, + Object { + "actorID": "test-actor-2", + "attributes": Array [], + "nodeName": "test-element-2", + "nodeType": 1, + }, + ] + } + /> +</details> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Settings.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Settings.test.js.snap new file mode 100644 index 0000000000..2f81b42dd6 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-Settings.test.js.snap @@ -0,0 +1,367 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Settings component renders default browsers with a selected browsers 1`] = ` +<section + className="compatibility-settings" +> + <header + className="compatibility-settings__header" + > + <Localized + id="compatibility-settings-header" + > + <label + className="compatibility-settings__header-label" + > + compatibility-settings-header + </label> + </Localized> + <Localized + attrs={ + Object { + "title": true, + } + } + id="compatibility-close-settings-button" + > + <button + className="compatibility-settings__header-button" + onClick={[Function]} + title="compatibility-close-settings-button" + > + <img + className="compatibility-settings__header-icon" + src="chrome://devtools/skin/images/close.svg" + /> + </button> + </Localized> + </header> + <section + className="compatibility-settings__target-browsers" + > + <Localized + id="compatibility-target-browsers-header" + > + <header + className="compatibility-settings__target-browsers-header" + > + compatibility-target-browsers-header + </header> + </Localized> + <ul + className="compatibility-settings__target-browsers-list" + > + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={false} + data-id="firefox" + data-status="nightly" + id="firefox-nightly" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-nightly" + > + <BrowserIcon + id="firefox" + title="Firefox nightly" + /> + Firefox nightly (78) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={true} + data-id="firefox" + data-status="beta" + id="firefox-beta" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-beta" + > + <BrowserIcon + id="firefox" + title="Firefox beta" + /> + Firefox beta (77) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={false} + data-id="firefox" + data-status="current" + id="firefox-current" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-current" + > + <BrowserIcon + id="firefox" + title="Firefox current" + /> + Firefox current (76) + </label> + </li> + </ul> + </section> +</section> +`; + +exports[`Settings component renders default browsers with full selected browsers 1`] = ` +<section + className="compatibility-settings" +> + <header + className="compatibility-settings__header" + > + <Localized + id="compatibility-settings-header" + > + <label + className="compatibility-settings__header-label" + > + compatibility-settings-header + </label> + </Localized> + <Localized + attrs={ + Object { + "title": true, + } + } + id="compatibility-close-settings-button" + > + <button + className="compatibility-settings__header-button" + onClick={[Function]} + title="compatibility-close-settings-button" + > + <img + className="compatibility-settings__header-icon" + src="chrome://devtools/skin/images/close.svg" + /> + </button> + </Localized> + </header> + <section + className="compatibility-settings__target-browsers" + > + <Localized + id="compatibility-target-browsers-header" + > + <header + className="compatibility-settings__target-browsers-header" + > + compatibility-target-browsers-header + </header> + </Localized> + <ul + className="compatibility-settings__target-browsers-list" + > + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={true} + data-id="firefox" + data-status="nightly" + id="firefox-nightly" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-nightly" + > + <BrowserIcon + id="firefox" + title="Firefox nightly" + /> + Firefox nightly (78) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={true} + data-id="firefox" + data-status="beta" + id="firefox-beta" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-beta" + > + <BrowserIcon + id="firefox" + title="Firefox beta" + /> + Firefox beta (77) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={true} + data-id="firefox" + data-status="current" + id="firefox-current" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-current" + > + <BrowserIcon + id="firefox" + title="Firefox current" + /> + Firefox current (76) + </label> + </li> + </ul> + </section> +</section> +`; + +exports[`Settings component renders default browsers with no selected browsers 1`] = ` +<section + className="compatibility-settings" +> + <header + className="compatibility-settings__header" + > + <Localized + id="compatibility-settings-header" + > + <label + className="compatibility-settings__header-label" + > + compatibility-settings-header + </label> + </Localized> + <Localized + attrs={ + Object { + "title": true, + } + } + id="compatibility-close-settings-button" + > + <button + className="compatibility-settings__header-button" + onClick={[Function]} + title="compatibility-close-settings-button" + > + <img + className="compatibility-settings__header-icon" + src="chrome://devtools/skin/images/close.svg" + /> + </button> + </Localized> + </header> + <section + className="compatibility-settings__target-browsers" + > + <Localized + id="compatibility-target-browsers-header" + > + <header + className="compatibility-settings__target-browsers-header" + > + compatibility-target-browsers-header + </header> + </Localized> + <ul + className="compatibility-settings__target-browsers-list" + > + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={false} + data-id="firefox" + data-status="nightly" + id="firefox-nightly" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-nightly" + > + <BrowserIcon + id="firefox" + title="Firefox nightly" + /> + Firefox nightly (78) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={false} + data-id="firefox" + data-status="beta" + id="firefox-beta" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-beta" + > + <BrowserIcon + id="firefox" + title="Firefox beta" + /> + Firefox beta (77) + </label> + </li> + <li + className="compatibility-settings__target-browsers-item" + > + <input + checked={false} + data-id="firefox" + data-status="current" + id="firefox-current" + onChange={[Function]} + type="checkbox" + /> + <label + className="compatibility-settings__target-browsers-item-label" + htmlFor="firefox-current" + > + <BrowserIcon + id="firefox" + title="Firefox current" + /> + Firefox current (76) + </label> + </li> + </ul> + </section> +</section> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserItem.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserItem.test.js.snap new file mode 100644 index 0000000000..38cc5f96bc --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserItem.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnsupportedBrowserItem component renders the browser 1`] = ` +<Localized + $browsers="Firefox 113 (current) +Firefox 114 (beta) +Firefox 115 (release)" + attrs={ + Object { + "title": true, + } + } + id="compatibility-issue-browsers-list" +> + <li + className="compatibility-browser" + data-browser-id="firefox" + > + <BrowserIcon + id="firefox" + name="Firefox" + /> + <span + className="compatibility-browser-version" + > + 113 + </span> + </li> +</Localized> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserList.test.js.snap b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserList.test.js.snap new file mode 100644 index 0000000000..82b5fc573a --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/__snapshots__/components-compatibility-UnsupportedBrowserList.test.js.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnsupportedBrowserList component does not show ESR version if newer version is not supported 1`] = ` +<ul + className="compatibility-unsupported-browser-list" +> + <UnsupportedBrowserItem + id="firefox" + key="firefox" + name="Firefox" + unsupportedVersions={ + Array [ + Object { + "status": "esr", + "version": "102", + }, + Object { + "status": "current", + "version": "112", + }, + ] + } + version="112" + /> +</ul> +`; + +exports[`UnsupportedBrowserList component renders the browsers 1`] = ` +<ul + className="compatibility-unsupported-browser-list" +> + <UnsupportedBrowserItem + id="firefox" + key="firefox" + name="Firefox" + unsupportedVersions={ + Array [ + Object { + "status": "beta", + "version": "69", + }, + Object { + "status": "nightly", + "version": "70", + }, + ] + } + version="69" + /> + <UnsupportedBrowserItem + id="test-browser" + key="test-browser" + name="Test Browser" + unsupportedVersions={ + Array [ + Object { + "status": undefined, + "version": "1", + }, + Object { + "status": undefined, + "version": "2", + }, + ] + } + version="1" + /> + <UnsupportedBrowserItem + id="sample-browser" + key="sample-browser" + name="Sample Browser" + unsupportedVersions={ + Array [ + Object { + "status": undefined, + "version": "100", + }, + ] + } + version="100" + /> +</ul> +`; + +exports[`UnsupportedBrowserList component shows ESR version if newer version is supported 1`] = ` +<ul + className="compatibility-unsupported-browser-list" +> + <UnsupportedBrowserItem + id="firefox" + key="firefox" + name="Firefox" + unsupportedVersions={ + Array [ + Object { + "status": "esr", + "version": "102", + }, + ] + } + version="102" + /> + <UnsupportedBrowserItem + id="test-browser" + key="test-browser" + name="Test Browser" + unsupportedVersions={ + Array [ + Object { + "status": undefined, + "version": "1", + }, + ] + } + version="1" + /> +</ul> +`; diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-CompatibilityApp.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-CompatibilityApp.test.js new file mode 100644 index 0000000000..f519d41308 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-CompatibilityApp.test.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the CompatibilityApp component. + */ + +const { shallow } = require("enzyme"); +const { + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + thunk, +} = require("resource://devtools/client/shared/redux/middleware/thunk.js"); +const configureStore = require("redux-mock-store").default; + +const CompatibilityApp = createFactory( + require("resource://devtools/client/inspector/compatibility/components/CompatibilityApp.js") +); + +describe("CompatibilityApp component", () => { + it("renders zero issues", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({ + compatibility: { + selectedNodeIssues: [], + topLevelTargetIssues: [], + }, + }); + + const withLocalizationWrapper = shallow(CompatibilityApp({ store })); + const connectWrapper = withLocalizationWrapper.dive(); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders with settings", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({ + compatibility: { + isSettingsVisible: true, + selectedNodeIssues: [], + topLevelTargetIssues: [], + }, + }); + + const withLocalizationWrapper = shallow(CompatibilityApp({ store })); + const connectWrapper = withLocalizationWrapper.dive(); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Footer.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Footer.test.js new file mode 100644 index 0000000000..c217915a63 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Footer.test.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the Footer component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); +const { + thunk, +} = require("resource://devtools/client/shared/redux/middleware/thunk.js"); +const configureStore = require("redux-mock-store").default; + +const Footer = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/Footer.js") +); + +describe("Footer component", () => { + it("renders", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({}); + const connectWrapper = shallow(Footer({ store })); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueItem.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueItem.test.js new file mode 100644 index 0000000000..caec522e17 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueItem.test.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the IssueItem component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); +const IssueItem = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssueItem.js") +); + +describe("IssueItem component", () => { + it("renders an unsupported issue of CSS property", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + deprecated: false, + experimental: false, + prefixNeeded: false, + unsupportedBrowsers: [ + { id: "firefox", name: "Firefox", version: "70", status: "nightly" }, + ], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders a deprecated issue of CSS property", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + deprecated: true, + experimental: false, + prefixNeeded: false, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an experimental issue of CSS property", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + deprecated: false, + experimental: true, + prefixNeeded: false, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders a prefixNeeded issue of CSS property", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + aliases: ["test-alias-1", "test-alias-2"], + deprecated: false, + experimental: false, + prefixNeeded: true, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an issue which has deprecated and experimental", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + aliases: ["test-alias-1", "test-alias-2"], + deprecated: true, + experimental: true, + prefixNeeded: false, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an issue which has deprecated and prefixNeeded", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + aliases: ["test-alias-1", "test-alias-2"], + deprecated: true, + experimental: false, + prefixNeeded: true, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an issue which has experimental and prefixNeeded", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + aliases: ["test-alias-1", "test-alias-2"], + deprecated: false, + experimental: true, + prefixNeeded: true, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an issue which has deprecated, experimental and prefixNeeded", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + aliases: ["test-alias-1", "test-alias-2"], + deprecated: true, + experimental: true, + prefixNeeded: true, + unsupportedBrowsers: [], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders an issue which has nodes that caused this issue", () => { + const targetComponent = shallow( + IssueItem({ + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "test-property", + url: "test-url", + unsupportedBrowsers: [], + nodes: [ + { + actorID: "test-actor", + attributes: [], + nodeName: "test-element", + nodeType: 1, + }, + ], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueList.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueList.test.js new file mode 100644 index 0000000000..91431771b9 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssueList.test.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the IssueList component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); +const IssueList = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssueList.js") +); + +describe("IssueList component", () => { + it("renders some issues", () => { + const list = shallow( + IssueList({ + issues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "border-block-color", + url: "https://developer.mozilla.org/docs/Web/CSS/border-block-color", + deprecated: false, + experimental: true, + unsupportedBrowsers: [], + }, + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES, + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + aliases: ["user-modify"], + deprecated: true, + experimental: false, + unsupportedBrowsers: [], + }, + ], + }) + ); + expect(list).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssuePane.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssuePane.test.js new file mode 100644 index 0000000000..0885cdf183 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-IssuePane.test.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the IssuePane component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); +const IssuePane = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/IssuePane.js") +); + +describe("IssuePane component", () => { + it("renders no issues", () => { + const targetComponent = shallow(IssuePane({ issues: [] })); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders some issues", () => { + const targetComponent = shallow( + IssuePane({ + issues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "border-block-color", + url: "https://developer.mozilla.org/docs/Web/CSS/border-block-color", + deprecated: false, + experimental: true, + unsupportedBrowsers: [], + }, + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES, + property: "user-modify", + url: "https://developer.mozilla.org/docs/Web/CSS/user-modify", + aliases: ["user-modify"], + deprecated: true, + experimental: false, + unsupportedBrowsers: [], + }, + ], + }) + ); + expect(targetComponent).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeItem.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeItem.test.js new file mode 100644 index 0000000000..8e4f0b7a72 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeItem.test.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the NodeItem component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const NodeItem = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodeItem.js") +); + +describe("NodeItem component", () => { + it("renders a node", () => { + const pane = shallow( + NodeItem({ + node: { + actorID: "test-actor", + attributes: [], + nodeName: "test-element", + nodeType: 1, + }, + }) + ); + expect(pane).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeList.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeList.test.js new file mode 100644 index 0000000000..b9ede9a6fd --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodeList.test.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the NodeList component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const NodeList = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodeList.js") +); + +describe("NodeList component", () => { + it("renders a node", () => { + const pane = shallow( + NodeList({ + nodes: [ + { + actorID: "test-actor", + attributes: [], + nodeName: "test-element", + nodeType: 1, + }, + ], + }) + ); + expect(pane).toMatchSnapshot(); + }); +}); + +describe("NodeList component", () => { + it("renders some nodes", () => { + const pane = shallow( + NodeList({ + nodes: [ + { + actorID: "test-actor-1", + attributes: [], + nodeName: "test-element-1", + nodeType: 1, + }, + { + actorID: "test-actor-2", + attributes: [], + nodeName: "test-element-2", + nodeType: 1, + }, + ], + }) + ); + expect(pane).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodePane.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodePane.test.js new file mode 100644 index 0000000000..a9a996eb33 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-NodePane.test.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the NodePane component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const NodePane = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/NodePane.js") +); + +describe("NodePane component", () => { + it("renders a node", () => { + const pane = shallow( + NodePane({ + nodes: [ + { + actorID: "test-actor", + attributes: [], + nodeName: "test-element", + nodeType: 1, + }, + ], + }) + ); + expect(pane).toMatchSnapshot(); + }); +}); + +describe("NodePane component", () => { + it("renders some nodes", () => { + const pane = shallow( + NodePane({ + nodes: [ + { + actorID: "test-actor-1", + attributes: [], + nodeName: "test-element-1", + nodeType: 1, + }, + { + actorID: "test-actor-2", + attributes: [], + nodeName: "test-element-2", + nodeType: 1, + }, + ], + }) + ); + expect(pane).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Settings.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Settings.test.js new file mode 100644 index 0000000000..57db4cba67 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-Settings.test.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the Settings component. + */ + +const { shallow } = require("enzyme"); +const { + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + thunk, +} = require("resource://devtools/client/shared/redux/middleware/thunk.js"); +const configureStore = require("redux-mock-store").default; + +const Settings = createFactory( + require("resource://devtools/client/inspector/compatibility/components/Settings.js") +); + +const DEFAULT_BROWSERS = [ + { id: "firefox", name: "Firefox", status: "nightly", version: "78" }, + { id: "firefox", name: "Firefox", status: "beta", version: "77" }, + { id: "firefox", name: "Firefox", status: "current", version: "76" }, +]; + +describe("Settings component", () => { + it("renders default browsers with no selected browsers", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({ + compatibility: { + defaultTargetBrowsers: DEFAULT_BROWSERS, + targetBrowsers: [], + }, + }); + + const connectWrapper = shallow(Settings({ store })); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders default browsers with a selected browsers", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({ + compatibility: { + defaultTargetBrowsers: DEFAULT_BROWSERS, + targetBrowsers: [DEFAULT_BROWSERS[1]], + }, + }); + + const connectWrapper = shallow(Settings({ store })); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); + + it("renders default browsers with full selected browsers", () => { + const mockStore = configureStore([thunk()]); + const store = mockStore({ + compatibility: { + defaultTargetBrowsers: DEFAULT_BROWSERS, + targetBrowsers: DEFAULT_BROWSERS, + }, + }); + + const connectWrapper = shallow(Settings({ store })); + const targetComponent = connectWrapper.dive(); + expect(targetComponent).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserItem.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserItem.test.js new file mode 100644 index 0000000000..1ffd023fb9 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserItem.test.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the UnsupportedBrowserItem component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const BrowserItem = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js") +); + +describe("UnsupportedBrowserItem component", () => { + it("renders the browser", () => { + const item = shallow( + BrowserItem({ + id: "firefox", + name: "Firefox", + version: "113", + unsupportedVersions: [ + { + status: "current", + version: "113", + }, + { + status: "beta", + version: "114", + }, + { + status: "release", + version: "115", + }, + ], + }) + ); + expect(item).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserList.test.js b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserList.test.js new file mode 100644 index 0000000000..ca18480638 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/components/components-compatibility-UnsupportedBrowserList.test.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the BrowserList component. + */ + +const { shallow } = require("enzyme"); +const React = require("react"); + +const UnsupportedBrowserList = React.createFactory( + require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js") +); + +describe("UnsupportedBrowserList component", () => { + it("renders the browsers", () => { + const list = shallow( + UnsupportedBrowserList({ + browsers: [ + { id: "firefox", name: "Firefox", version: "69", status: "beta" }, + { id: "firefox", name: "Firefox", version: "70", status: "nightly" }, + { id: "test-browser", name: "Test Browser", version: "1" }, + { id: "test-browser", name: "Test Browser", version: "2" }, + { id: "sample-browser", name: "Sample Browser", version: "100" }, + ], + }) + ); + expect(list).toMatchSnapshot(); + }); + + it("does not show ESR version if newer version is not supported", () => { + const list = shallow( + UnsupportedBrowserList({ + browsers: [ + { id: "firefox", name: "Firefox", version: "102", status: "esr" }, + { id: "firefox", name: "Firefox", version: "112", status: "current" }, + ], + }) + ); + expect(list).toMatchSnapshot(); + }); + + it("shows ESR version if newer version is supported", () => { + const list = shallow( + UnsupportedBrowserList({ + browsers: [ + { id: "firefox", name: "Firefox", version: "102", status: "esr" }, + { id: "test-browser", name: "Test Browser", version: "1" }, + ], + }) + ); + expect(list).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/inspector/compatibility/test/node/jest.config.js b/devtools/client/inspector/compatibility/test/node/jest.config.js new file mode 100644 index 0000000000..3107116da9 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/jest.config.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global __dirname */ + +const sharedJestConfig = require(`${__dirname}/../../../../shared/test-helpers/shared-jest.config`); + +module.exports = { + ...sharedJestConfig, + setupFiles: ["<rootDir>setup.js"], + snapshotSerializers: ["enzyme-to-json/serializer"], +}; diff --git a/devtools/client/inspector/compatibility/test/node/package.json b/devtools/client/inspector/compatibility/test/node/package.json new file mode 100644 index 0000000000..f381ecd089 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/package.json @@ -0,0 +1,28 @@ +{ + "name": "devtools-client-inspector-compatibility-test", + "license": "MPL-2.0", + "version": "0.0.1", + "engines": { + "node": ">=8.9.4" + }, + "scripts": { + "test": "jest", + "test-ci": "jest --json" + }, + "dependencies": { + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-proposal-optional-chaining": "^7.8.3", + "@babel/plugin-proposal-class-properties": "7.10.4", + "babel-plugin-transform-amd-to-commonjs": "1.4.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.13.2", + "enzyme-to-json": "^3.3.5", + "jest": "^24.6.0", + "react": "16.4.1", + "react-dom": "16.4.1", + "react-test-renderer": "16.4.1", + "redux": "^4.0.4", + "redux-mock-store": "^1.5.3" + } +} diff --git a/devtools/client/inspector/compatibility/test/node/setup.js b/devtools/client/inspector/compatibility/test/node/setup.js new file mode 100644 index 0000000000..570e4462ae --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/setup.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +// Configure enzyme with React 16 adapter. +const Enzyme = require("enzyme"); +const Adapter = require("enzyme-adapter-react-16"); +Enzyme.configure({ adapter: new Adapter() }); + +const { + setMocksInGlobal, +} = require("resource://devtools/client/shared/test-helpers/shared-node-helpers.js"); +setMocksInGlobal(); diff --git a/devtools/client/inspector/compatibility/test/node/yarn.lock b/devtools/client/inspector/compatibility/test/node/yarn.lock new file mode 100644 index 0000000000..17edf3108b --- /dev/null +++ b/devtools/client/inspector/compatibility/test/node/yarn.lock @@ -0,0 +1,4334 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@^7.1.0": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff" + integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.4" + "@babel/helpers" "^7.6.2" + "@babel/parser" "^7.6.4" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.3" + "@babel/types" "^7.6.3" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.11.5": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" + integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== + dependencies: + "@babel/types" "^7.11.5" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.6.3", "@babel/generator@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== + dependencies: + "@babel/types" "^7.6.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" + integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + +"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + +"@babel/helper-remap-async-to-generator@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" + integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-wrap-function" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helper-wrap-function@^7.1.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" + integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.2.0" + +"@babel/helpers@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" + integrity sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA== + dependencies: + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.2" + "@babel/types" "^7.6.0" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.3", "@babel/parser@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== + +"@babel/parser@^7.10.4", "@babel/parser@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== + +"@babel/plugin-proposal-async-generator-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" + integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" + +"@babel/plugin-proposal-class-properties@7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + +"@babel/plugin-proposal-optional-chaining@^7.8.3": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + +"@babel/plugin-syntax-async-generators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" + integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" + integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" + +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.3" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/traverse@^7.10.4": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" + integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.5" + "@babel/types" "^7.11.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" + integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@jest/console@^24.7.1", "@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@types/babel__core@^7.1.0": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" + integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a" + integrity sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/node@*": + version "12.12.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.0.tgz#ff3201972d6dc851a9275308a17b9b5094e68057" + integrity sha512-6N8Sa5AaENRtJnpKXZgvc119PKxT1Lk9VPy4kfT8JF23tIe1qDfaGkBR2DRKJFIA7NptMz+fps//C6aLi1Uoug== + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/yargs-parser@*": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== + +"@types/yargs@^13.0.0": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== + dependencies: + "@types/yargs-parser" "*" + +abab@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" + integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +acorn-globals@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-walk@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^5.5.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" + integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== + +airbnb-prop-types@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" + integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== + dependencies: + array.prototype.find "^2.1.0" + function.prototype.name "^1.1.1" + has "^1.0.3" + is-regex "^1.0.4" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.1.0" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.9.0" + +ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +array.prototype.find@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" + integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + +array.prototype.flat@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz#8f3c71d245ba349b6b64b4078f76f5576f1fd723" + integrity sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.15.0" + function-bind "^1.1.1" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== + dependencies: + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" + +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" + +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== + dependencies: + "@types/babel__traverse" "^7.0.6" + +babel-plugin-transform-amd-to-commonjs@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-amd-to-commonjs/-/babel-plugin-transform-amd-to-commonjs-1.4.0.tgz#d9bc5003eaa26dbdd4e854e453f84903852af2ca" + integrity sha512-Xx0kYPn0LPyms+8n2KLn9yd2R5XMb2P1sNe4qn64/UQY5F2KFYlhhhyYUNm/BThfODAzl7rbaOsEfpU2M8iDKQ== + +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +bser@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.1" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +chownr@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.19.0, commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +convert-source-map@^1.1.0, convert-source-map@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + dependencies: + cssom "0.3.x" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + +dom-serializer@0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb" + integrity sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + +enzyme-adapter-react-16@^1.13.2: + version "1.15.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" + integrity sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA== + dependencies: + enzyme-adapter-utils "^1.12.1" + enzyme-shallow-equal "^1.0.0" + has "^1.0.3" + object.assign "^4.1.0" + object.values "^1.1.0" + prop-types "^15.7.2" + react-is "^16.10.2" + react-test-renderer "^16.0.0-0" + semver "^5.7.0" + +enzyme-adapter-utils@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz#e828e0d038e2b1efa4b9619ce896226f85c9dd88" + integrity sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g== + dependencies: + airbnb-prop-types "^2.15.0" + function.prototype.name "^1.1.1" + object.assign "^4.1.0" + object.fromentries "^2.0.1" + prop-types "^15.7.2" + semver "^5.7.0" + +enzyme-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69" + integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ== + dependencies: + has "^1.0.3" + object-is "^1.0.1" + +enzyme-to-json@^3.3.5: + version "3.4.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.3.tgz#ed4386f48768ed29e2d1a2910893542c34e7e0af" + integrity sha512-jqNEZlHqLdz7OTpXSzzghArSS3vigj67IU/fWkPyl1c0TCj9P5s6Ze0kRkYZWNEoCqCR79xlQbigYlMx5erh8A== + dependencies: + lodash "^4.17.15" + +enzyme@^3.9.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6" + integrity sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg== + dependencies: + array.prototype.flat "^1.2.1" + cheerio "^1.0.0-rc.2" + function.prototype.name "^1.1.0" + has "^1.0.3" + html-element-map "^1.0.0" + is-boolean-object "^1.0.0" + is-callable "^1.1.4" + is-number-object "^1.0.3" + is-regex "^1.0.4" + is-string "^1.0.4" + is-subset "^0.1.1" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.6.0" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.0.4" + object.values "^1.0.4" + raf "^3.4.0" + rst-selector-parser "^2.2.3" + string.prototype.trim "^1.1.2" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.15.0, es-abstract@^1.5.1: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.9.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" + integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +fbjs@^0.8.16: + version "0.8.17" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" + integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fs-minipass@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.0, function.prototype.name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392" + integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + functions-have-names "^1.1.1" + is-callable "^1.1.4" + +functions-have-names@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" + integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" + integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +handlebars@^4.1.2: + version "4.5.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.1.tgz#8a01c382c180272260d07f2d1aa3ae745715c7ba" + integrity sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== + +html-element-map@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c" + integrity sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA== + dependencies: + array-filter "^1.0.0" + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== + dependencies: + minimatch "^3.0.4" + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-boolean-object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" + integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M= + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number-object@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" + integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-string@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64" + integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ= + +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + dependencies: + handlebars "^4.1.2" + +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== + dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== + dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" + +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" + +jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" + integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== + dependencies: + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" + +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== + dependencies: + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== + dependencies: + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" + +jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== + dependencies: + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== + +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0, jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@^24.6.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moo@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" + integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nearley@^2.7.10: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.0.tgz#37717781d0fd0f2bfc95e233ebd75678ca4bda46" + integrity sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA== + dependencies: + commander "^2.19.0" + moo "^0.4.3" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + semver "^5.4.1" + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +neo-async@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.4.2: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" + integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7: + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + +object-is@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" + integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.entries@^1.0.4, object.entries@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" + integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +object.fromentries@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" + integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.15.0" + function-bind "^1.1.1" + has "^1.0.3" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.0.4, object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" + integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +prompts@^2.0.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.2.1.tgz#f901dd2a2dfee080359c0e20059b24188d75ad35" + integrity sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.3" + +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +psl@^1.1.24, psl@^1.1.28: + version "1.4.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" + integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +raf@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-dom@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6" + integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A== + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + +react-is@^16.10.2, react-is@^16.4.1, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" + integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== + +react-test-renderer@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70" + integrity sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ== + dependencies: + fbjs "^0.8.16" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-is "^16.4.1" + +react-test-renderer@^16.0.0-0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8" + integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.17.0" + +react@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32" + integrity sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg== + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@^2.0.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +redux-mock-store@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d" + integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA== + dependencies: + lodash.isplainobject "^4.0.6" + +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@^1.10.0, resolve@^1.3.2: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +scheduler@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe" + integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +sisteransi@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" + integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trim@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d" + integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + function-bind "^1.1.1" + +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tar@^4: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +tough-cookie@^2.3.3, tough-cookie@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +ua-parser-js@^0.7.18: + version "0.7.20" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098" + integrity sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw== + +uglify-js@^3.1.4: + version "3.6.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.5.tgz#b0ee796d2ae7e25672e04f65629b997cd4b30bd6" + integrity sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g== + dependencies: + commander "~2.20.3" + source-map "~0.6.1" + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +uuid@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" + integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@>=0.10.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^3.0.0, yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" diff --git a/devtools/client/inspector/compatibility/test/xpcshell/.eslintrc.js b/devtools/client/inspector/compatibility/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..86bd54c245 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/client/inspector/compatibility/test/xpcshell/head.js b/devtools/client/inspector/compatibility/test/xpcshell/head.js new file mode 100644 index 0000000000..733c0400da --- /dev/null +++ b/devtools/client/inspector/compatibility/test/xpcshell/head.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); diff --git a/devtools/client/inspector/compatibility/test/xpcshell/test_default-browsers.js b/devtools/client/inspector/compatibility/test/xpcshell/test_default-browsers.js new file mode 100644 index 0000000000..c565e77d01 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/xpcshell/test_default-browsers.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test for the default browsers of user settings. + +const { + getBrowsersList, +} = require("resource://devtools/shared/compatibility/compatibility-user-settings.js"); + +add_task(async () => { + info("Check whether each default browsers data are unique by id and status"); + + const defaultBrowsers = await getBrowsersList(); + + for (const target of defaultBrowsers) { + const count = defaultBrowsers.reduce( + (currentCount, browser) => + target.id === browser.id && target.status === browser.status + ? currentCount + 1 + : currentCount, + 0 + ); + + equal(count, 1, `This browser (${target.id} - ${target.status}) is unique`); + } +}); diff --git a/devtools/client/inspector/compatibility/test/xpcshell/xpcshell.toml b/devtools/client/inspector/compatibility/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..f5f7e97e39 --- /dev/null +++ b/devtools/client/inspector/compatibility/test/xpcshell/xpcshell.toml @@ -0,0 +1,7 @@ +[DEFAULT] +tags = "devtools" +head = "head.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] + +["test_default-browsers.js"] diff --git a/devtools/client/inspector/compatibility/types.js b/devtools/client/inspector/compatibility/types.js new file mode 100644 index 0000000000..d1f35bcc6a --- /dev/null +++ b/devtools/client/inspector/compatibility/types.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const browser = { + // The id of the browser which is defined in MDN compatibility dataset. + // e.g. "firefox" + // https://github.com/mdn/browser-compat-data/tree/master/browsers + id: PropTypes.string.isRequired, + // The browser name. + // e.g. "Firefox", "Firefox Android". + name: PropTypes.string.isRequired, + // The status of the browser. + // This should be one of "release", "beta", "nightly", "esr" or undefined. + status: PropTypes.string, + // The version of this browser. + // e.g. "70.0" + version: PropTypes.string.isRequired, +}; + +const node = PropTypes.object; + +const issue = { + // Type of this issue. The type should be one of COMPATIBILITY_ISSUE_TYPE. + type: PropTypes.string.isRequired, + // The CSS property which caused this issue. + property: PropTypes.string.isRequired, + // The url of MDN documentation for the CSS property. + url: PropTypes.string, + // The url of the specification for the CSS property. + specUrl: PropTypes.string, + // Whether the CSS property is deprecated or not. + deprecated: PropTypes.bool.isRequired, + // Whether the CSS property is experimental or not. + experimental: PropTypes.bool.isRequired, + // Whether the CSS property is needed prefix to cover all target browsers or not. + prefixNeeded: PropTypes.bool.isRequired, + // The browsers which do not support the CSS property. + unsupportedBrowsers: PropTypes.arrayOf(PropTypes.shape(browser)).isRequired, + // Nodes that caused this issue. This will be available for top-level target issues only. + nodes: PropTypes.arrayOf(node), + // Prefixed properties that the user set. + aliases: PropTypes.arrayOf(PropTypes.string), +}; + +exports.browser = browser; +exports.issue = issue; +exports.node = node; diff --git a/devtools/client/inspector/compatibility/utils/cases.js b/devtools/client/inspector/compatibility/utils/cases.js new file mode 100644 index 0000000000..5050e77ec2 --- /dev/null +++ b/devtools/client/inspector/compatibility/utils/cases.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function toCamelCase(text) { + return text.replace(/-([a-z])/gi, (str, group) => { + return group.toUpperCase(); + }); +} + +function toSnakeCase(text) { + return text.replace(/[a-z]([A-Z])/g, (str, group) => { + return `${str.charAt(0)}-${group.toLowerCase()}`; + }); +} + +module.exports = { + toCamelCase, + toSnakeCase, +}; diff --git a/devtools/client/inspector/compatibility/utils/moz.build b/devtools/client/inspector/compatibility/utils/moz.build new file mode 100644 index 0000000000..5de4c7b3ed --- /dev/null +++ b/devtools/client/inspector/compatibility/utils/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "cases.js", +) diff --git a/devtools/client/inspector/components/InspectorTabPanel.css b/devtools/client/inspector/components/InspectorTabPanel.css new file mode 100644 index 0000000000..6bffa61462 --- /dev/null +++ b/devtools/client/inspector/components/InspectorTabPanel.css @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.devtools-inspector-tab-panel { + width: 100%; + height: 100%; +} diff --git a/devtools/client/inspector/components/InspectorTabPanel.js b/devtools/client/inspector/components/InspectorTabPanel.js new file mode 100644 index 0000000000..0da70fb1d4 --- /dev/null +++ b/devtools/client/inspector/components/InspectorTabPanel.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +// Shortcuts +const { div } = dom; + +/** + * Helper panel component that is using an existing DOM node + * as the content. It's used by Sidebar as well as SplitBox + * components. + */ +class InspectorTabPanel extends Component { + static get propTypes() { + return { + // ID of the node that should be rendered as the content. + id: PropTypes.string.isRequired, + // Optional prefix for panel IDs. + idPrefix: PropTypes.string, + // Optional mount callback. + onMount: PropTypes.func, + // Optional unmount callback. + onUnmount: PropTypes.func, + }; + } + + static get defaultProps() { + return { + idPrefix: "", + }; + } + + componentDidMount() { + const doc = this.refs.content.ownerDocument; + const panel = doc.getElementById(this.props.idPrefix + this.props.id); + + // Append existing DOM node into panel's content. + this.refs.content.appendChild(panel); + + if (this.props.onMount) { + this.props.onMount(this.refs.content, this.props); + } + } + + componentWillUnmount() { + const doc = this.refs.content.ownerDocument; + const panels = doc.getElementById("tabpanels"); + + if (this.props.onUnmount) { + this.props.onUnmount(this.refs.content, this.props); + } + + // Move panel's content node back into list of tab panels. + panels.appendChild(this.refs.content.firstChild); + } + + render() { + return div({ + ref: "content", + className: "devtools-inspector-tab-panel", + }); + } +} + +module.exports = InspectorTabPanel; diff --git a/devtools/client/inspector/components/moz.build b/devtools/client/inspector/components/moz.build new file mode 100644 index 0000000000..cf6226361a --- /dev/null +++ b/devtools/client/inspector/components/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "InspectorTabPanel.js", +) diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js new file mode 100644 index 0000000000..7d2c129c7b --- /dev/null +++ b/devtools/client/inspector/computed/computed.js @@ -0,0 +1,1713 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const flags = require("resource://devtools/shared/flags.js"); +const ToolDefinitions = + require("resource://devtools/client/definitions.js").Tools; +const CssLogic = require("resource://devtools/shared/inspector/css-logic.js"); +const { + style: { ELEMENT_STYLE }, +} = require("resource://devtools/shared/constants.js"); +const OutputParser = require("resource://devtools/client/shared/output-parser.js"); +const { PrefObserver } = require("resource://devtools/client/shared/prefs.js"); +const { + createChild, +} = require("resource://devtools/client/inspector/shared/utils.js"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_FONT_TYPE, +} = require("resource://devtools/client/inspector/shared/node-types.js"); +const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js"); + +loader.lazyRequireGetter( + this, + "StyleInspectorMenu", + "resource://devtools/client/inspector/shared/style-inspector-menu.js" +); +loader.lazyRequireGetter( + this, + "KeyShortcuts", + "resource://devtools/client/shared/key-shortcuts.js" +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); + +const STYLE_INSPECTOR_PROPERTIES = + "devtools/shared/locales/styleinspector.properties"; +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); +const L10N_TWISTY_EXPAND_LABEL = STYLE_INSPECTOR_L10N.getStr( + "rule.twistyExpand.label" +); +const L10N_TWISTY_COLLAPSE_LABEL = STYLE_INSPECTOR_L10N.getStr( + "rule.twistyCollapse.label" +); + +const FILTER_CHANGED_TIMEOUT = 150; + +/** + * Helper for long-running processes that should yield occasionally to + * the mainloop. + * + * @param {Window} win + * Timeouts will be set on this window when appropriate. + * @param {Array} array + * The array of items to process. + * @param {Object} options + * Options for the update process: + * onItem {function} Will be called with the value of each iteration. + * onBatch {function} Will be called after each batch of iterations, + * before yielding to the main loop. + * onDone {function} Will be called when iteration is complete. + * onCancel {function} Will be called if the process is canceled. + * threshold {int} How long to process before yielding, in ms. + */ +function UpdateProcess(win, array, options) { + this.win = win; + this.index = 0; + this.array = array; + + this.onItem = options.onItem || function () {}; + this.onBatch = options.onBatch || function () {}; + this.onDone = options.onDone || function () {}; + this.onCancel = options.onCancel || function () {}; + this.threshold = options.threshold || 45; + + this.canceled = false; +} + +UpdateProcess.prototype = { + /** + * Error thrown when the array of items to process is empty. + */ + ERROR_ITERATION_DONE: new Error("UpdateProcess iteration done"), + + /** + * Schedule a new batch on the main loop. + */ + schedule() { + if (this.canceled) { + return; + } + this._timeout = setTimeout(this._timeoutHandler.bind(this), 0); + }, + + /** + * Cancel the running process. onItem will not be called again, + * and onCancel will be called. + */ + cancel() { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = 0; + } + this.canceled = true; + this.onCancel(); + }, + + _timeoutHandler() { + this._timeout = null; + try { + this._runBatch(); + this.schedule(); + } catch (e) { + if (e === this.ERROR_ITERATION_DONE) { + this.onBatch(); + this.onDone(); + return; + } + console.error(e); + throw e; + } + }, + + _runBatch() { + const time = Date.now(); + while (!this.canceled) { + const next = this._next(); + this.onItem(next); + if (Date.now() - time > this.threshold) { + this.onBatch(); + return; + } + } + }, + + /** + * Returns the item at the current index and increases the index. + * If all items have already been processed, will throw ERROR_ITERATION_DONE. + */ + _next() { + if (this.index < this.array.length) { + return this.array[this.index++]; + } + throw this.ERROR_ITERATION_DONE; + }, +}; + +/** + * CssComputedView is a panel that manages the display of a table + * sorted by style. There should be one instance of CssComputedView + * per style display (of which there will generally only be one). + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the computed view. + */ +function CssComputedView(inspector, document) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + + this.propertyViews = []; + + this._outputParser = new OutputParser(document, inspector.cssProperties); + + // Create bound methods. + this.focusWindow = this.focusWindow.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onClick = this._onClick.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this); + this.refreshPanel = this.refreshPanel.bind(this); + + const doc = this.styleDocument; + this.element = doc.getElementById("computed-property-container"); + this.searchField = doc.getElementById("computed-searchbox"); + this.searchClearButton = doc.getElementById("computed-searchinput-clear"); + this.includeBrowserStylesCheckbox = doc.getElementById( + "browser-style-checkbox" + ); + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("CmdOrCtrl+F", event => + this._onShortcut("CmdOrCtrl+F", event) + ); + this.shortcuts.on("Escape", event => this._onShortcut("Escape", event)); + this.styleDocument.addEventListener("copy", this._onCopy); + this.styleDocument.addEventListener("mousedown", this.focusWindow); + this.element.addEventListener("click", this._onClick); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.addEventListener( + "input", + this._onIncludeBrowserStyles + ); + + if (flags.testing) { + // In tests, we start listening immediately to avoid having to simulate a mousemove. + this.highlighters.addToView(this); + } else { + this.element.addEventListener( + "mousemove", + () => { + this.highlighters.addToView(this); + }, + { once: true } + ); + } + + if (!this.inspector.is3PaneModeEnabled) { + // When the rules view is added in 3 pane mode, refresh the Computed view whenever + // the rules are changed. + this.inspector.on( + "ruleview-added", + () => { + this.ruleView.on("ruleview-changed", this.refreshPanel); + }, + { once: true } + ); + } + + if (this.ruleView) { + this.ruleView.on("ruleview-changed", this.refreshPanel); + } + + this.searchClearButton.hidden = true; + + // No results text. + this.noResults = this.styleDocument.getElementById("computed-no-results"); + + // Refresh panel when color unit changed or pref for showing + // original sources changes. + this._handlePrefChange = this._handlePrefChange.bind(this); + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on("devtools.defaultColorUnit", this._handlePrefChange); + + // The element that we're inspecting, and the document that it comes from. + this._viewedElement = null; + // The PageStyle front related to the currently selected element + this.viewedElementPageStyle = null; + + this.createStyleViews(); + + // Add the tooltips and highlightersoverlay + this.tooltips = new TooltipsOverlay(this); +} + +/** + * Lookup a l10n string in the shared styleinspector string bundle. + * + * @param {String} name + * The key to lookup. + * @returns {String} localized version of the given key. + */ +CssComputedView.l10n = function (name) { + try { + return STYLE_INSPECTOR_L10N.getStr(name); + } catch (ex) { + console.log("Error reading '" + name + "'"); + throw new Error("l10n error with " + name); + } +}; + +CssComputedView.prototype = { + // Cache the list of properties that match the selected element. + _matchedProperties: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // Holds the ID of the panelRefresh timeout. + _panelRefreshTimeout: null, + + // Number of visible properties + numVisibleProperties: 0, + + get contextMenu() { + if (!this._contextMenu) { + this._contextMenu = new StyleInspectorMenu(this); + } + + return this._contextMenu; + }, + + // Get the highlighters overlay from the Inspector. + get highlighters() { + if (!this._highlighters) { + // highlighters is a lazy getter in the inspector. + this._highlighters = this.inspector.highlighters; + } + + return this._highlighters; + }, + + get includeBrowserStyles() { + return this.includeBrowserStylesCheckbox.checked; + }, + + get ruleView() { + return ( + this.inspector.hasPanel("ruleview") && + this.inspector.getPanel("ruleview").view + ); + }, + + _handlePrefChange() { + if (this._computed) { + this.refreshPanel(); + } + }, + + /** + * Update the view with a new selected element. The CssComputedView panel + * will show the style information for the given element. + * + * @param {NodeFront} element + * The highlighted node to get styles for. + * @returns a promise that will be resolved when highlighting is complete. + */ + selectElement(element) { + if (!element) { + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off( + "stylesheet-updated", + this.refreshPanel + ); + this.viewedElementPageStyle = null; + } + this._viewedElement = null; + this.noResults.hidden = false; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + // Hiding all properties + for (const propView of this.propertyViews) { + propView.refresh(); + } + return Promise.resolve(undefined); + } + + if (element === this._viewedElement) { + return Promise.resolve(undefined); + } + + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel); + } + this.viewedElementPageStyle = element.inspectorFront.pageStyle; + this.viewedElementPageStyle.on("stylesheet-updated", this.refreshPanel); + + this._viewedElement = element; + + this.refreshSourceFilter(); + + return this.refreshPanel(); + }, + + /** + * Get the type of a given node in the computed-view + * + * @param {DOMNode} node + * The node which we want information about + * @return {Object} The type information object contains the following props: + * - view {String} Always "computed" to indicate the computed view. + * - type {String} One of the VIEW_NODE_XXX_TYPE const in + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null if the node isn't anything we care about + */ + // eslint-disable-next-line complexity + getNodeInfo(node) { + if (!node) { + return null; + } + + const classes = node.classList; + + // Check if the node isn't a selector first since this doesn't require + // walking the DOM + if ( + classes.contains("matched") || + classes.contains("bestmatch") || + classes.contains("parentmatch") + ) { + let selectorText = ""; + + for (const child of node.childNodes[1].childNodes) { + if (child.nodeType === node.TEXT_NODE) { + selectorText += child.textContent; + } + } + return { + type: VIEW_NODE_SELECTOR_TYPE, + value: selectorText.trim(), + }; + } + + const propertyView = node.closest(".computed-property-view"); + const propertyMatchedSelectors = node.closest(".matchedselectors"); + const parent = propertyMatchedSelectors || propertyView; + + if (!parent) { + return null; + } + + let value, type; + + // Get the property and value for a node that's a property name or value + const isHref = + classes.contains("theme-link") && !classes.contains("computed-link"); + + if (classes.contains("computed-font-family")) { + if (propertyMatchedSelectors) { + const view = propertyMatchedSelectors.closest("li"); + value = { + property: view.querySelector(".computed-property-name").firstChild + .textContent, + value: node.parentNode.textContent, + }; + } else if (propertyView) { + value = { + property: parent.querySelector(".computed-property-name").firstChild + .textContent, + value: node.parentNode.textContent, + }; + } else { + return null; + } + } else if ( + propertyMatchedSelectors && + (classes.contains("computed-other-property-value") || isHref) + ) { + const view = propertyMatchedSelectors.closest("li"); + value = { + property: view.querySelector(".computed-property-name").firstChild + .textContent, + value: node.textContent, + }; + } else if ( + propertyView && + (classes.contains("computed-property-name") || + classes.contains("computed-property-value") || + isHref) + ) { + value = { + property: parent.querySelector(".computed-property-name").firstChild + .textContent, + value: parent.querySelector(".computed-property-value").textContent, + }; + } + + // Get the type + if (classes.contains("computed-property-name")) { + type = VIEW_NODE_PROPERTY_TYPE; + } else if ( + classes.contains("computed-property-value") || + classes.contains("computed-other-property-value") + ) { + type = VIEW_NODE_VALUE_TYPE; + } else if (classes.contains("computed-font-family")) { + type = VIEW_NODE_FONT_TYPE; + } else if (isHref) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value.url = node.href; + } else { + return null; + } + + return { + view: "computed", + type, + value, + }; + }, + + _createPropertyViews() { + if (this._createViewsPromise) { + return this._createViewsPromise; + } + + this.refreshSourceFilter(); + this.numVisibleProperties = 0; + const fragment = this.styleDocument.createDocumentFragment(); + + this._createViewsPromise = new Promise((resolve, reject) => { + this._createViewsProcess = new UpdateProcess( + this.styleWindow, + CssComputedView.propertyNames, + { + onItem: propertyName => { + // Per-item callback. + const propView = new PropertyView(this, propertyName); + fragment.append(propView.createListItemElement()); + + if (propView.visible) { + this.numVisibleProperties++; + } + this.propertyViews.push(propView); + }, + onCancel: () => { + reject("_createPropertyViews cancelled"); + }, + onDone: () => { + // Completed callback. + this.element.appendChild(fragment); + this.noResults.hidden = this.numVisibleProperties > 0; + resolve(undefined); + }, + } + ); + }); + + this._createViewsProcess.schedule(); + + return this._createViewsPromise; + }, + + isPanelVisible() { + return ( + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() == "computedview" + ); + }, + + /** + * Refresh the panel content. This could be called by a "ruleview-changed" event, but + * we avoid the extra processing unless the panel is visible. + */ + refreshPanel() { + if (!this._viewedElement || !this.isPanelVisible()) { + return Promise.resolve(); + } + + // Capture the current viewed element to return from the promise handler + // early if it changed + const viewedElement = this._viewedElement; + + return Promise.all([ + this._createPropertyViews(), + this.viewedElementPageStyle.getComputed(this._viewedElement, { + filter: this._sourceFilter, + onlyMatched: !this.includeBrowserStyles, + markMatched: true, + }), + ]) + .then(([, computed]) => { + if (viewedElement !== this._viewedElement) { + return Promise.resolve(); + } + + this._computed = computed; + this._matchedProperties = new Set(); + const customProperties = new Set(); + + for (const name in computed) { + if (computed[name].matched) { + this._matchedProperties.add(name); + } + if (name.startsWith("--")) { + customProperties.add(name); + } + } + + // Removing custom property PropertyViews which won't be used + let customPropertiesStartIndex; + for (let i = this.propertyViews.length - 1; i >= 0; i--) { + const propView = this.propertyViews[i]; + + // custom properties are displayed at the bottom of the list, and we're looping + // backward through propertyViews, so if the current item does not represent + // a custom property, we can stop looping. + if (!propView.isCustomProperty) { + customPropertiesStartIndex = i + 1; + break; + } + + // If the custom property will be used, move to the next item. + if (customProperties.has(propView.name)) { + customProperties.delete(propView.name); + continue; + } + + // Otherwise remove property view element + if (propView.element) { + propView.element.remove(); + } + + propView.destroy(); + this.propertyViews.splice(i, 1); + } + + // At this point, `customProperties` only contains custom property names for + // which we don't have a PropertyView yet. + let insertIndex = customPropertiesStartIndex; + for (const customPropertyName of Array.from(customProperties).sort()) { + const propertyView = new PropertyView( + this, + customPropertyName, + // isCustomProperty + true + ); + + const len = this.propertyViews.length; + if (insertIndex !== len) { + for (let i = insertIndex; i <= len; i++) { + const existingPropView = this.propertyViews[i]; + if ( + !existingPropView || + !existingPropView.isCustomProperty || + customPropertyName < existingPropView.name + ) { + insertIndex = i; + break; + } + } + } + this.propertyViews.splice(insertIndex, 0, propertyView); + + // Insert the custom property PropertyView at the right spot so we + // keep the list ordered. + const previousSibling = this.element.childNodes[insertIndex - 1]; + previousSibling.insertAdjacentElement( + "afterend", + propertyView.createListItemElement() + ); + } + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + this.noResults.hidden = true; + + // Reset visible property count + this.numVisibleProperties = 0; + + return new Promise((resolve, reject) => { + this._refreshProcess = new UpdateProcess( + this.styleWindow, + this.propertyViews, + { + onItem: propView => { + propView.refresh(); + }, + onCancel: () => { + reject("_refreshProcess of computed view cancelled"); + }, + onDone: () => { + this._refreshProcess = null; + this.noResults.hidden = this.numVisibleProperties > 0; + + const searchBox = this.searchField.parentNode; + searchBox.classList.toggle( + "devtools-searchbox-no-match", + !!this.searchField.value.length && !this.numVisibleProperties + ); + + this.inspector.emit("computed-view-refreshed"); + resolve(undefined); + }, + } + ); + this._refreshProcess.schedule(); + }); + }) + .catch(console.error); + }, + + /** + * Handle the shortcut events in the computed view. + */ + _onShortcut(name, event) { + if (!event.target.closest("#sidebar-panel-computedview")) { + return; + } + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + if ( + name === "Escape" && + event.target === this.searchField && + this._onClearSearch() + ) { + event.preventDefault(); + event.stopPropagation(); + } else if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles(value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles() { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + const filterTimeout = this.searchField.value.length + ? FILTER_CHANGED_TIMEOUT + : 0; + this.searchClearButton.hidden = this.searchField.value.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + this.refreshPanel(); + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch() { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + /** + * The change event handler for the includeBrowserStyles checkbox. + */ + _onIncludeBrowserStyles() { + this.refreshSourceFilter(); + this.refreshPanel(); + }, + + /** + * When includeBrowserStylesCheckbox.checked is false we only display + * properties that have matched selectors and have been included by the + * document or one of thedocument's stylesheets. If .checked is false we + * display all properties including those that come from UA stylesheets. + */ + refreshSourceFilter() { + this._matchedProperties = null; + this._sourceFilter = this.includeBrowserStyles + ? CssLogic.FILTER.UA + : CssLogic.FILTER.USER; + }, + + /** + * The CSS as displayed by the UI. + */ + createStyleViews() { + if (CssComputedView.propertyNames) { + return; + } + + CssComputedView.propertyNames = []; + + // Here we build and cache a list of css properties supported by the browser + // We could use any element but let's use the main document's root element + const styles = this.styleWindow.getComputedStyle( + this.styleDocument.documentElement + ); + const mozProps = []; + for (let i = 0, numStyles = styles.length; i < numStyles; i++) { + const prop = styles.item(i); + if (prop.startsWith("--")) { + // Skip any CSS variables used inside of browser CSS files + continue; + } else if (prop.startsWith("-")) { + mozProps.push(prop); + } else { + CssComputedView.propertyNames.push(prop); + } + } + + CssComputedView.propertyNames.sort(); + CssComputedView.propertyNames.push.apply( + CssComputedView.propertyNames, + mozProps.sort() + ); + + this._createPropertyViews().catch(e => { + if (!this._isDestroyed) { + console.warn( + "The creation of property views was cancelled because " + + "the computed-view was destroyed before it was done creating views" + ); + } else { + console.error(e); + } + }); + }, + + /** + * Get a set of properties that have matched selectors. + * + * @return {Set} If a property name is in the set, it has matching selectors. + */ + get matchedProperties() { + return this._matchedProperties || new Set(); + }, + + /** + * Focus the window on mousedown. + */ + focusWindow() { + this.styleWindow.focus(); + }, + + /** + * Context menu handler. + */ + _onContextMenu(event) { + // Call stopPropagation() and preventDefault() here so that avoid to show default + // context menu in about:devtools-toolbox. See Bug 1515265. + event.stopPropagation(); + event.preventDefault(); + this.contextMenu.show(event); + }, + + _onClick(event) { + const target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + openContentLink(target.href); + } + }, + + /** + * Callback for copy event. Copy selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy(event) { + const win = this.styleWindow; + const text = win.getSelection().toString().trim(); + if (text !== "") { + this.copySelection(); + event.preventDefault(); + } + }, + + /** + * Copy the current selection to the clipboard + */ + copySelection() { + try { + const win = this.styleWindow; + const text = win.getSelection().toString().trim(); + + clipboardHelper.copyString(text); + } catch (e) { + console.error(e); + } + }, + + /** + * Destructor for CssComputedView. + */ + destroy() { + this._viewedElement = null; + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel); + this.viewedElementPageStyle = null; + } + this._outputParser = null; + + this._prefObserver.off("devtools.defaultColorUnit", this._handlePrefChange); + this._prefObserver.destroy(); + + // Cancel tree construction + if (this._createViewsProcess) { + this._createViewsProcess.cancel(); + } + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + if (this._contextMenu) { + this._contextMenu.destroy(); + this._contextMenu = null; + } + + if (this._highlighters) { + this._highlighters.removeFromView(this); + this._highlighters = null; + } + + this.tooltips.destroy(); + + // Remove bound listeners + this.element.removeEventListener("click", this._onClick); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.styleDocument.removeEventListener("copy", this._onCopy); + this.styleDocument.removeEventListener("mousedown", this.focusWindow); + this.includeBrowserStylesCheckbox.removeEventListener( + "input", + this._onIncludeBrowserStyles + ); + + if (this.ruleView) { + this.ruleView.off("ruleview-changed", this.refreshPanel); + } + + // Nodes used in templating + this.element = null; + this.searchField = null; + this.searchClearButton = null; + this.includeBrowserStylesCheckbox = null; + + // Property views + for (const propView of this.propertyViews) { + propView.destroy(); + } + this.propertyViews = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + this._isDestroyed = true; + }, +}; + +function PropertyInfo(tree, name) { + this.tree = tree; + this.name = name; +} + +PropertyInfo.prototype = { + get isSupported() { + // There can be a mismatch between the list of properties + // supported on the server and on the client. + // Ideally we should build PropertyInfo only for property names supported on + // the server. See Bug 1722348. + return this.tree._computed && this.name in this.tree._computed; + }, + + get value() { + if (this.isSupported) { + const value = this.tree._computed[this.name].value; + return value; + } + return null; + }, +}; + +/** + * A container to give easy access to property data from the template engine. + */ +class PropertyView { + /* + * @param {CssComputedView} tree + * The CssComputedView instance we are working with. + * @param {String} name + * The CSS property name for which this PropertyView + * instance will render the rules. + * @param {Boolean} isCustomProperty + * Set to true if this will represent a custom property. + */ + constructor(tree, name, isCustomProperty = false) { + this.tree = tree; + this.name = name; + + this.isCustomProperty = isCustomProperty; + + if (!this.isCustomProperty) { + this.link = "https://developer.mozilla.org/docs/Web/CSS/" + name; + } + + this.#propertyInfo = new PropertyInfo(tree, name); + const win = this.tree.styleWindow; + this.#abortController = new win.AbortController(); + } + + // The parent element which contains the open attribute + element = null; + + // Property header node + propertyHeader = null; + + // Destination for property values + valueNode = null; + + // Are matched rules expanded? + matchedExpanded = false; + + // Matched selector container + matchedSelectorsContainer = null; + + // Matched selector expando + matchedExpander = null; + + // AbortController for event listeners + #abortController = null; + + // Cache for matched selector views + #matchedSelectorViews = null; + + // The previously selected element used for the selector view caches + #prevViewedElement = null; + + // PropertyInfo + #propertyInfo = null; + + /** + * Get the computed style for the current property. + * + * @return {String} the computed style for the current property of the + * currently highlighted element. + */ + get value() { + return this.propertyInfo.value; + } + + /** + * An easy way to access the CssPropertyInfo behind this PropertyView. + */ + get propertyInfo() { + return this.#propertyInfo; + } + + /** + * Does the property have any matched selectors? + */ + get hasMatchedSelectors() { + return this.tree.matchedProperties.has(this.name); + } + + /** + * Should this property be visible? + */ + get visible() { + if (!this.tree._viewedElement) { + return false; + } + + if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { + return false; + } + + const searchTerm = this.tree.searchField.value.toLowerCase(); + const isValidSearchTerm = !!searchTerm.trim().length; + if ( + isValidSearchTerm && + !this.name.toLowerCase().includes(searchTerm) && + !this.value.toLowerCase().includes(searchTerm) + ) { + return false; + } + + return this.propertyInfo.isSupported; + } + + /** + * Returns the className that should be assigned to the propertyView. + * + * @return {String} + */ + get propertyHeaderClassName() { + return this.visible ? "computed-property-view" : "computed-property-hidden"; + } + + /** + * Create DOM elements for a property + * + * @return {Element} The <li> element + */ + createListItemElement() { + const doc = this.tree.styleDocument; + const baseEventListenerConfig = { signal: this.#abortController.signal }; + + // Build the container element + this.onMatchedToggle = this.onMatchedToggle.bind(this); + this.element = doc.createElement("li"); + this.element.className = this.propertyHeaderClassName; + this.element.addEventListener( + "dblclick", + this.onMatchedToggle, + baseEventListenerConfig + ); + + // Make it keyboard navigable + this.element.setAttribute("tabindex", "0"); + this.shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: this.element, + }); + this.shortcuts.on("F1", event => { + this.mdnLinkClick(event); + // Prevent opening the options panel + event.preventDefault(); + event.stopPropagation(); + }); + this.shortcuts.on("Return", this.onMatchedToggle); + this.shortcuts.on("Space", this.onMatchedToggle); + + const nameContainer = doc.createElement("span"); + nameContainer.className = "computed-property-name-container"; + + // Build the twisty expand/collapse + this.matchedExpander = doc.createElement("div"); + this.matchedExpander.className = "computed-expander theme-twisty"; + this.matchedExpander.setAttribute("role", "button"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + this.matchedExpander.addEventListener( + "click", + this.onMatchedToggle, + baseEventListenerConfig + ); + + // Build the style name element + const nameNode = doc.createElement("span"); + nameNode.classList.add("computed-property-name", "theme-fg-color3"); + + // Give it a heading role for screen readers. + nameNode.setAttribute("role", "heading"); + + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + nameNode.setAttribute("tabindex", ""); + // Avoid english text (css properties) from being altered + // by RTL mode + nameNode.setAttribute("dir", "ltr"); + nameNode.textContent = nameNode.title = this.name; + // Make it hand over the focus to the container + const focusElement = () => this.element.focus(); + nameNode.addEventListener("click", focusElement, baseEventListenerConfig); + + // Build the style name ":" separator + const nameSeparator = doc.createElement("span"); + nameSeparator.classList.add("visually-hidden"); + nameSeparator.textContent = ": "; + nameNode.appendChild(nameSeparator); + + nameContainer.appendChild(nameNode); + + const valueContainer = doc.createElement("span"); + valueContainer.className = "computed-property-value-container"; + + // Build the style value element + this.valueNode = doc.createElement("span"); + this.valueNode.classList.add("computed-property-value", "theme-fg-color1"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.valueNode.setAttribute("tabindex", ""); + this.valueNode.setAttribute("dir", "ltr"); + // Make it hand over the focus to the container + this.valueNode.addEventListener( + "click", + focusElement, + baseEventListenerConfig + ); + + // Build the style value ";" separator + const valueSeparator = doc.createElement("span"); + valueSeparator.classList.add("visually-hidden"); + valueSeparator.textContent = ";"; + + // Build the matched selectors container + this.matchedSelectorsContainer = doc.createElement("div"); + this.matchedSelectorsContainer.classList.add("matchedselectors"); + + valueContainer.append(this.valueNode, valueSeparator); + this.element.append( + this.matchedExpander, + nameContainer, + valueContainer, + this.matchedSelectorsContainer + ); + + return this.element; + } + + /** + * Refresh the panel's CSS property value. + */ + refresh() { + const className = this.propertyHeaderClassName; + if (this.element.className !== className) { + this.element.className = className; + } + + 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"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + return; + } + + this.tree.numVisibleProperties++; + + const outputParser = this.tree._outputParser; + const frag = outputParser.parseCssProperty( + this.propertyInfo.name, + this.propertyInfo.value, + { + colorSwatchClass: "computed-colorswatch", + colorClass: "computed-color", + urlClass: "theme-link", + fontFamilyClass: "computed-font-family", + // No need to use baseURI here as computed URIs are never relative. + } + ); + this.valueNode.innerHTML = ""; + this.valueNode.appendChild(frag); + + this.refreshMatchedSelectors(); + } + + /** + * Refresh the panel matched rules. + */ + refreshMatchedSelectors() { + const hasMatchedSelectors = this.hasMatchedSelectors; + this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; + + if (hasMatchedSelectors) { + this.matchedExpander.classList.add("computed-expandable"); + } else { + this.matchedExpander.classList.remove("computed-expandable"); + } + + if (this.matchedExpanded && hasMatchedSelectors) { + return this.tree.viewedElementPageStyle + .getMatchedSelectors(this.tree._viewedElement, this.name) + .then(matched => { + if (!this.matchedExpanded) { + return; + } + + this._matchedSelectorResponse = matched; + + this.#buildMatchedSelectors(); + this.matchedExpander.setAttribute("open", ""); + this.matchedExpander.setAttribute( + "aria-label", + L10N_TWISTY_COLLAPSE_LABEL + ); + this.tree.inspector.emit("computed-view-property-expanded"); + }) + .catch(console.error); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + this.tree.inspector.emit("computed-view-property-collapsed"); + return Promise.resolve(undefined); + } + + get matchedSelectors() { + return this._matchedSelectorResponse; + } + + #buildMatchedSelectors() { + const frag = this.element.ownerDocument.createDocumentFragment(); + + for (const selector of this.matchedSelectorViews) { + const p = createChild(frag, "p"); + const span = createChild(p, "span", { + class: "rule-link", + }); + + const link = createChild(span, "a", { + target: "_blank", + class: "computed-link theme-link", + title: selector.longSource, + sourcelocation: selector.source, + tabindex: "0", + textContent: selector.source, + }); + link.addEventListener("click", selector.openStyleEditor); + const shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: link, + }); + shortcuts.on("Return", () => selector.openStyleEditor()); + + const status = createChild(p, "span", { + dir: "ltr", + class: "rule-text theme-fg-color3 " + selector.statusClass, + title: selector.statusText, + }); + + // Add an explicit status text span for screen readers. + // They won't pick up the title from the status span. + createChild(status, "span", { + dir: "ltr", + class: "visually-hidden", + textContent: selector.statusText + " ", + }); + + createChild(status, "div", { + class: "fix-get-selection", + textContent: selector.sourceText, + }); + + const valueDiv = createChild(status, "div", { + class: + "fix-get-selection computed-other-property-value theme-fg-color1", + }); + valueDiv.appendChild(selector.outputFragment); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedSelectorsContainer.appendChild(frag); + } + + /** + * Provide access to the matched SelectorViews that we are currently + * displaying. + */ + get matchedSelectorViews() { + if (!this.#matchedSelectorViews) { + this.#matchedSelectorViews = []; + this._matchedSelectorResponse.forEach(selectorInfo => { + const selectorView = new SelectorView(this.tree, selectorInfo); + this.#matchedSelectorViews.push(selectorView); + }, this); + } + return this.#matchedSelectorViews; + } + + /** + * The action when a user expands matched selectors. + * + * @param {Event} event + * Used to determine the class name of the targets click + * event. + */ + onMatchedToggle(event) { + if (event.shiftKey) { + return; + } + this.matchedExpanded = !this.matchedExpanded; + this.refreshMatchedSelectors(); + event.preventDefault(); + } + + /** + * The action when a user clicks on the MDN help link for a property. + */ + mdnLinkClick(event) { + if (!this.link) { + return; + } + openContentLink(this.link); + } + + /** + * Destroy this property view, removing event listeners + */ + destroy() { + if (this.#matchedSelectorViews) { + for (const view of this.#matchedSelectorViews) { + view.destroy(); + } + } + + if (this.#abortController) { + this.#abortController.abort(); + this.#abortController = null; + } + + if (this.shortcuts) { + this.shortcuts.destroy(); + } + + this.shortcuts = null; + this.element = null; + this.matchedExpander = null; + this.valueNode = null; + } +} + +/** + * A container to give us easy access to display data from a CssRule + * + * @param CssComputedView tree + * the owning CssComputedView + * @param selectorInfo + */ +function SelectorView(tree, selectorInfo) { + this.tree = tree; + this.selectorInfo = selectorInfo; + this._cacheStatusNames(); + + this.openStyleEditor = this.openStyleEditor.bind(this); + this._updateLocation = this._updateLocation.bind(this); + + const rule = this.selectorInfo.rule; + if (!rule || !rule.parentStyleSheet || rule.type == ELEMENT_STYLE) { + this.source = CssLogic.l10n("rule.sourceElement"); + this.longSource = this.source; + } else { + // This always refers to the generated location. + const sheet = rule.parentStyleSheet; + const sourceSuffix = rule.line > 0 ? ":" + rule.line : ""; + this.source = CssLogic.shortSource(sheet) + sourceSuffix; + this.longSource = CssLogic.longSource(sheet) + sourceSuffix; + + this.generatedLocation = { + sheet, + href: sheet.href || sheet.nodeHref, + line: rule.line, + column: rule.column, + }; + this.sourceMapURLService = this.tree.inspector.toolbox.sourceMapURLService; + this._unsubscribeCallback = this.sourceMapURLService.subscribeByID( + this.generatedLocation.sheet.resourceId, + this.generatedLocation.line, + this.generatedLocation.column, + this._updateLocation + ); + } +} + +/** + * Decode for cssInfo.rule.status + * @see SelectorView.prototype._cacheStatusNames + * @see CssLogic.STATUS + */ +SelectorView.STATUS_NAMES = [ + // "Parent Match", "Matched", "Best Match" +]; + +SelectorView.CLASS_NAMES = ["parentmatch", "matched", "bestmatch"]; + +SelectorView.prototype = { + /** + * Cache localized status names. + * + * These statuses are localized inside the styleinspector.properties string + * bundle. + * @see css-logic.js - the CssLogic.STATUS array. + */ + _cacheStatusNames() { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (const status in CssLogic.STATUS) { + const i = CssLogic.STATUS[status]; + if (i > CssLogic.STATUS.UNMATCHED) { + const value = CssComputedView.l10n("rule.status." + status); + // Replace normal spaces with non-breaking spaces + SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0"); + } + } + }, + + /** + * A localized version of cssRule.status + */ + get statusText() { + return SelectorView.STATUS_NAMES[this.selectorInfo.status]; + }, + + /** + * Get class name for selector depending on status + */ + get statusClass() { + return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; + }, + + get href() { + if (this._href) { + return this._href; + } + const sheet = this.selectorInfo.rule.parentStyleSheet; + this._href = sheet ? sheet.href : "#"; + return this._href; + }, + + get sourceText() { + return this.selectorInfo.sourceText; + }, + + get value() { + return this.selectorInfo.value; + }, + + get outputFragment() { + // Sadly, because this fragment is added to the template by DOM Templater + // we lose any events that are attached. This means that URLs will open in a + // new window. At some point we should fix this by stopping using the + // templater. + const outputParser = this.tree._outputParser; + const frag = outputParser.parseCssProperty( + this.selectorInfo.name, + this.selectorInfo.value, + { + colorSwatchClass: "computed-colorswatch", + colorClass: "computed-color", + urlClass: "theme-link", + fontFamilyClass: "computed-font-family", + baseURI: this.selectorInfo.rule.href, + } + ); + return frag; + }, + + /** + * Update the text of the source link to reflect whether we're showing + * original sources or not. This is a callback for + * SourceMapURLService.subscribe, which see. + * + * @param {Object | null} originalLocation + * The original position object (url/line/column) or null. + */ + _updateLocation(originalLocation) { + if (!this.tree.element) { + return; + } + + // Update |currentLocation| to be whichever location is being + // displayed at the moment. + let currentLocation = this.generatedLocation; + if (originalLocation) { + const { url, line, column } = originalLocation; + currentLocation = { href: url, line, column }; + } + + const selector = '[sourcelocation="' + this.source + '"]'; + const link = this.tree.element.querySelector(selector); + if (link) { + const text = + CssLogic.shortSource(currentLocation) + ":" + currentLocation.line; + link.textContent = text; + } + + this.tree.inspector.emit("computed-view-sourcelinks-updated"); + }, + + /** + * When a css link is clicked this method is called in order to either: + * 1. Open the link in view source (for chrome stylesheets). + * 2. Open the link in the style editor. + * + * We can only view stylesheets contained in document.styleSheets inside the + * style editor. + */ + openStyleEditor() { + const inspector = this.tree.inspector; + const rule = this.selectorInfo.rule; + + // The style editor can only display stylesheets coming from content because + // chrome stylesheets are not listed in the editor's stylesheet selector. + // + // If the stylesheet is a content stylesheet we send it to the style + // editor else we display it in the view source window. + const parentStyleSheet = rule.parentStyleSheet; + if (!parentStyleSheet || parentStyleSheet.isSystem) { + inspector.toolbox.viewSource(rule.href, rule.line); + return; + } + + const { sheet, line, column } = this.generatedLocation; + if (ToolDefinitions.styleEditor.isToolSupported(inspector.toolbox)) { + inspector.toolbox.viewSourceInStyleEditorByResource(sheet, line, column); + } + }, + + /** + * Destroy this selector view, removing event listeners + */ + destroy() { + if (this._unsubscribeCallback) { + this._unsubscribeCallback(); + } + }, +}; + +function ComputedViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.computedView = new CssComputedView(this.inspector, this.document); + + this.onDetachedFront = this.onDetachedFront.bind(this); + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + + this.inspector.selection.on("detached-front", this.onDetachedFront); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); + this.inspector.styleChangeTracker.on("style-changed", this.refresh); + + this.computedView.selectElement(null); + + this.onSelected(); +} + +ComputedViewTool.prototype = { + isPanelVisible() { + if (!this.computedView) { + return false; + } + return this.computedView.isPanelVisible(); + }, + + onDetachedFront() { + this.onSelected(false); + }, + + async onSelected(selectElement = true) { + // Ignore the event if the view has been destroyed, or if it's inactive. + // But only if the current selection isn't null. If it's been set to null, + // let the update go through as this is needed to empty the view on + // navigation. + if (!this.computedView) { + return; + } + + const isInactive = + !this.isPanelVisible() && this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + if ( + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode() + ) { + this.computedView.selectElement(null); + return; + } + + if (selectElement) { + const done = this.inspector.updating("computed-view"); + await this.computedView.selectElement(this.inspector.selection.nodeFront); + done(); + } + }, + + refresh() { + if (this.isPanelVisible()) { + this.computedView.refreshPanel(); + } + }, + + onPanelSelected() { + if ( + this.inspector.selection.nodeFront === this.computedView._viewedElement + ) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + destroy() { + this.inspector.styleChangeTracker.off("style-changed", this.refresh); + this.inspector.sidebar.off("computedview-selected", this.refresh); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.selection.off("detached-front", this.onDetachedFront); + this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + + this.computedView.destroy(); + + this.computedView = this.document = this.inspector = null; + }, +}; + +exports.CssComputedView = CssComputedView; +exports.ComputedViewTool = ComputedViewTool; +exports.PropertyView = PropertyView; diff --git a/devtools/client/inspector/computed/moz.build b/devtools/client/inspector/computed/moz.build new file mode 100644 index 0000000000..4f84b2ebf0 --- /dev/null +++ b/devtools/client/inspector/computed/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "computed.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/devtools/client/inspector/computed/test/browser.toml b/devtools/client/inspector/computed/test/browser.toml new file mode 100644 index 0000000000..d4ec4fc993 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser.toml @@ -0,0 +1,86 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "doc_matched_selectors_imported_1.css", + "doc_matched_selectors_imported_2.css", + "doc_matched_selectors_imported_3.css", + "doc_matched_selectors_imported_4.css", + "doc_matched_selectors_imported_5.css", + "doc_matched_selectors_imported_6.css", + "doc_matched_selectors.html", + "doc_media_queries.html", + "doc_pseudoelement.html", + "doc_sourcemaps.css", + "doc_sourcemaps.css.map", + "doc_sourcemaps.html", + "doc_sourcemaps.scss", + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_computed_browser-styles.js"] + +["browser_computed_custom_properties.js"] + +["browser_computed_cycle_color.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_default_tab.js"] + +["browser_computed_getNodeInfo.js"] +skip-if = [ + "!debug && os == 'mac'", #Bug 1559033 + "a11y_checks", # Bugs 1849028 and 1858041 to investigate intermittent a11y_checks results +] + +["browser_computed_keybindings_01.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_keybindings_02.js"] + +["browser_computed_matched-selectors-order.js"] + +["browser_computed_matched-selectors-toggle.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_matched-selectors_01.js"] + +["browser_computed_matched-selectors_02.js"] + +["browser_computed_media-queries.js"] + +["browser_computed_no-results-placeholder.js"] + +["browser_computed_original-source-link.js"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try) + +["browser_computed_pseudo-element_01.js"] + +["browser_computed_refresh-on-ruleview-change.js"] + +["browser_computed_refresh-on-style-change_01.js"] + +["browser_computed_search-filter.js"] + +["browser_computed_search-filter_clear.js"] + +["browser_computed_search-filter_context-menu.js"] + +["browser_computed_search-filter_escape-keypress.js"] + +["browser_computed_search-filter_noproperties.js"] + +["browser_computed_select-and-copy-styles-01.js"] + +["browser_computed_select-and-copy-styles-02.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_shadow_host.js"] + +["browser_computed_style-editor-link.js"] +skip-if = ["true"] # bug 1307846 diff --git a/devtools/client/inspector/computed/test/browser_computed_browser-styles.js b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js new file mode 100644 index 0000000000..52477d21ed --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the checkbox to include browser styles works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + + info("Checking the default styles"); + is( + isPropertyVisible("color", view), + true, + "span #matches color property is visible" + ); + is( + isPropertyVisible("background-color", view), + false, + "span #matches background-color property is hidden" + ); + + info("Toggling the browser styles"); + const doc = view.styleDocument; + const checkbox = doc.querySelector(".includebrowserstyles"); + const onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + await onRefreshed; + + info("Checking the browser styles"); + is(isPropertyVisible("color", view), true, "span color property is visible"); + is( + isPropertyVisible("background-color", view), + true, + "span background-color property is visible" + ); +}); + +function isPropertyVisible(name, view) { + info("Checking property visibility for " + name); + const propertyViews = view.propertyViews; + for (const propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_custom_properties.js b/devtools/client/inspector/computed/test/browser_computed_custom_properties.js new file mode 100644 index 0000000000..230bffa726 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_custom_properties.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that custom properties are displayed in the computed view. + +const TEST_URI = ` + <style type="text/css"> + body { + --global-custom-property: red; + } + + h1 { + color: var(--global-custom-property); + } + + #match-1 { + --global-custom-property: blue; + --custom-property-1: lime; + } + #match-2 { + --global-custom-property: gold; + --custom-property-2: cyan; + } + </style> + <h1 id="match-1">Hello</h1> + <h1 id="match-2">World</h1> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + + await assertComputedPropertiesForNode(inspector, view, "body", [ + { + name: "--global-custom-property", + value: "red", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "#match-1", [ + { + name: "color", + value: "rgb(0, 0, 255)", + }, + { + name: "--custom-property-1", + value: "lime", + }, + { + name: "--global-custom-property", + value: "blue", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "#match-2", [ + { + name: "color", + value: "rgb(255, 215, 0)", + }, + { + name: "--custom-property-2", + value: "cyan", + }, + { + name: "--global-custom-property", + value: "gold", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "html", []); +}); + +async function assertComputedPropertiesForNode( + inspector, + view, + selector, + expected +) { + await selectNode(selector, inspector); + + const computedItems = getComputedViewProperties(view); + is( + computedItems.length, + expected.length, + `Computed view has the expected number of items for "${selector}"` + ); + for (let i = 0; i < computedItems.length; i++) { + const expectedData = expected[i]; + const computedEl = computedItems[i]; + const nameSpan = computedEl.querySelector(".computed-property-name"); + const valueSpan = computedEl.querySelector(".computed-property-value"); + + is( + nameSpan.firstChild.textContent, + expectedData.name, + `computed item #${i} for "${selector}" is the expected one` + ); + is( + valueSpan.textContent, + expectedData.value, + `computed item #${i} for "${selector}" has expected value` + ); + } +} diff --git a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js new file mode 100644 index 0000000000..465c453adf --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Computed view color cycling test. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #f00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + + info("Checking the property itself"); + let container = getComputedViewPropertyView(view, "color").valueNode; + await checkColorCycling(container, view); + + info("Checking matched selectors"); + container = await getComputedViewMatchedRules(view, "color"); + await checkColorCycling(container, view); +}); + +async function checkColorCycling(container, view) { + const valueNode = container.querySelector(".computed-color"); + const win = view.styleWindow; + + // "Authored" (default; currently the computed value) + is( + valueNode.textContent, + "rgb(255, 0, 0)", + "Color displayed as an RGB value." + ); + + const tests = [ + { + value: "hwb(0 0% 0%)", + comment: "Color displayed as an HWB value.", + }, + { + value: "red", + comment: "Color displayed as a color name.", + }, + { + value: "#f00", + comment: "Color displayed as a HEX value.", + }, + { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value.", + }, + { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value again.", + }, + ]; + + for (const test of tests) { + await checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +async function checkSwatchShiftClick(container, win, expectedValue, comment) { + const swatch = container.querySelector(".computed-colorswatch"); + const valueNode = container.querySelector(".computed-color"); + swatch.scrollIntoView(); + + const onUnitChange = once(swatch, "unit-change"); + EventUtils.synthesizeMouseAtCenter( + swatch, + { + type: "mousedown", + shiftKey: true, + }, + win + ); + // we need to have the mouse up event in order to make sure that the platform + // lets go of the last container, and is not waiting for something to happen. + // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1442153 + EventUtils.synthesizeMouseAtCenter(swatch, { type: "mouseup" }, win); + await onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_default_tab.js b/devtools/client/inspector/computed/test/browser_computed_default_tab.js new file mode 100644 index 0000000000..1a485a7cc7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_default_tab.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the computed view is initialized when the computed view is the default tab +// for the inspector. + +const TEST_URI = ` + <style type="text/css"> + #matches { + color: #F00; + } + </style> + <span id="matches">Some styled text</span> +`; + +add_task(async function () { + await pushPref("devtools.inspector.activeSidebar", "computedview"); + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + is( + isPropertyVisible("color", view), + true, + "span #matches color property is visible" + ); +}); + +function isPropertyVisible(name, view) { + info("Checking property visibility for " + name); + const propertyViews = view.propertyViews; + for (const propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js new file mode 100644 index 0000000000..0aa1c85ff2 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests various output of the computed-view's getNodeInfo method. +// This method is used by the HighlightersOverlay and TooltipsOverlay on mouseover to +// decide which highlighter or tooltip to show when hovering over a value/name/selector +// if any. +// +// For instance, browser_ruleview_selector-highlighter_01.js and +// browser_ruleview_selector-highlighter_02.js test that the selector +// highlighter appear when hovering over a selector in the rule-view. +// Since the code to make this work for the computed-view is 90% the same, +// there is no need for testing it again here. +// This test however serves as a unit test for getNodeInfo. + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("resource://devtools/client/inspector/shared/node-types.js"); + +const TEST_URI = ` + <style type="text/css"> + body { + background: red; + color: white; + } + div { + background: green; + } + div div { + background-color: yellow; + background-image: url(chrome://branding/content/icon64.png); + color: red; + } + </style> + <div><div id="testElement">Test element</div></div> +`; + +// Each item in this array must have the following properties: +// - desc {String} will be logged for information +// - getHoveredNode {Generator Function} received the computed-view instance as +// argument and must return the node to be tested +// - assertNodeInfo {Function} should check the validity of the nodeInfo +// argument it receives +const TEST_DATA = [ + { + desc: "Testing a null node", + getHoveredNode() { + return null; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, + { + desc: "Testing a useless node", + getHoveredNode(view) { + return view.element; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, + { + desc: "Testing a property name", + getHoveredNode(view) { + return getComputedViewProperty(view, "color").nameSpan; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "rgb(255, 0, 0)"); + }, + }, + { + desc: "Testing a property value", + getHoveredNode(view) { + return getComputedViewProperty(view, "color").valueSpan; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "rgb(255, 0, 0)"); + }, + }, + { + desc: "Testing an image url", + getHoveredNode(view) { + const { valueSpan } = getComputedViewProperty(view, "background-image"); + return valueSpan.querySelector(".theme-link"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "background-image"); + is(nodeInfo.value.value, 'url("chrome://branding/content/icon64.png")'); + is(nodeInfo.value.url, "chrome://branding/content/icon64.png"); + }, + }, + { + desc: "Testing a matched rule selector (bestmatch)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".bestmatch"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div div"); + }, + }, + { + desc: "Testing a matched rule selector (matched)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".matched"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div"); + }, + }, + { + desc: "Testing a matched rule selector (parentmatch)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".parentmatch"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "body"); + }, + }, + { + desc: "Testing a matched rule value", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".computed-other-property-value"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "red"); + }, + }, + { + desc: "Testing a matched rule stylesheet link", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".rule-link .theme-link"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#testElement", inspector); + + for (const { desc, getHoveredNode, assertNodeInfo } of TEST_DATA) { + info(desc); + const nodeInfo = view.getNodeInfo(await getHoveredNode(view)); + assertNodeInfo(nodeInfo); + } +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js new file mode 100644 index 0000000000..5a6681f139 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests computed view key bindings. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode(".matches", inspector); + + const propView = getFirstVisiblePropertyView(view); + const rulesTable = propView.matchedSelectorsContainer; + const matchedExpander = propView.element; + + info("Focusing the property"); + matchedExpander.scrollIntoView(); + const onMatchedExpanderFocus = once(matchedExpander, "focus", true); + EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow); + await onMatchedExpanderFocus; + + await checkToggleKeyBinding( + view.styleWindow, + "VK_SPACE", + rulesTable, + inspector + ); + await checkToggleKeyBinding( + view.styleWindow, + "VK_RETURN", + rulesTable, + inspector + ); + await checkHelpLinkKeybinding(view); +}); + +function getFirstVisiblePropertyView(view) { + let propView = null; + view.propertyViews.some(p => { + if (p.visible) { + propView = p; + return true; + } + return false; + }); + + return propView; +} + +async function checkToggleKeyBinding(win, key, rulesTable, inspector) { + info( + "Pressing " + + key + + " key a couple of times to check that the " + + "property gets expanded/collapsed" + ); + + const onExpand = inspector.once("computed-view-property-expanded"); + const onCollapse = inspector.once("computed-view-property-collapsed"); + + info("Expanding the property"); + EventUtils.synthesizeKey(key, {}, win); + await onExpand; + isnot(rulesTable.innerHTML, "", "The property has been expanded"); + + info("Collapsing the property"); + EventUtils.synthesizeKey(key, {}, win); + await onCollapse; + is(rulesTable.innerHTML, "", "The property has been collapsed"); +} + +function checkHelpLinkKeybinding(view) { + info('Check that MDN link is opened on "F1"'); + const propView = getFirstVisiblePropertyView(view); + return new Promise(resolve => { + propView.mdnLinkClick = function (event) { + ok(true, "Pressing F1 opened the MDN link"); + resolve(); + }; + EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js new file mode 100644 index 0000000000..46434f0660 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the computed-view keyboard navigation. + +const TEST_URI = ` + <style type="text/css"> + span { + font-variant: small-caps; + color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + info("Selecting the first computed style in the list"); + const firstStyle = view.styleDocument.querySelector( + "#computed-container .computed-property-view" + ); + ok(firstStyle, "First computed style found in panel"); + firstStyle.focus(); + + info("Tab to select the 2nd style and press return"); + let onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await onExpanded; + + info("Verify the 2nd style has been expanded"); + const secondStyleSelectors = view.styleDocument.querySelectorAll( + ".computed-property-view .matchedselectors" + )[1]; + ok(!!secondStyleSelectors.childNodes.length, "Matched selectors expanded"); + + info("Tab back up and test the same thing, with space"); + onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + EventUtils.synthesizeKey(" "); + await onExpanded; + + info("Verify the 1st style has been expanded too"); + const firstStyleSelectors = view.styleDocument.querySelectorAll( + ".computed-property-view .matchedselectors" + )[0]; + ok(!!firstStyleSelectors.childNodes.length, "Matched selectors expanded"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js new file mode 100644 index 0000000000..b90e86a295 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js @@ -0,0 +1,889 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for the order of matched selector in the computed view. +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + const checkMatchedSelectors = options => + checkBackgroundColorMatchedSelectors(inspector, view, options); + + info("matching rules with different specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "specificity", + class: "mySection", + }, + style: ` + #specificity.mySection { + --spec_highest: var(--winning-color); + background-color: var(--spec_highest); + } + #specificity { + background-color: var(--spec_lowest); + }`, + expectedMatchedSelectors: [ + // Higher specificity wins + { selector: "#specificity.mySection", value: "var(--spec_highest)" }, + { selector: "#specificity", value: "var(--spec_lowest)" }, + ], + }); + + info("matching rules with same specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "order-of-appearance", + }, + style: ` + #order-of-appearance { + background-color: var(--appearance-order_first); + } + #order-of-appearance { + --appearance-order_second: var(--winning-color); + background-color: var(--appearance-order_second); + }`, + expectedMatchedSelectors: [ + // Last rule in stylesheet wins + { + selector: "#order-of-appearance", + value: "var(--appearance-order_second)", + }, + { + selector: "#order-of-appearance", + value: "var(--appearance-order_first)", + }, + ], + }); + + info("matching rules on element with style attribute"); + await checkMatchedSelectors({ + elementAttributes: { + id: "style-attr", + style: "background-color: var(--style-attr_in-attr)", + }, + style: ` + main { + --style-attr_in-attr: var(--winning-color); + } + + #style-attr { + background-color: var(--style-attr_in-rule); + } + `, + expectedMatchedSelectors: [ + // style attribute wins + { selector: "this.style", value: "var(--style-attr_in-attr)" }, + { selector: "#style-attr", value: "var(--style-attr_in-rule)" }, + ], + }); + + info("matching rules on different layers"); + await checkMatchedSelectors({ + elementAttributes: { + id: "layers", + class: "layers", + }, + style: ` + @layer second { + .layers { + --layers_in-second: var(--winning-color); + background-color: var(--layers_in-second); + } + } + @layer first { + #layers { + background-color: var(--layers_in-first); + } + } + `, + expectedMatchedSelectors: [ + // rule in last declared layer wins + { selector: ".layers", value: "var(--layers_in-second)" }, + { selector: "#layers", value: "var(--layers_in-first)" }, + ], + }); + + info("matching rules on same layer, with same specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "same-layers-order-of-appearance", + }, + style: ` + @layer second { + #same-layers-order-of-appearance { + background-color: var(--same-layers-appearance-order_first); + } + + #same-layers-order-of-appearance { + --same-layers-appearance-order_second: var(--winning-color); + background-color: var(--same-layers-appearance-order_second); + } + } + `, + expectedMatchedSelectors: [ + // last rule in the layer wins + { + selector: "#same-layers-order-of-appearance", + value: "var(--same-layers-appearance-order_second)", + }, + { + selector: "#same-layers-order-of-appearance", + value: "var(--same-layers-appearance-order_first)", + }, + ], + }); + + info("matching rules some in layers, some not"); + await checkMatchedSelectors({ + elementAttributes: { + id: "in-layer-and-no-layer", + }, + style: ` + @layer second { + #in-layer-and-no-layer { + background-color: var(--in-layer-and-no-layer_in-second); + } + } + + @layer first { + #in-layer-and-no-layer { + background-color: var(--in-layer-and-no-layer_in-first); + } + } + + #in-layer-and-no-layer { + --in-layer-and-no-layer_no-layer: var(--winning-color); + background-color: var(--in-layer-and-no-layer_no-layer); + }`, + expectedMatchedSelectors: [ + // rule not in layer wins + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_no-layer)", + }, + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_in-second)", + }, + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_in-first)", + }, + ], + }); + + info( + "matching rules with different specificity and one property declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-specificity", + class: "myImportantSection", + }, + style: ` + #important-specificity.myImportantSection { + background-color: var(--important-spec_highest); + } + #important-specificity { + --important-spec_lowest-important: var(--winning-color); + background-color: var(--important-spec_lowest-important) !important; + }`, + expectedMatchedSelectors: [ + // lesser specificity, but value was set with !important + { + selector: "#important-specificity", + value: "var(--important-spec_lowest-important)", + }, + { + selector: "#important-specificity.myImportantSection", + value: "var(--important-spec_highest)", + }, + ], + }); + + info( + "matching rules with different specificity and all properties declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-specificity", + class: "myAllImportantSection", + }, + style: ` + #all-important-specificity.myAllImportantSection { + --all-important-spec_highest-important: var(--winning-color); + background-color: var(--all-important-spec_highest-important) !important; + } + #all-important-specificity { + background-color: var(--all-important-spec_lowest-important) !important; + }`, + expectedMatchedSelectors: [ + // all values !important, so highest specificity rule wins + { + selector: "#all-important-specificity.myAllImportantSection", + value: "var(--all-important-spec_highest-important)", + }, + { + selector: "#all-important-specificity", + value: "var(--all-important-spec_lowest-important)", + }, + ], + }); + + info( + "matching rules with same specificity and one property declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-order-of-appearance", + }, + style: ` + #important-order-of-appearance { + --important-appearance-order_first-important: var(--winning-color); + background-color: var(--important-appearance-order_first-important) !important; + } + #important-order-of-appearance { + background-color: var(--important-appearance-order_second); + }`, + expectedMatchedSelectors: [ + // same specificity, but this value was set with !important + { + selector: "#important-order-of-appearance", + value: "var(--important-appearance-order_first-important)", + }, + { + selector: "#important-order-of-appearance", + value: "var(--important-appearance-order_second)", + }, + ], + }); + + info( + "matching rules with same specificity and all properties declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-order-of-appearance", + }, + style: ` + #all-important-order-of-appearance { + background-color: var(--all-important-appearance-order_first-important) !important; + } + #all-important-order-of-appearance { + --all-important-appearance-order_second-important: var(--winning-color); + background-color: var(--all-important-appearance-order_second-important) !important; + }`, + expectedMatchedSelectors: [ + // all values !important, so latest rule in stylesheet wins + { + selector: "#all-important-order-of-appearance", + value: "var(--all-important-appearance-order_second-important)", + }, + { + selector: "#all-important-order-of-appearance", + value: "var(--all-important-appearance-order_first-important)", + }, + ], + }); + + info( + "matching rules with important property on element with style attribute" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-style-attr", + style: "background-color: var(--important-style-attr_in-attr);", + }, + style: ` + #important-style-attr { + --important-style-attr_in-rule-important: var(--winning-color); + background-color: var(--important-style-attr_in-rule-important) !important; + }`, + expectedMatchedSelectors: [ + // important property wins over style attribute + { + selector: "#important-style-attr", + value: "var(--important-style-attr_in-rule-important)", + }, + { selector: "this.style", value: "var(--important-style-attr_in-attr)" }, + ], + }); + + info( + "matching rules with important property on element with style attribute and important value" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-style-attr", + style: + "background-color: var(--all-important-style-attr_in-attr-important) !important;", + }, + style: ` + main { + --all-important-style-attr_in-attr-important: var(--winning-color); + } + #all-important-style-attr { + background-color: var(--all-important-style-attr_in-rule-important); + }`, + expectedMatchedSelectors: [ + // both values are important, so style attribute wins + { + selector: "this.style", + value: "var(--all-important-style-attr_in-attr-important)", + }, + { + selector: "#all-important-style-attr", + value: "var(--all-important-style-attr_in-rule-important)", + }, + ], + }); + + info( + "matching rules on different layer, with same specificity and important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-layers", + }, + style: ` + @layer second { + #important-layers { + background-color: var(--important-layers_in-second); + } + } + @layer first { + #important-layers { + --important-layers_in-first-important: var(--winning-color); + background-color: var(--important-layers_in-first-important) !important; + } + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-layers", + value: "var(--important-layers_in-first-important)", + }, + { + selector: "#important-layers", + value: "var(--important-layers_in-second)", + }, + ], + }); + + info( + "matching rules on different layer, with same specificity and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-layers", + }, + style: ` + @layer second { + #all-important-layers { + background-color: var(--all-important-layers_in-second-important) !important; + } + } + @layer first { + #all-important-layers { + --all-important-layers_in-first-important: var(--winning-color); + background-color: var(--all-important-layers_in-first-important) !important; + } + }`, + expectedMatchedSelectors: [ + // all properties are important, rule from first declared layer wins + { + selector: "#all-important-layers", + value: "var(--all-important-layers_in-first-important)", + }, + { + selector: "#all-important-layers", + value: "var(--all-important-layers_in-second-important)", + }, + ], + }); + + info( + "matching rules on same layer, with same specificity and important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-same-layers-order-of-appearance", + }, + style: ` + @layer second { + #important-same-layers-order-of-appearance { + --important-same-layers-appearance-order_first-important: var(--winning-color); + background-color: var(--important-same-layers-appearance-order_first-important) !important; + } + + #important-same-layers-order-of-appearance { + background-color: var(--important-same-layers-appearance-order_second); + } + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-same-layers-order-of-appearance", + value: "var(--important-same-layers-appearance-order_first-important)", + }, + { + selector: "#important-same-layers-order-of-appearance", + value: "var(--important-same-layers-appearance-order_second)", + }, + ], + }); + + info( + "matching rules on same layer, with same specificity and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-layers-order-of-appearance", + }, + style: ` + @layer second { + #all-important-same-layers-order-of-appearance { + background-color: var(--all-important-same-layers-appearance-order_first-important) !important; + } + + #all-important-same-layers-order-of-appearance { + --all-important-same-layers-appearance-order_second-important: var(--winning-color); + background-color: var(--all-important-same-layers-appearance-order_second-important) !important; + } + }`, + expectedMatchedSelectors: [ + // last rule with important property wins + { + selector: "#all-important-same-layers-order-of-appearance", + value: + "var(--all-important-same-layers-appearance-order_second-important)", + }, + { + selector: "#all-important-same-layers-order-of-appearance", + value: + "var(--all-important-same-layers-appearance-order_first-important)", + }, + ], + }); + + info("matching rules ,some in layers, some not, important values in layers"); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-in-layer-and-no-layer", + }, + style: ` + @layer second { + #important-in-layer-and-no-layer { + background-color: var(--important-in-layer-and-no-layer_in-second); + } + } + + @layer first { + #important-in-layer-and-no-layer { + --important-in-layer-and-no-layer_in-first-important: var(--winning-color); + background-color: var(--important-in-layer-and-no-layer_in-first-important) !important; + } + } + + #important-in-layer-and-no-layer { + background-color: var(--important-in-layer-and-no-layer_no-layer); + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_in-first-important)", + }, + // then rule not in layer + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_no-layer)", + }, + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_in-second)", + }, + ], + }); + + info("matching rules ,some in layers, some not, all important values"); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-in-layer-and-no-layer", + }, + style: ` + @layer second { + #all-important-in-layer-and-no-layer { + background-color: var(--all-important-in-layer-and-no-layer_in-second-important) !important; + } + } + + @layer first { + #all-important-in-layer-and-no-layer { + --all-important-in-layer-and-no-layer_in-first-important: var(--winning-color); + background-color: var(--all-important-in-layer-and-no-layer_in-first-important) !important; + } + } + + #all-important-in-layer-and-no-layer { + background-color: var(--all-important-in-layer-and-no-layer_no-layer-important) !important; + }`, + expectedMatchedSelectors: [ + // important properties in first declared layer wins + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_in-first-important)", + }, + // then following important rules in layers + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_in-second-important)", + }, + // then important rules not in layers + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_no-layer-important)", + }, + ], + }); + + info( + "matching rules ,some in layers, some not, and style attribute all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-in-layer-no-layer-style-attr", + style: + "background-color: var(--all-important-in-layer-no-layer-style-attr_in-attr-important) !important", + }, + style: ` + main { + --all-important-in-layer-no-layer-style-attr_in-attr-important: var(--winning-color); + } + + @layer second { + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_in-second-important) !important; + } + } + + @layer first { + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_in-first-important) !important; + } + } + + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_no-layer-important) !important; + }`, + expectedMatchedSelectors: [ + // important properties in style attribute wins + { + selector: "this.style", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-attr-important)", + }, + // then important property in first declared layer + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-first-important)", + }, + // then following important property in layers + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-second-important)", + }, + // then important property not in layers + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_no-layer-important)", + }, + ], + }); + + info( + "matching rules on same layer but different rules and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-layer-different-rule", + }, + style: ` + @layer first { + #all-important-same-layer-different-rule { + background-color: var(--all-important-same-layer-different-rule_first-important) !important; + } + } + + @layer first { + #all-important-same-layer-different-rule { + --all-important-same-layer-different-rule_second-important: var(--winning-color); + background-color: var(--all-important-same-layer-different-rule_second-important) !important; + } + }`, + expectedMatchedSelectors: [ + // last rule for the layer with important property wins + { + selector: "#all-important-same-layer-different-rule", + value: + "var(--all-important-same-layer-different-rule_second-important)", + }, + { + selector: "#all-important-same-layer-different-rule", + value: "var(--all-important-same-layer-different-rule_first-important)", + }, + ], + }); + + info( + "matching rules on same layer but different nested rules and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-nested-layer-different-rule", + }, + style: ` + @layer first { + @layer { + @layer second { + #all-important-same-nested-layer-different-rule { + background-color: var(--all-important-same-nested-layer-different-rule_first-important) !important; + } + } + + @layer second { + #all-important-same-nested-layer-different-rule { + --all-important-same-nested-layer-different-rule_second-important: var(--winning-color); + background-color: var(--all-important-same-nested-layer-different-rule_second-important) !important; + } + } + } + }`, + expectedMatchedSelectors: [ + // last rule for the layer with important property wins + { + selector: "#all-important-same-nested-layer-different-rule", + value: + "var(--all-important-same-nested-layer-different-rule_second-important)", + }, + { + selector: "#all-important-same-nested-layer-different-rule", + value: + "var(--all-important-same-nested-layer-different-rule_first-important)", + }, + ], + }); + + info("matching rules on different nameless layers and all important values"); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-different-nameless-layers", + }, + style: ` + @layer { + @layer first { + #all-important-different-nameless-layers { + --all-important-different-nameless-layers_first-important: var(--winning-color); + background-color: var(--all-important-different-nameless-layers_first-important) !important; + } + } + } + @layer { + @layer first { + #all-important-different-nameless-layers { + background-color: var(--all-important-different-nameless-layers_second-important) !important; + } + } + }`, + expectedMatchedSelectors: [ + // rule with important property in first declared layer wins + { + selector: "#all-important-different-nameless-layers", + value: "var(--all-important-different-nameless-layers_first-important)", + }, + { + selector: "#all-important-different-nameless-layers", + value: + "var(--all-important-different-nameless-layers_second-important)", + }, + ], + }); + + info("matching rules on different imported layers"); + // no provided style as rules are defined in doc_matched_selectors_imported_*.css + await checkMatchedSelectors({ + elementAttributes: { + id: "imported-layers", + }, + expectedMatchedSelectors: [ + // rule in last declared layer wins + { + selector: "#imported-layers", + value: "var(--imported-layers_in-anonymous-second)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-nested-importedSecond)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-anonymous-first)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedSecond)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedFirst-second)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedFirst-first)", + }, + ], + }); + + info("matching rules on different imported layers all with important values"); + // no provided style as rules are defined in doc_matched_selectors_imported_*.css + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-imported-layers", + }, + expectedMatchedSelectors: [ + // last important property in first declared layer wins + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedFirst-second-important)", + }, + // then earlier important property for first declared layer + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedFirst-first-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedSecond-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-anonymous-first-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-nested-importedSecond-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-anonymous-second-important)", + }, + ], + }); +}); + +async function checkBackgroundColorMatchedSelectors( + inspector, + view, + { elementAttributes, style, expectedMatchedSelectors } +) { + const elementId = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [elementAttributes, style], + (attr, _style) => { + const sectionEl = content.document.createElement("section"); + for (const [name, value] of Object.entries(attr)) { + sectionEl.setAttribute(name, value); + } + + if (_style) { + const styleEl = content.document.createElement("style"); + styleEl.innerText = _style; + styleEl.setAttribute("id", `style-${sectionEl.id}`); + content.document.head.append(styleEl); + } + content.document.querySelector("main").append(sectionEl); + + return sectionEl.id; + } + ); + const selector = `#${elementId}`; + await selectNode(selector, inspector); + + const bgColorComputedValue = await getComputedStyleProperty( + selector, + null, + "background-color" + ); + is( + bgColorComputedValue, + "rgb(0, 0, 255)", + `The created element does have a "blue" background-color` + ); + + const propertyView = getPropertyView(view, "background-color"); + ok(propertyView, "found PropertyView for background-color"); + const valueNode = propertyView.valueNode.querySelector(".computed-color"); + is( + valueNode.textContent, + "rgb(0, 0, 255)", + `The displayed computed value is the expected "blue"` + ); + + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true"); + + info("Expanding the matched selectors"); + propertyView.matchedExpanded = true; + await propertyView.refreshMatchedSelectors(); + + const selectorsEl = + propertyView.matchedSelectorsContainer.querySelectorAll(".rule-text"); + is( + selectorsEl.length, + expectedMatchedSelectors.length, + "Expected number of selectors are displayed" + ); + + selectorsEl.forEach((selectorEl, index) => { + is( + selectorEl.querySelector(".fix-get-selection").innerText, + expectedMatchedSelectors[index].selector, + `Selector #${index} is the expected one` + ); + is( + selectorEl.querySelector(".computed-other-property-value").innerText, + expectedMatchedSelectors[index].value, + `Selector #${index} has the expected background color` + ); + const classToMatch = index === 0 ? "bestmatch" : "matched"; + ok( + selectorEl.classList.contains(classToMatch), + `selector element has expected "${classToMatch}" class` + ); + }); + + // cleanup + await SpecialPowers.spawn(gBrowser.selectedBrowser, [elementId], id => { + // Remove added element and stylesheet + content.document.getElementById(id).remove(); + // Some test cases don't insert a style element + content.document.getElementById(`style-${id}`)?.remove(); + }); +} + +function getPropertyView(computedView, name) { + return computedView.propertyViews.find(view => view.name === name); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js new file mode 100644 index 0000000000..2de29c2607 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the computed view properties can be expanded and collapsed with +// either the twisty or by dbl-clicking on the container. + +const TEST_URI = ` + <style type="text/css"> , + html { color: #000000; font-size: 15pt; } + h1 { color: red; } + </style> + <h1>Some header text</h1> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("h1", inspector); + + await testExpandOnTwistyClick(view, inspector); + await testCollapseOnTwistyClick(view, inspector); + await testExpandOnDblClick(view, inspector); + await testCollapseOnDblClick(view, inspector); +}); + +async function testExpandOnTwistyClick({ styleDocument }, inspector) { + info("Testing that a property expands on twisty click"); + + info("Getting twisty element"); + const twisty = styleDocument.querySelector(".computed-expandable"); + ok(twisty, "Twisty found"); + + const onExpand = inspector.once("computed-view-property-expanded"); + info("Clicking on the twisty element"); + twisty.click(); + + await onExpand; + + // Expanded means the matchedselectors div is not empty + const matchedSelectorsEl = twisty + .closest(".computed-property-view") + .querySelector(".matchedselectors"); + ok( + !!matchedSelectorsEl.childNodes.length, + "Matched selectors are expanded on twisty click" + ); +} + +async function testCollapseOnTwistyClick({ styleDocument }, inspector) { + info("Testing that a property collapses on twisty click"); + + info("Getting twisty element"); + const twisty = styleDocument.querySelector(".computed-expandable"); + ok(twisty, "Twisty found"); + + const onCollapse = inspector.once("computed-view-property-collapsed"); + info("Clicking on the twisty element"); + twisty.click(); + + await onCollapse; + + // Collapsed means the matchedselectors div is empty + const matchedSelectorsEl = twisty + .closest(".computed-property-view") + .querySelector(".matchedselectors"); + is( + matchedSelectorsEl.childNodes.length, + 0, + "Matched selectors are collapsed on twisty click" + ); +} + +async function testExpandOnDblClick({ styleDocument, styleWindow }, inspector) { + info("Testing that a property expands on container dbl-click"); + + info("Getting computed property container"); + const container = styleDocument.querySelector( + "#computed-container .computed-property-view" + ); + ok(container, "Container found"); + + container.scrollIntoView(); + + const onExpand = inspector.once("computed-view-property-expanded"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, { clickCount: 2 }, styleWindow); + + await onExpand; + + // Expanded means the matchedselectors div is not empty + const matchedSelectorsEl = container.querySelector(".matchedselectors"); + ok( + !!matchedSelectorsEl.childNodes.length, + "Matched selectors are expanded on dblclick" + ); +} + +async function testCollapseOnDblClick( + { styleDocument, styleWindow }, + inspector +) { + info("Testing that a property collapses on container dbl-click"); + + info("Getting computed property container"); + const container = styleDocument.querySelector( + "#computed-container .computed-property-view" + ); + ok(container, "Container found"); + + const onCollapse = inspector.once("computed-view-property-collapsed"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, { clickCount: 2 }, styleWindow); + + await onCollapse; + + // Collapsed means the matchedselectors div is empty + const matchedSelectorsEl = container.querySelector(".matchedselectors"); + is( + matchedSelectorsEl.childNodes.length, + 0, + "Matched selectors are collapsed on dblclick" + ); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js new file mode 100644 index 0000000000..bb90dfb4ea --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Checking selector counts, matched rules and titles in the computed-view. + +const { + PropertyView, +} = require("resource://devtools/client/inspector/computed/computed.js"); +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + await selectNode("#test", inspector); + await testMatchedSelectors(view, inspector); +}); + +async function testMatchedSelectors(view, inspector) { + info("checking selector counts, matched rules and titles"); + + const nodeFront = await getNodeFront("#test", inspector); + is( + nodeFront, + view._viewedElement, + "style inspector node matches the selected node" + ); + + const propertyView = new PropertyView(view, "color"); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + await propertyView.refreshMatchedSelectors(); + + const numMatchedSelectors = propertyView.matchedSelectors.length; + is( + numMatchedSelectors, + 7, + "CssLogic returns the correct number of matched selectors for div" + ); + is( + propertyView.hasMatchedSelectors, + true, + "hasMatchedSelectors returns true" + ); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js new file mode 100644 index 0000000000..b0327a30cf --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for matched selector texts in the computed view. + +add_task(async function () { + await addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>"); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + + info("Checking the color property view"); + const propertyView = getPropertyView(view, "color"); + ok(propertyView, "found PropertyView for color"); + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true"); + + info("Expanding the matched selectors"); + propertyView.matchedExpanded = true; + await propertyView.refreshMatchedSelectors(); + + const span = + propertyView.matchedSelectorsContainer.querySelector("span.rule-text"); + ok(span, "Found the first table row"); + + const selector = propertyView.matchedSelectorViews[0]; + ok(selector, "Found the first matched selector view"); +}); + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function (view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_media-queries.js b/devtools/client/inspector/computed/test/browser_computed_media-queries.js new file mode 100644 index 0000000000..9f09fc7e3c --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_media-queries.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that we correctly display appropriate media query titles in the +// property view. + +const TEST_URI = URL_ROOT + "doc_media_queries.html"; + +var { + PropertyView, +} = require("resource://devtools/client/inspector/computed/computed.js"); + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + await checkPropertyView(view); +}); + +function checkPropertyView(view) { + const propertyView = new PropertyView(view, "width"); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + return propertyView.refreshMatchedSelectors().then(() => { + const numMatchedSelectors = propertyView.matchedSelectors.length; + + is( + numMatchedSelectors, + 2, + "Property view has the correct number of matched selectors for div" + ); + + is( + propertyView.hasMatchedSelectors, + true, + "hasMatchedSelectors returns true" + ); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js new file mode 100644 index 0000000000..3e63bfb9b3 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the no results placeholder works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + + await enterInvalidFilter(inspector, view); + checkNoResultsPlaceholderShown(view); + + await clearFilterText(inspector, view); + checkNoResultsPlaceholderHidden(view); +}); + +async function enterInvalidFilter(inspector, computedView) { + const searchbar = computedView.searchField; + const searchTerm = "xxxxx"; + + info('setting filter text to "' + searchTerm + '"'); + + const onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + synthesizeKeys(searchTerm, computedView.styleWindow); + await onRefreshed; +} + +function checkNoResultsPlaceholderShown(computedView) { + info("Checking that the no results placeholder is shown"); + + const placeholder = computedView.noResults; + const win = computedView.styleWindow; + const display = win.getComputedStyle(placeholder).display; + is(display, "block", "placeholder is visible"); +} + +async function clearFilterText(inspector, computedView) { + info("Clearing the filter text"); + + const searchbar = computedView.searchField; + + const onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + searchbar.value = ""; + EventUtils.synthesizeKey("c", {}, computedView.styleWindow); + await onRefreshed; +} + +function checkNoResultsPlaceholderHidden(computedView) { + info("Checking that the no results placeholder is hidden"); + + const placeholder = computedView.noResults; + const win = computedView.styleWindow; + const display = win.getComputedStyle(placeholder).display; + is(display, "none", "placeholder is hidden"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_original-source-link.js b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js new file mode 100644 index 0000000000..771d349b3b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the computed view shows the original source link when source maps +// are enabled. + +const TESTCASE_URI = URL_ROOT_SSL + "doc_sourcemaps.html"; +const PREF = "devtools.source-map.client-service.enabled"; +const SCSS_LOC = "doc_sourcemaps.scss:4"; +const CSS_LOC = "doc_sourcemaps.css:1"; + +add_task(async function () { + info("Turning the pref " + PREF + " on"); + Services.prefs.setBoolPref(PREF, true); + + await addTab(TESTCASE_URI); + const { toolbox, inspector, view } = await openComputedView(); + let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + await selectNode("div", inspector); + + info("Expanding the first property"); + await expandComputedViewPropertyByIndex(view, 0); + + info("Verifying the link text"); + await onLinksUpdated; + verifyLinkText(view, SCSS_LOC); + + info("Toggling the pref"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, false); + await onLinksUpdated; + + info("Verifying that the link text has changed after the pref change"); + await verifyLinkText(view, CSS_LOC); + + info("Toggling the pref again"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, true); + await onLinksUpdated; + + info("Testing that clicking on the link works"); + await testClickingLink(toolbox, view); + + info("Turning the pref " + PREF + " off"); + Services.prefs.clearUserPref(PREF); +}); + +async function testClickingLink(toolbox, view) { + const onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss"); + + info("Clicking the computedview stylesheet link"); + const link = getComputedViewLinkByIndex(view, 0); + link.scrollIntoView(); + link.click(); + + const editor = await onEditor; + + const { line } = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(view, text) { + const link = getComputedViewLinkByIndex(view, 0); + is( + link.textContent, + text, + "Linked text changed to display the correct location" + ); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js new file mode 100644 index 0000000000..e972a0257a --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that pseudoelements are displayed correctly in the rule view. + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + await testTopLeft(inspector, view); +}); + +async function testTopLeft(inspector, view) { + const node = await getNodeFront("#topleft", inspector.markup); + await selectNode(node, inspector); + const float = getComputedViewPropertyValue(view, "float"); + is(float, "left", "The computed view shows the correct float"); + + const children = await inspector.markup.walker.children(node); + is(children.nodes.length, 3, "Element has correct number of children"); + + const beforeElement = children.nodes[0]; + await selectNode(beforeElement, inspector); + let top = getComputedViewPropertyValue(view, "top"); + is(top, "0px", "The computed view shows the correct top"); + let left = getComputedViewPropertyValue(view, "left"); + is(left, "0px", "The computed view shows the correct left"); + + const afterElement = children.nodes[children.nodes.length - 1]; + await selectNode(afterElement, inspector); + top = getComputedViewPropertyValue(view, "top"); + is(top, "96px", "The computed view shows the correct top"); + left = getComputedViewPropertyValue(view, "left"); + is(left, "96px", "The computed view shows the correct left"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js new file mode 100644 index 0000000000..12b7901970 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the computed view refreshes when the rule view is updated in 3 pane mode. + +const TEST_URI = "<div id='target' style='color: rgb(255, 0, 0);'>test</div>"; + +add_task(async function () { + info( + "Check whether the color as well in computed view is updated " + + "when the rule in rule view is changed in case of 3 pane mode" + ); + await pushPref("devtools.inspector.three-pane-enabled", true); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#target", inspector); + + is( + getComputedViewPropertyValue(view, "color"), + "rgb(255, 0, 0)", + "The computed view shows the right color" + ); + + info("Change the value in the ruleview"); + const ruleView = inspector.getPanel("ruleview").view; + const editor = await getValueEditor(ruleView); + const onRuleViewChanged = ruleView.once("ruleview-changed"); + const onComputedViewRefreshed = inspector.once("computed-view-refreshed"); + editor.input.value = "rgb(0, 255, 0)"; + EventUtils.synthesizeKey("VK_RETURN", {}, ruleView.styleWindow); + await Promise.all([onRuleViewChanged, onComputedViewRefreshed]); + + info("Check the value in the computed view"); + is( + getComputedViewPropertyValue(view, "color"), + "rgb(0, 255, 0)", + "The computed value is updated when the rule in ruleview is changed" + ); +}); + +add_task(async function () { + info( + "Check that the computed view is not updated " + + "if the rule view is changed in 2 pane mode." + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openComputedView(); + await selectNode("#target", inspector); + + info("Select the rule view"); + const ruleView = inspector.getPanel("ruleview").view; + const onRuleViewReady = ruleView.once("ruleview-refreshed"); + const onSidebarSelect = inspector.sidebar.once("select"); + inspector.sidebar.select("ruleview"); + await Promise.all([onSidebarSelect, onRuleViewReady]); + + info( + "Prepare the counter which counts how many times computed view is refreshed" + ); + let computedViewRefreshCount = 0; + const computedViewRefreshListener = () => { + computedViewRefreshCount += 1; + }; + inspector.on("computed-view-refreshed", computedViewRefreshListener); + + info("Change the value in the rule view"); + const editor = await getValueEditor(ruleView); + const onRuleViewChanged = ruleView.once("ruleview-changed"); + editor.input.value = "rgb(0, 255, 0)"; + EventUtils.synthesizeKey("VK_RETURN", {}, ruleView.styleWindow); + await onRuleViewChanged; + + info( + "Wait for time enough to check whether the computed value is updated or not" + ); + await wait(1000); + + info("Check the counter"); + is(computedViewRefreshCount, 0, "The computed view is not updated"); + + inspector.off("computed-view-refreshed", computedViewRefreshListener); +}); + +async function getValueEditor(ruleView) { + const ruleEditor = ruleView.element.children[0]._ruleEditor; + const propEditor = ruleEditor.rule.textProps[0].editor; + return focusEditableField(ruleView, propEditor.valueSpan); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js new file mode 100644 index 0000000000..190593497b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the computed view refreshes when the current node has its style +// changed. + +const TEST_URI = "<div id='testdiv' style='font-size:10px;'>Test div!</div>"; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#testdiv", inspector); + + let fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "10px", "The computed view shows the right font-size"); + + info("Changing the node's style and waiting for the update"); + const onUpdated = inspector.once("computed-view-refreshed"); + await setContentPageElementAttribute( + "#testdiv", + "style", + "font-size: 15px; color: red;" + ); + await onUpdated; + + fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "15px", "The computed view shows the updated font-size"); + const color = getComputedViewPropertyValue(view, "color"); + is(color, "rgb(255, 0, 0)", "The computed view also shows the color now"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter.js b/devtools/client/inspector/computed/test/browser_computed_search-filter.js new file mode 100644 index 0000000000..a22cdb4038 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the search filter works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testToggleDefaultStyles(inspector, view); + await testAddTextInFilter(inspector, view); +}); + +async function testToggleDefaultStyles(inspector, computedView) { + info('checking "Browser styles" checkbox'); + const checkbox = computedView.includeBrowserStylesCheckbox; + const onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + await onRefreshed; +} + +async function testAddTextInFilter(inspector, computedView) { + info('setting filter text to "color"'); + const searchField = computedView.searchField; + const onRefreshed = inspector.once("computed-view-refreshed"); + const win = computedView.styleWindow; + + // First check to make sure that accel + F doesn't focus search if the + // container isn't focused + inspector.panelWin.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + isnot( + inspector.panelDoc.activeElement, + searchField, + "Search field isn't focused" + ); + + computedView.element.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + is(inspector.panelDoc.activeElement, searchField, "Search field is focused"); + + synthesizeKeys("color", win); + await onRefreshed; + + info("check that the correct properties are visible"); + + const propertyViews = computedView.propertyViews; + propertyViews.forEach(propView => { + const name = propView.name; + is( + propView.visible, + name.indexOf("color") > -1, + "span " + name + " property visibility check" + ); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js new file mode 100644 index 0000000000..f77a71cc18 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the search filter clear button works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testAddTextInFilter(inspector, view); + await testClearSearchFilter(inspector, view); +}); + +async function testAddTextInFilter(inspector, computedView) { + info('Setting filter text to "background-color"'); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + + searchField.focus(); + synthesizeKeys("background-color", win); + await inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach(propView => { + const name = propView.name; + is( + propView.visible, + name.indexOf("background-color") > -1, + "span " + name + " property visibility check" + ); + }); +} + +async function testClearSearchFilter(inspector, computedView) { + info("Clearing the search filter"); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const searchClearButton = computedView.searchClearButton; + const onRefreshed = inspector.once("computed-view-refreshed"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + await onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach(propView => { + is( + propView.visible, + propView.hasMatchedSelectors, + "span " + propView.name + " property visibility check" + ); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js new file mode 100644 index 0000000000..0069d644c7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests computed view search filter context menu works properly. + +const TEST_INPUT = "h1"; + +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { toolbox, inspector, view } = await openComputedView(); + await selectNode("h1", inspector); + + const searchField = view.searchField; + + info("Opening context menu"); + + emptyClipboard(); + + const onFocus = once(searchField, "focus"); + searchField.focus(); + await onFocus; + + let onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + let searchContextMenu = toolbox.getTextBoxContextMenu(); + ok( + searchContextMenu, + "The search filter context menu is loaded in the computed view" + ); + + let cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + let cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + let cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + let cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + let cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + let cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled"); + is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled"); + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuClose = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuClose; + + info("Copy text in search field using the context menu"); + searchField.setUserInput(TEST_INPUT); + searchField.select(); + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + onContextMenuClose = toolbox.once("menu-close"); + await waitForClipboardPromise( + () => searchContextMenu.activateItem(cmdCopy), + TEST_INPUT + ); + await onContextMenuClose; + + info("Reopen context menu and check command properties"); + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); + + onContextMenuClose = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuClose; +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js new file mode 100644 index 0000000000..59c71d01fe --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Avoid test timeouts on Linux debug builds where the test takes just a bit too long to +// run (see bug 1258081). +requestLongerTimeout(2); + +// Tests that search filter escape keypress will clear the search field. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testAddTextInFilter(inspector, view); + await testEscapeKeypress(inspector, view); +}); + +async function testAddTextInFilter(inspector, computedView) { + info('Setting filter text to "background-color"'); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const checkbox = computedView.includeBrowserStylesCheckbox; + + info("Include browser styles"); + checkbox.click(); + await inspector.once("computed-view-refreshed"); + + searchField.focus(); + synthesizeKeys("background-color", win); + await inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach(propView => { + const name = propView.name; + is( + propView.visible, + name.indexOf("background-color") > -1, + "span " + name + " property visibility check" + ); + }); +} + +async function testEscapeKeypress(inspector, computedView) { + info("Pressing the escape key on search filter"); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const onRefreshed = inspector.once("computed-view-refreshed"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + await onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach(propView => { + const name = propView.name; + is(propView.visible, true, "span " + name + " property is visible"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js new file mode 100644 index 0000000000..b24722e237 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the "no-results" message is displayed when selecting an invalid element or +// when all properties have been filtered out. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <div> + <!-- comment node --> + <span id="matches" class="matches">Some styled text</span> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + const propertyViews = view.propertyViews; + + info("Select the #matches node"); + const matchesNode = await getNodeFront("#matches", inspector); + let onRefresh = inspector.once("computed-view-refreshed"); + await selectNode(matchesNode, inspector); + await onRefresh; + + ok( + !!propertyViews.filter(p => p.visible).length, + "CSS properties are displayed" + ); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Select a comment node"); + const commentNode = await inspector.walker.previousSibling(matchesNode); + await selectNode(commentNode, inspector); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); + + info("Select the #matches node again"); + onRefresh = inspector.once("computed-view-refreshed"); + await selectNode(matchesNode, inspector); + await onRefresh; + + ok( + !!propertyViews.filter(p => p.visible).length, + "CSS properties are displayed" + ); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info( + "Filter by 'will-not-match' and check the no-results message is displayed" + ); + const searchField = view.searchField; + searchField.focus(); + synthesizeKeys("will-not-match", view.styleWindow); + await inspector.once("computed-view-refreshed"); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js new file mode 100644 index 0000000000..4c75d2885e --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that properties can be selected and copied from the computed view. + +const TEST_URI = ` + <style type="text/css"> + span { + font-variant-caps: small-caps; + color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + await testCopySome(view); + await testCopyAll(view); +}); + +async function testCopySome(view) { + const expectedPattern = + "font-family: helvetica, sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + await copySomeTextAndCheckClipboard( + view, + { + start: { prop: 1, offset: 0 }, + end: { prop: 3, offset: 3 }, + }, + expectedPattern + ); +} + +async function testCopyAll(view) { + const expectedPattern = + "color: rgb\\(255, 255, 0\\);[\\r\\n]+" + + "font-family: helvetica, sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + await copyAllAndCheckClipboard(view, expectedPattern); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js new file mode 100644 index 0000000000..87fe1d9629 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that properties can be selected and copied from the computed view. + +const TEST_URI = `<div style="text-align:left;width:25px;">Hello world</div>`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + + let expectedPattern = "text-align: left;[\\r\\n]+" + "width: 25px;[\\r\\n]*"; + await copyAllAndCheckClipboard(view, expectedPattern); + + info("Testing expand then select all copy"); + + expectedPattern = + "text-align: left;[\\r\\n]+" + + "element[\\r\\n]+" + + "Best Match this.style[\\r\\n]+" + + "left[\\r\\n]+" + + "width: 25px;[\\r\\n]+" + + "element[\\r\\n]+" + + "Best Match this.style[\\r\\n]+" + + "25px[\\r\\n]*"; + + info("Expanding computed view properties"); + await expandComputedViewPropertyByIndex(view, 0); + await expandComputedViewPropertyByIndex(view, 1); + + await copyAllAndCheckClipboard(view, expectedPattern); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_shadow_host.js b/devtools/client/inspector/computed/test/browser_computed_shadow_host.js new file mode 100644 index 0000000000..19562eb1ed --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_shadow_host.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + PropertyView, +} = require("resource://devtools/client/inspector/computed/computed.js"); + +// Test matched selectors for a :host selector in the computed view. + +const SHADOW_DOM = `<style> + :host { + color: red; + } + + .test-span { + color: blue; + } +</style> +<span class="test-span">test</span>`; + +const TEST_PAGE = ` + <div id="host"></div> + <script> + const div = document.querySelector("div"); + div.attachShadow({ mode: "open" }).innerHTML = \`${SHADOW_DOM}\`; + </script>`; + +const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent( + TEST_PAGE +)}`; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + { + await selectNode("#host", inspector); + const propertyView = await getPropertyViewWithSelectors(view, "color"); + const selectors = propertyView.matchedSelectors.map(s => s.selector); + Assert.deepEqual( + selectors, + [":host", ":root"], + "host has the expected selectors for color" + ); + } + + { + const nodeFront = await getNodeFrontInShadowDom( + ".test-span", + "#host", + inspector + ); + await selectNode(nodeFront, inspector); + const propertyView = await getPropertyViewWithSelectors(view, "color"); + const selectors = propertyView.matchedSelectors.map(s => s.selector); + Assert.deepEqual( + selectors, + [".test-span", ":host", ":root"], + "shadow host child has the expected selectors for color" + ); + } +}); + +async function getPropertyViewWithSelectors(view, property) { + const propertyView = new PropertyView(view, property); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + await propertyView.refreshMatchedSelectors(); + + return propertyView; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js new file mode 100644 index 0000000000..832c1658a5 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the links from the computed view to the style editor. + +const STYLESHEET_URL = + "data:text/css," + encodeURIComponent(".highlight {color: blue}"); + +const DOCUMENT_URL = + "data:text/html;charset=utf-8," + + encodeURIComponent( + `<html> + <head> + <title>Computed view style editor link test +
+ + + + + + +

Some header text

+

hi.

+

I am a test-case. This text exists + solely to provide some things to + + highlight and count + 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. + some text

+

more text

+

even more text

+
+ + ` + ); + +add_task(async function () { + await addTab(DOCUMENT_URL); + const { toolbox, inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + await testInlineStyle(view); + await testFirstInlineStyleSheet(view, toolbox); + await testSecondInlineStyleSheet(view, toolbox); + await testExternalStyleSheet(view, toolbox); + await testConstructedStyleSheet(view, toolbox); +}); + +async function testInlineStyle(view) { + info("Testing inline style"); + + await expandComputedViewPropertyByIndex(view, 0); + + const onTab = waitForTab(); + info("Clicking on the first rule-link in the computed-view"); + checkComputedViewLink(view, { + index: 0, + expectedText: "element", + expectedTitle: "element", + }); + + const tab = await onTab; + + const tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +async function testFirstInlineStyleSheet(view, toolbox) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + const onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + checkComputedViewLink(view, { + index: 3, + expectedText: "inline:3", + expectedTitle: "inline:3", + }); + const editor = await onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + await validateStyleEditorSheet(editor, 0); +} + +async function testSecondInlineStyleSheet(view, toolbox) { + info("Testing second inline stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + checkComputedViewLink(view, { + index: 5, + expectedText: "inline:2", + expectedTitle: "inline:2", + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + await validateStyleEditorSheet(editor, 1); +} + +async function testExternalStyleSheet(view, toolbox) { + info("Testing external stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + checkComputedViewLink(view, { + index: 2, + expectedText: `${STYLESHEET_URL.replace("data:text/css,", "")}:1`, + expectedTitle: `${STYLESHEET_URL}:1`, + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + await validateStyleEditorSheet(editor, 2); +} + +async function testConstructedStyleSheet(view, toolbox) { + info("Testing constructed stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on an constructed stylesheet link"); + + checkComputedViewLink(view, { + index: 1, + expectedText: "constructed", + expectedTitle: "constructed", + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + ok(editor.styleSheet.constructed, "The constructed stylesheet is selected"); +} + +async function validateStyleEditorSheet(editor, expectedSheetIndex) { + info("Validating style editor stylesheet"); + const expectedHref = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [expectedSheetIndex], + _expectedSheetIndex => + content.document.styleSheets[_expectedSheetIndex].href + ); + is( + editor.styleSheet.href, + expectedHref, + "loaded stylesheet matches document stylesheet" + ); +} + +function checkComputedViewLink(view, { index, expectedText, expectedTitle }) { + const link = getComputedViewLinkByIndex(view, index); + is(link.innerText, expectedText, `Link #${index} has expected label`); + is( + link.getAttribute("title"), + expectedTitle, + `Link #${index} has expected title attribute` + ); + link.scrollIntoView(); + link.click(); +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors.html b/devtools/client/inspector/computed/test/doc_matched_selectors.html new file mode 100644 index 0000000000..41abe48826 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors.html @@ -0,0 +1,54 @@ + + + + + + + + + inspectstyle($("test")); +
Test div
+
+
+
+
+ + diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css new file mode 100644 index 0000000000..3eca2e8086 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedFirst-first); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-importedFirst-first-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css new file mode 100644 index 0000000000..035d9e0ff5 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedSecond); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-importedSecond-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css new file mode 100644 index 0000000000..1139d7e107 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css @@ -0,0 +1,8 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedFirst-second); +} + +#all-important-imported-layers { + --all-important-imported-layers_in-importedFirst-second-important: var(--winning-color); + background-color: var(--all-important-imported-layers_in-importedFirst-second-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css new file mode 100644 index 0000000000..abee8206b6 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-anonymous-first); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-anonymous-first-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css new file mode 100644 index 0000000000..26fb567293 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css @@ -0,0 +1,10 @@ +@import url(./doc_matched_selectors_imported_6.css) layer(importedSecond); + +#imported-layers { + --imported-layers_in-anonymous-second: var(--winning-color); + background-color: var(--imported-layers_in-anonymous-second); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-anonymous-second-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css new file mode 100644 index 0000000000..63b1cf0dc9 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-nested-importedSecond); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-nested-importedSecond-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_media_queries.html b/devtools/client/inspector/computed/test/doc_media_queries.html new file mode 100644 index 0000000000..819e1ea7aa --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_media_queries.html @@ -0,0 +1,21 @@ + + + test + + + +
+ + diff --git a/devtools/client/inspector/computed/test/doc_pseudoelement.html b/devtools/client/inspector/computed/test/doc_pseudoelement.html new file mode 100644 index 0000000000..6145d4bf1b --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_pseudoelement.html @@ -0,0 +1,131 @@ + + + + + + +

ruleview pseudoelement($("test"));

+ +
+

Top Left
Position

+
+ +
+

Top Right
Position

+
+ +
+

Bottom Right
Position

+
+ +
+

Bottom Left
Position

+
+ + + diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css b/devtools/client/inspector/computed/test/doc_sourcemaps.css new file mode 100644 index 0000000000..f62fbda21e --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */ diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css.map b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map new file mode 100644 index 0000000000..0f7486fd91 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["doc_sourcemaps.scss"], +"names": [], +"file": "doc_sourcemaps.css" +} diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.html b/devtools/client/inspector/computed/test/doc_sourcemaps.html new file mode 100644 index 0000000000..0014e55fe9 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.html @@ -0,0 +1,11 @@ + + + + testcase for testing CSS source maps + + + + +
source maps testcase
+ + diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.scss b/devtools/client/inspector/computed/test/doc_sourcemaps.scss new file mode 100644 index 0000000000..0ff6c471bb --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +} \ No newline at end of file diff --git a/devtools/client/inspector/computed/test/head.js b/devtools/client/inspector/computed/test/head.js new file mode 100644 index 0000000000..dfa1f87e9c --- /dev/null +++ b/devtools/client/inspector/computed/test/head.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * Dispatch the copy event on the given element + */ +function fireCopyEvent(element) { + const evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * Return all the computed items in the computed view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @returns {Array} + */ +function getComputedViewProperties(view) { + return Array.from( + view.styleDocument.querySelectorAll( + "#computed-container .computed-property-view" + ) + ); +} + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (const property of getComputedViewProperties(view)) { + const nameSpan = property.querySelector(".computed-property-name"); + const valueSpan = property.querySelector(".computed-property-value"); + + if (nameSpan.firstChild.textContent === name) { + prop = { nameSpan, valueSpan }; + break; + } + } + return prop; +} + +/** + * Get an instance of PropertyView from the computed-view. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {PropertyView} + */ +function getComputedViewPropertyView(view, name) { + let propView; + for (const propertyView of view.propertyViews) { + if (propertyView.propertyInfo.name === name) { + propView = propertyView; + break; + } + } + return propView; +} + +/** + * Get a reference to the matched rules element for a given property name in + * the computed-view. + * A matched rule element is inside the property element (
  • ) itself + * and is only shown when the twisty icon is expanded on the property. + * It contains matched rules, with selectors, properties, values and stylesheet links. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {Promise} A promise that resolves to the property matched rules + * container + */ +var getComputedViewMatchedRules = async function (view, name) { + let expander; + let matchedRulesEl; + for (const property of view.styleDocument.querySelectorAll( + "#computed-container .computed-property-view" + )) { + const nameSpan = property.querySelector(".computed-property-name"); + if (nameSpan.firstChild.textContent === name) { + expander = property.querySelector(".computed-expandable"); + matchedRulesEl = property.querySelector(".matchedselectors"); + + break; + } + } + + if (!expander.hasAttribute("open")) { + // Need to expand the property + const onExpand = view.inspector.once("computed-view-property-expanded"); + expander.click(); + await onExpand; + + await waitFor(() => expander.hasAttribute("open")); + } + + return matchedRulesEl; +}; + +/** + * Get the text value of the property corresponding to a given name in the + * computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name) { + return getComputedViewProperty(view, name).valueSpan.textContent; +} + +/** + * Expand a given property, given its index in the current property list of + * the computed view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Number} index + * The index of the property to be expanded + * @return a promise that resolves when the property has been expanded, or + * rejects if the property was not found + */ +function expandComputedViewPropertyByIndex(view, index) { + info("Expanding property " + index + " in the computed view"); + const expandos = view.styleDocument.querySelectorAll(".computed-expandable"); + if (!expandos.length || !expandos[index]) { + return Promise.reject(); + } + + const onExpand = view.inspector.once("computed-view-property-expanded"); + expandos[index].click(); + return onExpand; +} + +/** + * Get a rule-link from the computed-view given its index + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Number} index + * The index of the link to be retrieved + * @return {DOMNode} The link at the given index, if one exists, null otherwise + */ +function getComputedViewLinkByIndex(view, index) { + const links = view.styleDocument.querySelectorAll( + ".rule-link .computed-link" + ); + return links[index]; +} + +/** + * Trigger the select all action in the computed view. + * + * @param {CssComputedView} view + * The instance of the computed view panel + */ +function selectAllText(view) { + info("Selecting all the text"); + view.contextMenu._onSelectAll(); +} + +/** + * Select all the text, copy it, and check the content in the clipboard. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} expectedPattern + * A regular expression used to check the content of the clipboard + */ +async function copyAllAndCheckClipboard(view, expectedPattern) { + selectAllText(view); + const contentDoc = view.styleDocument; + const prop = contentDoc.querySelector( + "#computed-container .computed-property-view" + ); + + try { + info("Trigger a copy event and wait for the clipboard content"); + await waitForClipboardPromise( + () => fireCopyEvent(prop), + () => checkClipboard(expectedPattern) + ); + } catch (e) { + failClipboardCheck(expectedPattern); + } +} + +/** + * Select some text, copy it, and check the content in the clipboard. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Object} positions + * The start and end positions of the text to be selected. This must be an object + * like this: + * { start: {prop: 1, offset: 0}, end: {prop: 3, offset: 5} } + * @param {String} expectedPattern + * A regular expression used to check the content of the clipboard + */ +async function copySomeTextAndCheckClipboard(view, positions, expectedPattern) { + info("Testing selection copy"); + + const contentDocument = view.styleDocument; + const props = contentDocument.querySelectorAll( + "#computed-container .computed-property-view" + ); + + info("Create the text selection range"); + const range = contentDocument.createRange(); + range.setStart(props[positions.start.prop], positions.start.offset); + range.setEnd(props[positions.end.prop], positions.end.offset); + contentDocument.defaultView.getSelection().addRange(range); + + try { + info("Trigger a copy event and wait for the clipboard content"); + await waitForClipboardPromise( + () => fireCopyEvent(props[0]), + () => checkClipboard(expectedPattern) + ); + } catch (e) { + failClipboardCheck(expectedPattern); + } +} + +function checkClipboard(expectedPattern) { + const actual = SpecialPowers.getClipboardData("text/plain"); + const expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failClipboardCheck(expectedPattern) { + // Format expected text for comparison + const terminator = Services.appinfo.OS == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/plain"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + dump( + "TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " + + "results (escaped for accurate comparison):\n" + ); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/configs/development.json b/devtools/client/inspector/configs/development.json new file mode 100644 index 0000000000..57cba7c994 --- /dev/null +++ b/devtools/client/inspector/configs/development.json @@ -0,0 +1,21 @@ +{ + "title": "Inspector", + "environment": "development", + "baseWorkerURL": "public/build/", + "theme": "light", + "host": "", + "logging": { + "client": false, + "firefoxProxy": false + }, + "features": {}, + "firefox": { + "proxyHost": "localhost:9000", + "webSocketConnection": false, + "webSocketHost": "localhost:6080" + }, + "development": { + "serverPort": 8000, + "customIndex": true + } +} diff --git a/devtools/client/inspector/extensions/actions/index.js b/devtools/client/inspector/extensions/actions/index.js new file mode 100644 index 0000000000..384dfb0da7 --- /dev/null +++ b/devtools/client/inspector/extensions/actions/index.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Update the extension sidebar with an object TreeView. + "EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE", + + // Update the extension sidebar with an expression result. + "EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE", + + // Switch the extension sidebar into an extension page container. + "EXTENSION_SIDEBAR_PAGE_UPDATE", + + // Remove an extension sidebar from the inspector store. + "EXTENSION_SIDEBAR_REMOVE", + ], + module.exports +); diff --git a/devtools/client/inspector/extensions/actions/moz.build b/devtools/client/inspector/extensions/actions/moz.build new file mode 100644 index 0000000000..d101bc2fea --- /dev/null +++ b/devtools/client/inspector/extensions/actions/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "index.js", + "sidebar.js", +) diff --git a/devtools/client/inspector/extensions/actions/sidebar.js b/devtools/client/inspector/extensions/actions/sidebar.js new file mode 100644 index 0000000000..34c7276c27 --- /dev/null +++ b/devtools/client/inspector/extensions/actions/sidebar.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE, + EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE, + EXTENSION_SIDEBAR_PAGE_UPDATE, + EXTENSION_SIDEBAR_REMOVE, +} = require("resource://devtools/client/inspector/extensions/actions/index.js"); + +module.exports = { + /** + * Update the sidebar with an object treeview. + */ + updateObjectTreeView(sidebarId, object) { + return { + type: EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE, + sidebarId, + object, + }; + }, + + /** + * Update the sidebar with an expression result. + */ + updateExpressionResultView(sidebarId, expressionResult, rootTitle) { + return { + type: EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE, + sidebarId, + expressionResult, + rootTitle, + }; + }, + + /** + * Switch the sidebar into the extension page mode. + */ + updateExtensionPage(sidebarId, iframeURL) { + return { + type: EXTENSION_SIDEBAR_PAGE_UPDATE, + sidebarId, + iframeURL, + }; + }, + + /** + * Remove the extension sidebar from the inspector store. + */ + removeExtensionSidebar(sidebarId) { + return { + type: EXTENSION_SIDEBAR_REMOVE, + sidebarId, + }; + }, +}; diff --git a/devtools/client/inspector/extensions/components/ExpressionResultView.js b/devtools/client/inspector/extensions/components/ExpressionResultView.js new file mode 100644 index 0000000000..887c5610c4 --- /dev/null +++ b/devtools/client/inspector/extensions/components/ExpressionResultView.js @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Accordion = createFactory( + require("resource://devtools/client/shared/components/Accordion.js") +); + +const Types = require("resource://devtools/client/inspector/extensions/types.js"); + +const { + REPS: { Grip }, + MODE, + objectInspector: { ObjectInspector: ObjectInspectorClass }, +} = require("resource://devtools/client/shared/components/reps/index.js"); + +loader.lazyRequireGetter( + this, + "LongStringFront", + "resource://devtools/client/fronts/string.js", + true +); + +loader.lazyRequireGetter( + this, + "ObjectFront", + "resource://devtools/client/fronts/object.js", + true +); + +const ObjectInspector = createFactory(ObjectInspectorClass); + +class ObjectValueGripView extends PureComponent { + static get propTypes() { + return { + rootTitle: PropTypes.string, + expressionResult: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.object, + ]).isRequired, + // Helpers injected as props by extension-sidebar.js. + serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired, + }; + } + + render() { + const { expressionResult, serviceContainer, rootTitle } = this.props; + + const isFront = + expressionResult instanceof ObjectFront || + expressionResult instanceof LongStringFront; + const grip = isFront ? expressionResult.getGrip() : expressionResult; + + const objectInspectorProps = { + autoExpandDepth: 1, + mode: MODE.SHORT, + // TODO: we disable focus since it's not currently working well in ObjectInspector. + // Let's remove the property below when problem are fixed in OI. + disabledFocus: true, + roots: [ + { + path: expressionResult?.actorID || JSON.stringify(expressionResult), + contents: { value: grip, front: isFront ? expressionResult : null }, + }, + ], + // TODO: evaluate if there should also be a serviceContainer.openLink. + }; + + if (expressionResult?.actorID) { + Object.assign(objectInspectorProps, { + onDOMNodeMouseOver: serviceContainer.highlightDomElement, + onDOMNodeMouseOut: serviceContainer.unHighlightDomElement, + onInspectIconClick(object, e) { + // Stop the event propagation so we don't trigger ObjectInspector + // expand/collapse. + e.stopPropagation(); + serviceContainer.openNodeInInspector(object); + }, + defaultRep: Grip, + }); + } + + if (rootTitle) { + return Accordion({ + items: [ + { + component: ObjectInspector, + componentProps: objectInspectorProps, + header: rootTitle, + id: rootTitle.replace(/\s/g, "-"), + opened: true, + }, + ], + }); + } + + return ObjectInspector(objectInspectorProps); + } +} + +module.exports = ObjectValueGripView; diff --git a/devtools/client/inspector/extensions/components/ExtensionPage.js b/devtools/client/inspector/extensions/components/ExtensionPage.js new file mode 100644 index 0000000000..3f92b8f41f --- /dev/null +++ b/devtools/client/inspector/extensions/components/ExtensionPage.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +/** + * The ExtensionPage React Component is used in the ExtensionSidebar component to provide + * a UI viewMode which shows an extension page rendered inside the sidebar panel. + */ +class ExtensionPage extends PureComponent { + static get propTypes() { + return { + iframeURL: PropTypes.string.isRequired, + onExtensionPageMount: PropTypes.func.isRequired, + onExtensionPageUnmount: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.iframeRef = createRef(); + } + + componentDidMount() { + this.props.onExtensionPageMount(this.iframeRef.current); + } + + componentWillUnmount() { + this.props.onExtensionPageUnmount(this.iframeRef.current); + } + + render() { + return dom.iframe({ + className: "inspector-extension-sidebar-page", + src: this.props.iframeURL, + style: { + width: "100%", + height: "100%", + margin: 0, + padding: 0, + }, + ref: this.iframeRef, + }); + } +} + +module.exports = ExtensionPage; diff --git a/devtools/client/inspector/extensions/components/ExtensionSidebar.js b/devtools/client/inspector/extensions/components/ExtensionSidebar.js new file mode 100644 index 0000000000..a351b6add9 --- /dev/null +++ b/devtools/client/inspector/extensions/components/ExtensionSidebar.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const ExtensionPage = createFactory( + require("resource://devtools/client/inspector/extensions/components/ExtensionPage.js") +); +const ObjectTreeView = createFactory( + require("resource://devtools/client/inspector/extensions/components/ObjectTreeView.js") +); +const ExpressionResultView = createFactory( + require("resource://devtools/client/inspector/extensions/components/ExpressionResultView.js") +); +const Types = require("resource://devtools/client/inspector/extensions/types.js"); + +/** + * The ExtensionSidebar is a React component with 2 supported viewMode: + * - an ObjectTreeView UI, used to show the JS objects + * (used by the sidebar.setObject WebExtensions APIs) + * - an ExpressionResultView UI, used to show the result for an expression + * (used by sidebar.setExpression WebExtensions APIs) + * - an ExtensionPage UI used to show an extension page + * (used by the sidebar.setPage WebExtensions APIs). + * + * TODO: implement the ExtensionPage viewMode. + */ +class ExtensionSidebar extends PureComponent { + static get propTypes() { + return { + id: PropTypes.string.isRequired, + extensionsSidebar: PropTypes.object.isRequired, + onExtensionPageMount: PropTypes.func.isRequired, + onExtensionPageUnmount: PropTypes.func.isRequired, + // Helpers injected as props by extension-sidebar.js. + serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired, + }; + } + + render() { + const { + id, + extensionsSidebar, + onExtensionPageMount, + onExtensionPageUnmount, + serviceContainer, + } = this.props; + + const { + iframeURL, + object, + expressionResult, + rootTitle, + viewMode = "empty-sidebar", + } = extensionsSidebar[id] || {}; + + let sidebarContentEl; + + switch (viewMode) { + case "object-treeview": + sidebarContentEl = ObjectTreeView({ object }); + break; + case "object-value-grip-view": + sidebarContentEl = ExpressionResultView({ + expressionResult, + rootTitle, + serviceContainer, + }); + break; + case "extension-page": + sidebarContentEl = ExtensionPage({ + iframeURL, + onExtensionPageMount, + onExtensionPageUnmount, + }); + break; + case "empty-sidebar": + break; + default: + throw new Error(`Unknown ExtensionSidebar viewMode: "${viewMode}"`); + } + + const className = "devtools-monospace extension-sidebar inspector-tabpanel"; + + return dom.div( + { + id, + className, + }, + sidebarContentEl + ); + } +} + +module.exports = connect(state => state)(ExtensionSidebar); diff --git a/devtools/client/inspector/extensions/components/ObjectTreeView.js b/devtools/client/inspector/extensions/components/ObjectTreeView.js new file mode 100644 index 0000000000..1788f8d020 --- /dev/null +++ b/devtools/client/inspector/extensions/components/ObjectTreeView.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const { + REPS, + MODE, +} = require("resource://devtools/client/shared/components/reps/index.js"); +const { Rep } = REPS; +const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js"); +const TreeView = createFactory(TreeViewClass); + +/** + * The ExpressionResultView React Component is used in the ExtensionSidebar component to + * provide a UI viewMode which shows a tree view of the passed JavaScript object. + */ +class ExpressionResultView extends PureComponent { + static get propTypes() { + return { + object: PropTypes.object.isRequired, + }; + } + + render() { + const { object } = this.props; + + const columns = [ + { + id: "value", + }, + ]; + + // Render the node value (omitted on the root element if it has children). + const renderValue = props => { + if (props.member.level === 0 && props.member.hasChildren) { + return undefined; + } + + return Rep( + Object.assign({}, props, { + cropLimit: 50, + }) + ); + }; + + return TreeView({ + object, + mode: MODE.SHORT, + columns, + renderValue, + expandedNodes: TreeViewClass.getExpandedNodes(object, { + maxLevel: 1, + maxNodes: 1, + }), + }); + } +} + +module.exports = ExpressionResultView; diff --git a/devtools/client/inspector/extensions/components/moz.build b/devtools/client/inspector/extensions/components/moz.build new file mode 100644 index 0000000000..62f3d991c1 --- /dev/null +++ b/devtools/client/inspector/extensions/components/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "ExpressionResultView.js", + "ExtensionPage.js", + "ExtensionSidebar.js", + "ObjectTreeView.js", +) diff --git a/devtools/client/inspector/extensions/extension-sidebar.js b/devtools/client/inspector/extensions/extension-sidebar.js new file mode 100644 index 0000000000..c359d8b4fa --- /dev/null +++ b/devtools/client/inspector/extensions/extension-sidebar.js @@ -0,0 +1,189 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); +const { + Provider, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const extensionsSidebarReducer = require("resource://devtools/client/inspector/extensions/reducers/sidebar.js"); +const { + default: objectInspectorReducer, +} = require("resource://devtools/client/shared/components/object-inspector/reducer.js"); + +const ExtensionSidebarComponent = createFactory( + require("resource://devtools/client/inspector/extensions/components/ExtensionSidebar.js") +); + +const { + updateExtensionPage, + updateObjectTreeView, + updateExpressionResultView, + removeExtensionSidebar, +} = require("resource://devtools/client/inspector/extensions/actions/sidebar.js"); + +/** + * ExtensionSidebar instances represents Inspector sidebars installed by add-ons + * using the devtools.panels.elements.createSidebarPane WebExtensions API. + * + * The WebExtensions API registers the extensions' sidebars on the toolbox instance + * (using the registerInspectorExtensionSidebar method) and, once the Inspector has been + * created, the toolbox uses the Inpector createExtensionSidebar method to create the + * ExtensionSidebar instances and then it registers them to the Inspector. + * + * @param {Inspector} inspector + * The inspector where the sidebar should be hooked to. + * @param {Object} options + * @param {String} options.id + * The unique id of the sidebar. + * @param {String} options.title + * The title of the sidebar. + */ +class ExtensionSidebar { + constructor(inspector, { id, title }) { + EventEmitter.decorate(this); + this.inspector = inspector; + this.store = inspector.store; + this.id = id; + this.title = title; + this.destroyed = false; + + this.store.injectReducer("extensionsSidebar", extensionsSidebarReducer); + this.store.injectReducer("objectInspector", objectInspectorReducer); + } + + /** + * Lazily create a React ExtensionSidebarComponent wrapped into a Redux Provider. + */ + get provider() { + if (!this._provider) { + this._provider = createElement( + Provider, + { + store: this.store, + key: this.id, + title: this.title, + }, + ExtensionSidebarComponent({ + id: this.id, + onExtensionPageMount: containerEl => { + this.emit("extension-page-mount", containerEl); + }, + onExtensionPageUnmount: containerEl => { + this.emit("extension-page-unmount", containerEl); + }, + serviceContainer: { + highlightDomElement: async (grip, options = {}) => { + const nodeFront = + await this.inspector.inspectorFront.getNodeFrontFromNodeGrip( + grip + ); + return this.inspector.highlighters.showHighlighterTypeForNode( + this.inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }, + unHighlightDomElement: async () => { + return this.inspector.highlighters.hideHighlighterType( + this.inspector.highlighters.TYPES.BOXMODEL + ); + }, + openNodeInInspector: async grip => { + const nodeFront = + await this.inspector.inspectorFront.getNodeFrontFromNodeGrip( + grip + ); + const onInspectorUpdated = + this.inspector.once("inspector-updated"); + const onNodeFrontSet = + this.inspector.toolbox.selection.setNodeFront(nodeFront, { + reason: "inspector-extension-sidebar", + }); + + return Promise.all([onNodeFrontSet, onInspectorUpdated]); + }, + }, + }) + ); + } + + return this._provider; + } + + /** + * Destroy the ExtensionSidebar instance, dispatch a removeExtensionSidebar Redux action + * (which removes the related state from the Inspector store) and clear any reference + * to the inspector, the Redux store and the lazily created Redux Provider component. + * + * This method is called by the inspector when the ExtensionSidebar is being removed + * (or when the inspector is being destroyed). + */ + destroy() { + if (this.destroyed) { + throw new Error( + `ExtensionSidebar instances cannot be destroyed more than once` + ); + } + + // Remove the data related to this extension from the inspector store. + this.store.dispatch(removeExtensionSidebar(this.id)); + + this.inspector = null; + this.store = null; + this._provider = null; + + this.destroyed = true; + } + + /** + * Dispatch an objectTreeView action to change the SidebarComponent into an + * ObjectTreeView React Component, which shows the passed javascript object + * in the sidebar. + */ + setObject(object) { + if (this.removed) { + throw new Error( + "Unable to set an object preview on a removed ExtensionSidebar" + ); + } + + this.store.dispatch(updateObjectTreeView(this.id, object)); + } + + /** + * Dispatch an objectPreview action to change the SidebarComponent into an + * ObjectPreview React Component, which shows the passed value grip + * in the sidebar. + */ + setExpressionResult(expressionResult, rootTitle) { + if (this.removed) { + throw new Error( + "Unable to set an object preview on a removed ExtensionSidebar" + ); + } + + this.store.dispatch( + updateExpressionResultView(this.id, expressionResult, rootTitle) + ); + } + + setExtensionPage(iframeURL) { + if (this.removed) { + throw new Error( + "Unable to set an object preview on a removed ExtensionSidebar" + ); + } + + this.store.dispatch(updateExtensionPage(this.id, iframeURL)); + } +} + +module.exports = ExtensionSidebar; diff --git a/devtools/client/inspector/extensions/moz.build b/devtools/client/inspector/extensions/moz.build new file mode 100644 index 0000000000..39647c8e5a --- /dev/null +++ b/devtools/client/inspector/extensions/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "actions", + "components", + "reducers", +] + +DevToolsModules( + "extension-sidebar.js", + "types.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/devtools/client/inspector/extensions/reducers/moz.build b/devtools/client/inspector/extensions/reducers/moz.build new file mode 100644 index 0000000000..0f8a5757c8 --- /dev/null +++ b/devtools/client/inspector/extensions/reducers/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "sidebar.js", +) diff --git a/devtools/client/inspector/extensions/reducers/sidebar.js b/devtools/client/inspector/extensions/reducers/sidebar.js new file mode 100644 index 0000000000..c7566f3b67 --- /dev/null +++ b/devtools/client/inspector/extensions/reducers/sidebar.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE, + EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE, + EXTENSION_SIDEBAR_PAGE_UPDATE, + EXTENSION_SIDEBAR_REMOVE, +} = require("resource://devtools/client/inspector/extensions/actions/index.js"); + +const INITIAL_SIDEBAR = {}; + +const reducers = { + [EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE](sidebar, { sidebarId, object }) { + // Update the sidebar to a "object-treeview" which shows + // the passed object. + return Object.assign({}, sidebar, { + [sidebarId]: { + viewMode: "object-treeview", + object, + }, + }); + }, + + [EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE]( + sidebar, + { sidebarId, expressionResult, rootTitle } + ) { + // Update the sidebar to a "object-treeview" which shows + // the passed object. + return Object.assign({}, sidebar, { + [sidebarId]: { + viewMode: "object-value-grip-view", + expressionResult, + rootTitle, + }, + }); + }, + + [EXTENSION_SIDEBAR_PAGE_UPDATE](sidebar, { sidebarId, iframeURL }) { + // Update the sidebar to a "object-treeview" which shows + // the passed object. + return Object.assign({}, sidebar, { + [sidebarId]: { + viewMode: "extension-page", + iframeURL, + }, + }); + }, + + [EXTENSION_SIDEBAR_REMOVE](sidebar, { sidebarId }) { + // Remove the sidebar from the Redux store. + delete sidebar[sidebarId]; + return Object.assign({}, sidebar); + }, +}; + +module.exports = function (sidebar = INITIAL_SIDEBAR, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return sidebar; + } + return reducer(sidebar, action); +}; diff --git a/devtools/client/inspector/extensions/test/browser.toml b/devtools/client/inspector/extensions/test/browser.toml new file mode 100644 index 0000000000..2c6ad4538c --- /dev/null +++ b/devtools/client/inspector/extensions/test/browser.toml @@ -0,0 +1,15 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "head_devtools_inspector_sidebar.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_inspector_extension_sidebar.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled diff --git a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js new file mode 100644 index 0000000000..740b1fda13 --- /dev/null +++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js @@ -0,0 +1,451 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SIDEBAR_ID = "an-extension-sidebar"; +const SIDEBAR_TITLE = "Sidebar Title"; + +let extension; +let fakeExtCallerInfo; + +let toolbox; +let inspector; + +add_task(async function setupExtensionSidebar() { + extension = ExtensionTestUtils.loadExtension({ + background() { + // This is just an empty extension used to ensure that the caller extension uuid + // actually exists. + }, + }); + + await extension.startup(); + + fakeExtCallerInfo = { + url: WebExtensionPolicy.getByID(extension.id).getURL( + "fake-caller-script.js" + ), + lineNumber: 1, + addonId: extension.id, + }; + + const res = await openInspectorForURL("about:blank"); + inspector = res.inspector; + toolbox = res.toolbox; + + const onceSidebarCreated = toolbox.once( + `extension-sidebar-created-${SIDEBAR_ID}` + ); + toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, { + title: SIDEBAR_TITLE, + }); + + const sidebar = await onceSidebarCreated; + + // Test sidebar properties. + is( + sidebar, + inspector.getPanel(SIDEBAR_ID), + "Got an extension sidebar instance equal to the one saved in the inspector" + ); + is( + sidebar.title, + SIDEBAR_TITLE, + "Got the expected title in the extension sidebar instance" + ); + is( + sidebar.provider.props.title, + SIDEBAR_TITLE, + "Got the expeted title in the provider props" + ); + + // Test sidebar Redux state. + const inspectorStoreState = inspector.store.getState(); + ok( + "extensionsSidebar" in inspectorStoreState, + "Got the extensionsSidebar sub-state in the inspector Redux store" + ); + Assert.deepEqual( + inspectorStoreState.extensionsSidebar, + {}, + "The extensionsSidebar should be initially empty" + ); +}); + +add_task(async function testSidebarSetObject() { + const object = { + propertyName: { + nestedProperty: "propertyValue", + anotherProperty: "anotherValue", + }, + }; + + const sidebar = inspector.getPanel(SIDEBAR_ID); + sidebar.setObject(object); + + // Test updated sidebar Redux state. + const inspectorStoreState = inspector.store.getState(); + is( + Object.keys(inspectorStoreState.extensionsSidebar).length, + 1, + "The extensionsSidebar state contains the newly registered extension sidebar state" + ); + Assert.deepEqual( + inspectorStoreState.extensionsSidebar, + { + [SIDEBAR_ID]: { + viewMode: "object-treeview", + object, + }, + }, + "Got the expected state for the registered extension sidebar" + ); + + // Select the extension sidebar. + const waitSidebarSelected = toolbox.once(`inspector-sidebar-select`); + inspector.sidebar.show(SIDEBAR_ID); + await waitSidebarSelected; + + const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); + + // Test extension sidebar content. + ok( + sidebarPanelContent, + "Got a sidebar panel for the registered extension sidebar" + ); + + assertTreeView(sidebarPanelContent, { + expectedTreeTables: 1, + expectedStringCells: 2, + expectedNumberCells: 0, + }); + + // Test sidebar refreshed on further sidebar.setObject calls. + info("Change the inspected object in the extension sidebar object treeview"); + sidebar.setObject({ aNewProperty: 123 }); + + assertTreeView(sidebarPanelContent, { + expectedTreeTables: 1, + expectedStringCells: 0, + expectedNumberCells: 1, + }); +}); + +add_task(async function testSidebarSetExpressionResult() { + const { commands } = toolbox; + const sidebar = inspector.getPanel(SIDEBAR_ID); + const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); + + info("Testing sidebar.setExpressionResult with rootTitle"); + + const expression = ` + var obj = Object.create(null); + obj.prop1 = 123; + obj[Symbol('sym1')] = 456; + obj.cyclic = obj; + obj; + `; + + const consoleFront = await toolbox.target.getFront("console"); + let evalResult = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + expression, + { + consoleFront, + } + ); + + sidebar.setExpressionResult(evalResult, "Expected Root Title"); + + // Wait the ObjectInspector component to be rendered and test its content. + await testSetExpressionSidebarPanel(sidebarPanelContent, { + nodesLength: 4, + propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"], + rootTitle: "Expected Root Title", + }); + + info("Testing sidebar.setExpressionResult without rootTitle"); + + sidebar.setExpressionResult(evalResult); + + // Wait the ObjectInspector component to be rendered and test its content. + await testSetExpressionSidebarPanel(sidebarPanelContent, { + nodesLength: 4, + propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"], + }); + + info("Test expanding the object"); + const oi = sidebarPanelContent.querySelector(".tree"); + const cyclicNode = oi.querySelectorAll(".node")[1]; + ok(cyclicNode.innerText.includes("cyclic"), "Found the expected node"); + cyclicNode.click(); + + await TestUtils.waitForCondition( + () => oi.querySelectorAll(".node").length === 7, + "Wait for the 'cyclic' node to be expanded" + ); + + await TestUtils.waitForCondition( + () => oi.querySelector(".tree-node.focused"), + "Wait for the 'cyclic' node to be focused" + ); + ok( + oi.querySelector(".tree-node.focused").innerText.includes("cyclic"), + "'cyclic' node is focused" + ); + + info("Test keyboard navigation"); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, oi.ownerDocument.defaultView); + await TestUtils.waitForCondition( + () => oi.querySelectorAll(".node").length === 4, + "Wait for the 'cyclic' node to be collapsed" + ); + ok( + oi.querySelector(".tree-node.focused").innerText.includes("cyclic"), + "'cyclic' node is still focused" + ); + + EventUtils.synthesizeKey("KEY_ArrowDown", {}, oi.ownerDocument.defaultView); + await TestUtils.waitForCondition( + () => oi.querySelectorAll(".tree-node")[2].classList.contains("focused"), + "Wait for the 'prop1' node to be focused" + ); + + ok( + oi.querySelector(".tree-node.focused").innerText.includes("prop1"), + "'prop1' node is focused" + ); + + info( + "Testing sidebar.setExpressionResult for an expression returning a longstring" + ); + evalResult = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + `"ab ".repeat(10000)`, + { + consoleFront, + } + ); + sidebar.setExpressionResult(evalResult); + + await TestUtils.waitForCondition(() => { + const longStringEl = sidebarPanelContent.querySelector( + ".tree .objectBox-string" + ); + return ( + longStringEl && longStringEl.textContent.includes("ab ".repeat(10000)) + ); + }, "Wait for the longString to be render with its full text"); + ok(true, "The longString is expanded and its full text is displayed"); + + info( + "Testing sidebar.setExpressionResult for an expression returning a primitive" + ); + evalResult = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + `1 + 2`, + { + consoleFront, + } + ); + sidebar.setExpressionResult(evalResult); + const numberEl = await TestUtils.waitForCondition( + () => sidebarPanelContent.querySelector(".objectBox-number"), + "Wait for the result number element to be rendered" + ); + is(numberEl.textContent, "3", `The "1 + 2" expression was evaluated as "3"`); +}); + +add_task(async function testSidebarDOMNodeHighlighting() { + const { commands } = toolbox; + const sidebar = inspector.getPanel(SIDEBAR_ID); + const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); + + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + const expression = "({ body: document.body })"; + + const consoleFront = await toolbox.target.getFront("console"); + const evalResult = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + expression, + { + consoleFront, + } + ); + + sidebar.setExpressionResult(evalResult); + + // Wait the DOM node to be rendered inside the component. + await waitForObjectInspector(sidebarPanelContent, "node"); + + // Wait for the object to be expanded so we only target the "body" property node, and + // not the root object element. + await TestUtils.waitForCondition( + () => + sidebarPanelContent.querySelectorAll(".object-inspector .tree-node") + .length > 1 + ); + + // Get and verify the DOMNode and the "open inspector"" icon + // rendered inside the ObjectInspector. + + assertObjectInspector(sidebarPanelContent, { + expectedDOMNodes: 2, + expectedOpenInspectors: 2, + }); + + // Test highlight DOMNode on mouseover. + info("Highlight the node by moving the cursor on it"); + + const onNodeHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + moveMouseOnObjectInspectorDOMNode(sidebarPanelContent); + + const { nodeFront } = await onNodeHighlight; + is(nodeFront.displayName, "body", "The correct node was highlighted"); + + // Test unhighlight DOMNode on mousemove. + info("Unhighlight the node by moving away from the node"); + const onNodeUnhighlight = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + + moveMouseOnPanelCenter(sidebarPanelContent); + + await onNodeUnhighlight; + info("The node is no longer highlighted"); +}); + +add_task(async function testSidebarDOMNodeOpenInspector() { + const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); + + // Test DOMNode selected in the inspector when "open inspector"" icon clicked. + info("Unselect node in the inspector"); + let onceNewNodeFront = inspector.selection.once("new-node-front"); + inspector.selection.setNodeFront(null); + let nodeFront = await onceNewNodeFront; + is(nodeFront, null, "The inspector selection should have been unselected"); + + info( + "Select the ObjectInspector DOMNode in the inspector panel by clicking on it" + ); + + // In test mode, shown highlighters are not automatically hidden after a delay to + // prevent intermittent test failures from race conditions. + // Restore this behavior just for this test because it is explicitly checked. + const HIGHLIGHTER_AUTOHIDE_TIMER = inspector.HIGHLIGHTER_AUTOHIDE_TIMER; + inspector.HIGHLIGHTER_AUTOHIDE_TIMER = 1000; + registerCleanupFunction(() => { + // Restore the value to disable autohiding to not impact other tests. + inspector.HIGHLIGHTER_AUTOHIDE_TIMER = HIGHLIGHTER_AUTOHIDE_TIMER; + }); + + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + // Once we click the open-inspector icon we expect a new node front to be selected + // and the node to have been highlighted and unhighlighted. + const onNodeHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + const onNodeUnhighlight = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + onceNewNodeFront = inspector.selection.once("new-node-front"); + + clickOpenInspectorIcon(sidebarPanelContent); + + nodeFront = await onceNewNodeFront; + is(nodeFront.displayName, "body", "The correct node has been selected"); + const { nodeFront: highlightedNodeFront } = await onNodeHighlight; + is( + highlightedNodeFront.displayName, + "body", + "The correct node was highlighted" + ); + + await onNodeUnhighlight; +}); + +add_task(async function testSidebarSetExtensionPage() { + const sidebar = inspector.getPanel(SIDEBAR_ID); + const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); + + info("Testing sidebar.setExtensionPage"); + + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + + const expectedURL = + "data:text/html,

    Extension Page"; + + sidebar.setExtensionPage(expectedURL); + + await testSetExtensionPageSidebarPanel(sidebarPanelContent, expectedURL); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function teardownExtensionSidebar() { + info("Remove the sidebar instance"); + + toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID); + + ok( + !inspector.sidebar.getTabPanel(SIDEBAR_ID), + "The rendered extension sidebar has been removed" + ); + + const inspectorStoreState = inspector.store.getState(); + + Assert.deepEqual( + inspectorStoreState.extensionsSidebar, + {}, + "The extensions sidebar Redux store data has been cleared" + ); + + await extension.unload(); + + toolbox = null; + inspector = null; + extension = null; +}); + +add_task(async function testActiveTabOnNonExistingSidebar() { + // Set a fake non existing sidebar id in the activeSidebar pref, + // to simulate the scenario where an extension has installed a sidebar + // which has been saved in the preference but it doesn't exist anymore. + await SpecialPowers.pushPrefEnv({ + set: [["devtools.inspector.activeSidebar", "unexisting-sidebar-id"]], + }); + + const res = await openInspectorForURL("about:blank"); + inspector = res.inspector; + toolbox = res.toolbox; + + const onceSidebarCreated = toolbox.once( + `extension-sidebar-created-${SIDEBAR_ID}` + ); + toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, { + title: SIDEBAR_TITLE, + }); + + // Wait the extension sidebar to be created and then unregister it to force the tabbar + // to select a new one. + await onceSidebarCreated; + toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID); + + is( + inspector.sidebar.getCurrentTabID(), + "layoutview", + "Got the expected inspector sidebar tab selected" + ); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/devtools/client/inspector/extensions/test/head.js b/devtools/client/inspector/extensions/test/head.js new file mode 100644 index 0000000000..17d7538904 --- /dev/null +++ b/devtools/client/inspector/extensions/test/head.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +// Import the inspector extensions test helpers (shared between the tests that live +// in the current devtools test directory and the devtools sidebar tests that live +// in browser/components/extensions/test/browser). +Services.scriptloader.loadSubScript( + new URL("head_devtools_inspector_sidebar.js", gTestPath).href, + this +); diff --git a/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js new file mode 100644 index 0000000000..73467c3e31 --- /dev/null +++ b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js @@ -0,0 +1,224 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* exported getExtensionSidebarActors, expectNoSuchActorIDs, + waitForObjectInspector, testSetExpressionSidebarPanel, assertTreeView, + assertObjectInspector, moveMouseOnObjectInspectorDOMNode, + moveMouseOnPanelCenter, clickOpenInspectorIcon */ + +"use strict"; + +const ACCORDION_LABEL_SELECTOR = ".accordion-header-label"; +const ACCORDION_CONTENT_SELECTOR = ".accordion-content"; + +// Retrieve the array of all the objectValueGrip actors from the +// inspector extension sidebars state +// (used in browser_ext_devtools_panels_elements_sidebar.js). +function getExtensionSidebarActors(inspector) { + const state = inspector.store.getState(); + + const actors = []; + + for (const sidebarId of Object.keys(state.extensionsSidebar)) { + const sidebarState = state.extensionsSidebar[sidebarId]; + + if ( + sidebarState.viewMode === "object-value-grip-view" && + sidebarState.objectValueGrip && + sidebarState.objectValueGrip.actor + ) { + actors.push(sidebarState.objectValueGrip.actor); + } + } + + return actors; +} + +// Test that the specified objectValueGrip actors have been released +// on the remote debugging server +// (used in browser_ext_devtools_panels_elements_sidebar.js). +async function expectNoSuchActorIDs(client, actors) { + info(`Test that all the objectValueGrip actors have been released`); + for (const actor of actors) { + await Assert.rejects( + client.request({ to: actor, type: "requestTypes" }), + err => err.message == `No such actor for ID: ${actor}` + ); + } +} + +function waitForObjectInspector(panelDoc, waitForNodeWithType = "object") { + const selector = `.object-inspector .objectBox-${waitForNodeWithType}`; + return TestUtils.waitForCondition(() => { + return !!panelDoc.querySelectorAll(selector).length; + }, `Wait for objectInspector's node type "${waitForNodeWithType}" to be loaded`); +} + +// Helper function used inside the sidebar.setExtensionPage test case. +async function testSetExtensionPageSidebarPanel(panelDoc, expectedURL) { + const selector = "iframe.inspector-extension-sidebar-page"; + const iframesCount = await TestUtils.waitForCondition(() => { + return panelDoc.querySelectorAll(selector).length; + }, "Wait for the extension page iframe"); + + is( + iframesCount, + 1, + "Got the expected number of iframes in the extension panel" + ); + + const iframeWindow = panelDoc.querySelector(selector).contentWindow; + await TestUtils.waitForCondition(() => { + return iframeWindow.document.readyState === "complete"; + }, "Wait for the extension page iframe to complete to load"); + + is( + iframeWindow.location.href, + expectedURL, + "Got the expected url in the extension panel iframe" + ); +} + +// Helper function used inside the sidebar.setObjectValueGrip test case. +async function testSetExpressionSidebarPanel(panel, expected) { + const { nodesLength, propertiesNames, rootTitle } = expected; + + await waitForObjectInspector(panel); + + const objectInspectors = [...panel.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + const [objectInspector] = objectInspectors; + + await TestUtils.waitForCondition(() => { + return objectInspector.querySelectorAll(".node").length >= nodesLength; + }, "Wait the objectInspector to have been fully rendered"); + + const oiNodes = objectInspector.querySelectorAll(".node"); + + is( + oiNodes.length, + nodesLength, + "Got the expected number of nodes in the tree" + ); + const propertiesNodes = [ + ...objectInspector.querySelectorAll(".object-label"), + ].map(el => el.textContent); + is( + JSON.stringify(propertiesNodes), + JSON.stringify(propertiesNames), + "Got the expected property names" + ); + + if (rootTitle) { + // Also check that the ObjectInspector is rendered inside + // an Accordion component with the expected title. + const accordion = panel.querySelector(".accordion"); + + ok(accordion, "Got an Accordion component as expected"); + + is( + accordion.querySelector(ACCORDION_CONTENT_SELECTOR).firstChild, + objectInspector, + "The ObjectInspector should be inside the Accordion content" + ); + + is( + accordion.querySelector(ACCORDION_LABEL_SELECTOR).textContent, + rootTitle, + "The Accordion has the expected label" + ); + } else { + // Also check that there is no Accordion component rendered + // inside the sidebar panel. + ok( + !panel.querySelector(".accordion"), + "Got no Accordion component as expected" + ); + } +} + +function assertTreeView(panelDoc, expectedContent) { + const { expectedTreeTables, expectedStringCells, expectedNumberCells } = + expectedContent; + + if (expectedTreeTables) { + is( + panelDoc.querySelectorAll("table.treeTable").length, + expectedTreeTables, + "The panel document contains the expected number of TreeView components" + ); + } + + if (expectedStringCells) { + is( + panelDoc.querySelectorAll("table.treeTable .stringCell").length, + expectedStringCells, + "The panel document contains the expected number of string cells." + ); + } + + if (expectedNumberCells) { + is( + panelDoc.querySelectorAll("table.treeTable .numberCell").length, + expectedNumberCells, + "The panel document contains the expected number of number cells." + ); + } +} + +async function assertObjectInspector(panelDoc, expectedContent) { + const { expectedDOMNodes, expectedOpenInspectors } = expectedContent; + + // Get and verify the DOMNode and the "open inspector"" icon + // rendered inside the ObjectInspector. + const nodes = panelDoc.querySelectorAll(".objectBox-node"); + const nodeOpenInspectors = panelDoc.querySelectorAll( + ".objectBox-node .open-inspector" + ); + + is( + nodes.length, + expectedDOMNodes, + "Found the expected number of ObjectInspector DOMNodes" + ); + is( + nodeOpenInspectors.length, + expectedOpenInspectors, + "Found the expected nuber of open-inspector icons inside the ObjectInspector" + ); +} + +function moveMouseOnObjectInspectorDOMNode(panelDoc, nodeIndex = 0) { + const nodes = panelDoc.querySelectorAll(".objectBox-node"); + const node = nodes[nodeIndex]; + + ok(node, "Found the ObjectInspector DOMNode"); + + EventUtils.synthesizeMouseAtCenter( + node, + { type: "mousemove" }, + node.ownerDocument.defaultView + ); +} + +function moveMouseOnPanelCenter(panelDoc) { + EventUtils.synthesizeMouseAtCenter( + panelDoc, + { type: "mousemove" }, + panelDoc.window + ); +} + +function clickOpenInspectorIcon(panelDoc, nodeIndex = 0) { + const nodes = panelDoc.querySelectorAll(".objectBox-node .open-inspector"); + const node = nodes[nodeIndex]; + + ok(node, "Found the ObjectInspector open-inspector icon"); + + node.click(); +} diff --git a/devtools/client/inspector/extensions/types.js b/devtools/client/inspector/extensions/types.js new file mode 100644 index 0000000000..187c281031 --- /dev/null +++ b/devtools/client/inspector/extensions/types.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +// Helpers injected as props by extension-sidebar.js and used by the +// ObjectInspector component (which is part of the ExpressionResultView). +exports.serviceContainer = { + highlightDomElement: PropTypes.func.isRequired, + unHighlightDomElement: PropTypes.func.isRequired, + openNodeInInspector: PropTypes.func.isRequired, +}; diff --git a/devtools/client/inspector/flexbox/actions/flexbox-highlighter.js b/devtools/client/inspector/flexbox/actions/flexbox-highlighter.js new file mode 100644 index 0000000000..487a4c3b12 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/flexbox-highlighter.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This module exports thunks. + * Thunks are functions that can be dispatched to the Inspector Redux store. + * + * These functions receive one object with options that contains: + * - dispatch() => function to dispatch Redux actions to the store + * - getState() => function to get the current state of the entire Inspector Redux store + * - inspector => object instance of Inspector panel + * + * They provide a shortcut for React components to invoke the flexbox highlighter + * without having to know where the highlighter exists. + */ + +module.exports = { + /** + * Toggle the flexbox highlighter for the given node front. + * + * @param {NodeFront} nodeFront + * Node for which the highlighter should be toggled. + * @param {String} reason + * Reason why the highlighter was toggled; used in telemetry. + */ + toggleFlexboxHighlighter(nodeFront, reason) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.toggleFlexboxHighlighter(nodeFront, reason); + }; + }, +}; diff --git a/devtools/client/inspector/flexbox/actions/flexbox.js b/devtools/client/inspector/flexbox/actions/flexbox.js new file mode 100644 index 0000000000..64c1e1f074 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/flexbox.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + CLEAR_FLEXBOX, + UPDATE_FLEXBOX, + UPDATE_FLEXBOX_COLOR, + UPDATE_FLEXBOX_HIGHLIGHTED, +} = require("resource://devtools/client/inspector/flexbox/actions/index.js"); + +module.exports = { + /** + * Clears the flexbox state by resetting it back to the initial flexbox state. + */ + clearFlexbox() { + return { + type: CLEAR_FLEXBOX, + }; + }, + + /** + * Updates the flexbox state with the newly selected flexbox. + */ + updateFlexbox(flexbox) { + return { + type: UPDATE_FLEXBOX, + flexbox, + }; + }, + + /** + * Updates the color used for the flexbox's highlighter. + * + * @param {String} color + * The color to use for this nodeFront's flexbox highlighter. + */ + updateFlexboxColor(color) { + return { + type: UPDATE_FLEXBOX_COLOR, + color, + }; + }, + + /** + * Updates the flexbox highlighted state. + * + * @param {Boolean} highlighted + * Whether or not the flexbox highlighter is highlighting the flexbox. + */ + updateFlexboxHighlighted(highlighted) { + return { + type: UPDATE_FLEXBOX_HIGHLIGHTED, + highlighted, + }; + }, +}; diff --git a/devtools/client/inspector/flexbox/actions/index.js b/devtools/client/inspector/flexbox/actions/index.js new file mode 100644 index 0000000000..bc4e30044e --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/index.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Clears the flexbox state by resetting it back to the initial flexbox state. + "CLEAR_FLEXBOX", + + // Updates the flexbox state with the newly selected flexbox. + "UPDATE_FLEXBOX", + + // Updates the color used for the overlay of a flexbox. + "UPDATE_FLEXBOX_COLOR", + + // Updates the flexbox highlighted state. + "UPDATE_FLEXBOX_HIGHLIGHTED", + ], + module.exports +); diff --git a/devtools/client/inspector/flexbox/actions/moz.build b/devtools/client/inspector/flexbox/actions/moz.build new file mode 100644 index 0000000000..3e5ab1eda8 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "flexbox-highlighter.js", + "flexbox.js", + "index.js", +) diff --git a/devtools/client/inspector/flexbox/components/FlexContainer.js b/devtools/client/inspector/flexbox/components/FlexContainer.js new file mode 100644 index 0000000000..c8c264defa --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexContainer.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createRef, + Fragment, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormatStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class FlexContainer extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + color: PropTypes.string.isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.swatchEl = createRef(); + + this.setFlexboxColor = this.setFlexboxColor.bind(this); + } + + componentDidMount() { + const tooltip = this.props.getSwatchColorPickerTooltip(); + + let previousColor; + tooltip.addSwatch(this.swatchEl.current, { + onCommit: this.setFlexboxColor, + onPreview: this.setFlexboxColor, + onRevert: () => { + this.props.onSetFlexboxOverlayColor(previousColor); + }, + onShow: () => { + previousColor = this.props.color; + }, + }); + } + + componentWillUnmount() { + const tooltip = this.props.getSwatchColorPickerTooltip(); + tooltip.removeSwatch(this.swatchEl.current); + } + + setFlexboxColor() { + const color = this.swatchEl.current.dataset.color; + this.props.onSetFlexboxOverlayColor(color); + } + + render() { + const { color, flexContainer, dispatch } = this.props; + const { nodeFront, properties } = flexContainer; + + return createElement( + Fragment, + null, + dom.div( + { + className: "flex-header-container-label", + }, + getNodeRep(nodeFront, { + onDOMNodeMouseOut: () => dispatch(unhighlightNode()), + onDOMNodeMouseOver: () => dispatch(highlightNode(nodeFront)), + }), + dom.button({ + className: "layout-color-swatch", + "data-color": color, + ref: this.swatchEl, + style: { + backgroundColor: color, + }, + title: getFormatStr("layout.colorSwatch.tooltip", color), + }) + ), + dom.div( + { className: "flex-header-container-properties" }, + dom.div( + { + className: "inspector-badge", + role: "figure", + title: `flex-direction: ${properties["flex-direction"]}`, + }, + properties["flex-direction"] + ), + dom.div( + { + className: "inspector-badge", + role: "figure", + title: `flex-wrap: ${properties["flex-wrap"]}`, + }, + properties["flex-wrap"] + ) + ) + ); + } +} + +module.exports = FlexContainer; diff --git a/devtools/client/inspector/flexbox/components/FlexItem.js b/devtools/client/inspector/flexbox/components/FlexItem.js new file mode 100644 index 0000000000..b29da8243e --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItem.js @@ -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/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class FlexItem extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + index: PropTypes.number.isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, flexItem, index, scrollToTop, setSelectedNode } = + this.props; + const { nodeFront } = flexItem; + + return dom.button( + { + className: "devtools-button devtools-monospace", + onClick: e => { + e.stopPropagation(); + scrollToTop(); + setSelectedNode(nodeFront); + dispatch(unhighlightNode()); + }, + onMouseOut: () => dispatch(unhighlightNode()), + onMouseOver: () => dispatch(highlightNode(nodeFront)), + }, + dom.span({ className: "flex-item-index" }, index), + getNodeRep(nodeFront) + ); + } +} + +module.exports = FlexItem; diff --git a/devtools/client/inspector/flexbox/components/FlexItemList.js b/devtools/client/inspector/flexbox/components/FlexItemList.js new file mode 100644 index 0000000000..0824ee1175 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemList.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +const FlexItem = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItem.js") +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class FlexItemList extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexItems: PropTypes.arrayOf(PropTypes.shape(Types.flexItem)).isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, flexItems, scrollToTop, setSelectedNode } = this.props; + + return dom.div( + { className: "flex-item-list" }, + dom.div( + { + className: "flex-item-list-header", + role: "heading", + "aria-level": "3", + }, + !flexItems.length + ? getStr("flexbox.noFlexItems") + : getStr("flexbox.flexItems") + ), + flexItems.map((flexItem, index) => + FlexItem({ + key: flexItem.actorID, + dispatch, + flexItem, + index: index + 1, + scrollToTop, + setSelectedNode, + }) + ) + ); + } +} + +module.exports = FlexItemList; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSelector.js b/devtools/client/inspector/flexbox/components/FlexItemSelector.js new file mode 100644 index 0000000000..d8dd2db7a3 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSelector.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getSelectorFromGrip, + translateNodeFrontToGrip, +} = require("resource://devtools/client/inspector/shared/utils.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); + +class FlexItemSelector extends PureComponent { + static get propTypes() { + return { + flexItem: PropTypes.shape(Types.flexItem).isRequired, + flexItems: PropTypes.arrayOf(PropTypes.shape(Types.flexItem)).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onShowFlexItemMenu = this.onShowFlexItemMenu.bind(this); + } + + onShowFlexItemMenu(event) { + event.stopPropagation(); + + const { flexItem, flexItems, setSelectedNode } = this.props; + const menuItems = []; + + for (const item of flexItems) { + const grip = translateNodeFrontToGrip(item.nodeFront); + menuItems.push({ + label: getSelectorFromGrip(grip), + type: "checkbox", + checked: item === flexItem, + click: () => setSelectedNode(item.nodeFront), + }); + } + + showMenu(menuItems, { + button: event.target, + }); + } + + render() { + const { flexItem } = this.props; + + return dom.button( + { + id: "flex-item-selector", + className: "devtools-button devtools-dropdown-button", + onClick: this.onShowFlexItemMenu, + }, + getNodeRep(flexItem.nodeFront) + ); + } +} + +module.exports = FlexItemSelector; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js b/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js new file mode 100644 index 0000000000..f0c35cd6be --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class FlexItemSizingOutline extends PureComponent { + static get propTypes() { + return { + flexDirection: PropTypes.string.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + }; + } + + renderBasisOutline(mainBaseSize) { + return dom.div({ + className: "flex-outline-basis" + (!mainBaseSize ? " zero-basis" : ""), + }); + } + + renderDeltaOutline(mainDeltaSize) { + if (!mainDeltaSize) { + return null; + } + + return dom.div({ + className: "flex-outline-delta", + }); + } + + renderFinalOutline(isClamped) { + return dom.div({ + className: "flex-outline-final" + (isClamped ? " clamped" : ""), + }); + } + + renderPoint(className, label = className) { + return dom.div({ + key: className, + className: `flex-outline-point ${className}`, + "data-label": label, + }); + } + + render() { + const { flexItemSizing } = this.props.flexItem; + const { + mainAxisDirection, + mainBaseSize, + mainDeltaSize, + mainMaxSize, + mainMinSize, + clampState, + } = flexItemSizing; + + // Calculate the final size. This is base + delta, then clamped by min or max. + let mainFinalSize = mainBaseSize + mainDeltaSize; + mainFinalSize = Math.max(mainFinalSize, mainMinSize); + mainFinalSize = + mainMaxSize === null + ? mainFinalSize + : Math.min(mainFinalSize, mainMaxSize); + + // Just don't display anything if there isn't anything useful. + if (!mainFinalSize && !mainBaseSize && !mainDeltaSize) { + return null; + } + + // The max size is only interesting to show if it did clamp the item. + const showMax = clampState === "clamped_to_max"; + + // The min size is only really interesting if it actually clamped the item. + // TODO: We might also want to check if the min-size property is defined. + const showMin = clampState === "clamped_to_min"; + + // Create an array of all of the sizes we want to display that we can use to create + // a grid track template. + let sizes = [ + { name: "basis-start", size: 0 }, + { name: "basis-end", size: mainBaseSize }, + { name: "final-start", size: 0 }, + { name: "final-end", size: mainFinalSize }, + ]; + + // Because mainDeltaSize is just a delta from base, make sure to make it absolute like + // the other sizes in the array, so we can later sort all sizes in the same way. + if (mainDeltaSize > 0) { + sizes.push({ name: "delta-start", size: mainBaseSize }); + sizes.push({ name: "delta-end", size: mainBaseSize + mainDeltaSize }); + } else { + sizes.push({ name: "delta-start", size: mainBaseSize + mainDeltaSize }); + sizes.push({ name: "delta-end", size: mainBaseSize }); + } + + if (showMax) { + sizes.push({ name: "max", size: mainMaxSize }); + } + if (showMin) { + sizes.push({ name: "min", size: mainMinSize }); + } + + // Sort all of the dimensions so we can create the grid track template correctly. + sizes = sizes.sort((a, b) => a.size - b.size); + + // In some cases, the delta-start may be negative (when an item wanted to shrink more + // than the item's base size). As a negative value would break the grid track template + // offset all values so they're all positive. + const offsetBy = sizes.reduce( + (acc, curr) => (curr.size < acc ? curr.size : acc), + 0 + ); + sizes = sizes.map(entry => ({ + size: entry.size - offsetBy, + name: entry.name, + })); + + let gridTemplateColumns = "["; + let accumulatedSize = 0; + for (const { name, size } of sizes) { + const breadth = Math.round(size - accumulatedSize); + if (breadth === 0) { + gridTemplateColumns += ` ${name}`; + continue; + } + + gridTemplateColumns += `] ${breadth}fr [${name}`; + accumulatedSize = size; + } + gridTemplateColumns += "]"; + + // Check the final and basis points to see if they are the same and if so, combine + // them into a single rendered point. + const renderedBaseAndFinalPoints = []; + if (mainFinalSize === mainBaseSize) { + renderedBaseAndFinalPoints.push( + this.renderPoint("basisfinal", "basis/final") + ); + } else { + renderedBaseAndFinalPoints.push(this.renderPoint("basis")); + renderedBaseAndFinalPoints.push(this.renderPoint("final")); + } + + return dom.div( + { className: "flex-outline-container" }, + dom.div( + { + className: + `flex-outline ${mainAxisDirection}` + + (mainDeltaSize > 0 ? " growing" : " shrinking"), + style: { + gridTemplateColumns, + }, + }, + renderedBaseAndFinalPoints, + showMin ? this.renderPoint("min") : null, + showMax ? this.renderPoint("max") : null, + this.renderBasisOutline(mainBaseSize), + this.renderDeltaOutline(mainDeltaSize), + this.renderFinalOutline(clampState !== "unclamped") + ) + ); + } +} + +module.exports = FlexItemSizingOutline; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js new file mode 100644 index 0000000000..00bea31e57 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js @@ -0,0 +1,326 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const getFlexibilityReasons = ({ + lineGrowthState, + computedFlexGrow, + computedFlexShrink, + grew, + shrank, +}) => { + const reasons = []; + + // Tell users whether the item was set to grow or shrink. + if (computedFlexGrow && lineGrowthState === "growing") { + reasons.push(getStr("flexbox.itemSizing.setToGrow")); + } + if (computedFlexShrink && lineGrowthState === "shrinking") { + reasons.push(getStr("flexbox.itemSizing.setToShrink")); + } + if (!computedFlexGrow && !grew && !shrank && lineGrowthState === "growing") { + reasons.push(getStr("flexbox.itemSizing.notSetToGrow")); + } + if ( + !computedFlexShrink && + !grew && + !shrank && + lineGrowthState === "shrinking" + ) { + reasons.push(getStr("flexbox.itemSizing.notSetToShrink")); + } + + return reasons; +}; + +class FlexItemSizingProperties extends PureComponent { + static get propTypes() { + return { + flexDirection: PropTypes.string.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + }; + } + + /** + * Rounds some size in pixels and render it. + * The rendered value will end with 'px' (unless the dimension is 0 in which case the + * unit will be omitted) + * + * @param {Number} value + * The number to be rounded + * @param {Boolean} prependPlusSign + * If set to true, the + sign will be printed before a positive value + * @return {Object} + * The React component representing this rounded size + */ + renderSize(value, prependPlusSign) { + if (value == 0) { + return dom.span({ className: "value" }, "0"); + } + + value = Math.round(value * 100) / 100; + if (prependPlusSign && value > 0) { + value = "+" + value; + } + + return dom.span( + { className: "value" }, + value, + dom.span({ className: "unit" }, "px") + ); + } + + /** + * Render an authored CSS property. + * + * @param {String} name + * The name for this CSS property + * @param {String} value + * The property value + * @param {Booleam} isDefaultValue + * Whether the value come from the browser default style + * @return {Object} + * The React component representing this CSS property + */ + renderCssProperty(name, value, isDefaultValue) { + return dom.span({ className: "css-property-link" }, `(${name}: ${value})`); + } + + /** + * Render a list of sentences to be displayed in the UI as reasons why a certain sizing + * value happened. + * + * @param {Array} sentences + * The list of sentences as Strings + * @return {Object} + * The React component representing these sentences + */ + renderReasons(sentences) { + return dom.ul( + { className: "reasons" }, + sentences.map(sentence => dom.li({}, sentence)) + ); + } + + renderBaseSizeSection({ mainBaseSize, clampState }, properties, dimension) { + const flexBasisValue = properties["flex-basis"]; + const dimensionValue = properties[dimension]; + + let title = getStr("flexbox.itemSizing.baseSizeSectionHeader"); + let property = null; + + if (flexBasisValue) { + // If flex-basis is defined, then that's what is used for the base size. + property = this.renderCssProperty("flex-basis", flexBasisValue); + } else if (dimensionValue) { + // If not and width/height is defined, then that's what defines the base size. + property = this.renderCssProperty(dimension, dimensionValue); + } else { + // Finally, if nothing is set, then the base size is the max-content size. + // In this case replace the section's title. + title = getStr("flexbox.itemSizing.itemContentSize"); + } + + const className = "section base"; + return dom.li( + { className: className + (property ? "" : " no-property") }, + dom.span({ className: "name" }, title, property), + this.renderSize(mainBaseSize) + ); + } + + renderFlexibilitySection( + flexItemSizing, + mainFinalSize, + properties, + computedStyle + ) { + const { mainDeltaSize, mainBaseSize, lineGrowthState } = flexItemSizing; + + // Don't display anything if all interesting sizes are 0. + if (!mainFinalSize && !mainBaseSize && !mainDeltaSize) { + return null; + } + + // Also don't display anything if the item did not grow or shrink. + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + if (!grew && !shrank) { + return null; + } + + const definedFlexGrow = properties["flex-grow"]; + const computedFlexGrow = computedStyle.flexGrow; + const definedFlexShrink = properties["flex-shrink"]; + const computedFlexShrink = computedStyle.flexShrink; + + const reasons = getFlexibilityReasons({ + lineGrowthState, + computedFlexGrow, + computedFlexShrink, + grew, + shrank, + }); + + let property = null; + + if (grew && definedFlexGrow && computedFlexGrow) { + // If the item grew it's normally because it was set to grow (flex-grow is non 0). + property = this.renderCssProperty("flex-grow", definedFlexGrow); + } else if (shrank && definedFlexShrink && computedFlexShrink) { + // If the item shrank it's either because flex-shrink is non 0. + property = this.renderCssProperty("flex-shrink", definedFlexShrink); + } else if (shrank && computedFlexShrink) { + // Or also because it's default value is 1 anyway. + property = this.renderCssProperty( + "flex-shrink", + computedFlexShrink, + true + ); + } + + // Don't display the section at all if there's nothing useful to show users. + if (!property && !reasons.length) { + return null; + } + + const className = "section flexibility"; + return dom.li( + { className: className + (property ? "" : " no-property") }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.flexibilitySectionHeader"), + property + ), + this.renderSize(mainDeltaSize, true), + this.renderReasons(reasons) + ); + } + + renderMinimumSizeSection(flexItemSizing, properties, dimension) { + const { clampState, mainMinSize, mainDeltaSize } = flexItemSizing; + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + const minDimensionValue = properties[`min-${dimension}`]; + + // We only display the minimum size when the item actually violates that size during + // layout & is clamped. + if (clampState !== "clamped_to_min") { + return null; + } + + const reasons = []; + if (grew || shrank) { + // The item may have wanted to grow less, but was min-clamped to a larger size. + // Or the item may have wanted to shrink more but was min-clamped to a larger size. + reasons.push(getStr("flexbox.itemSizing.clampedToMin")); + } + + return dom.li( + { className: "section min" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.minSizeSectionHeader"), + minDimensionValue.length + ? this.renderCssProperty(`min-${dimension}`, minDimensionValue) + : null + ), + this.renderSize(mainMinSize), + this.renderReasons(reasons) + ); + } + + renderMaximumSizeSection(flexItemSizing, properties, dimension) { + const { clampState, mainMaxSize, mainDeltaSize } = flexItemSizing; + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + const maxDimensionValue = properties[`max-${dimension}`]; + + if (clampState !== "clamped_to_max") { + return null; + } + + const reasons = []; + if (grew || shrank) { + // The item may have wanted to grow more than it did, because it was max-clamped. + // Or the item may have wanted shrink more, but it was clamped to its max size. + reasons.push(getStr("flexbox.itemSizing.clampedToMax")); + } + + return dom.li( + { className: "section max" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.maxSizeSectionHeader"), + maxDimensionValue.length + ? this.renderCssProperty(`max-${dimension}`, maxDimensionValue) + : null + ), + this.renderSize(mainMaxSize), + this.renderReasons(reasons) + ); + } + + renderFinalSizeSection(mainFinalSize) { + return dom.li( + { className: "section final no-property" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.finalSizeSectionHeader") + ), + this.renderSize(mainFinalSize) + ); + } + + render() { + const { flexItem } = this.props; + const { computedStyle, flexItemSizing, properties } = flexItem; + const { + mainAxisDirection, + mainBaseSize, + mainDeltaSize, + mainMaxSize, + mainMinSize, + } = flexItemSizing; + const dimension = mainAxisDirection.startsWith("horizontal") + ? "width" + : "height"; + + // Calculate the final size. This is base + delta, then clamped by min or max. + let mainFinalSize = mainBaseSize + mainDeltaSize; + mainFinalSize = Math.max(mainFinalSize, mainMinSize); + mainFinalSize = + mainMaxSize === null + ? mainFinalSize + : Math.min(mainFinalSize, mainMaxSize); + + return dom.ul( + { className: "flex-item-sizing" }, + this.renderBaseSizeSection(flexItemSizing, properties, dimension), + this.renderFlexibilitySection( + flexItemSizing, + mainFinalSize, + properties, + computedStyle + ), + this.renderMinimumSizeSection(flexItemSizing, properties, dimension), + this.renderMaximumSizeSection(flexItemSizing, properties, dimension), + this.renderFinalSizeSection(mainFinalSize) + ); + } +} + +module.exports = FlexItemSizingProperties; diff --git a/devtools/client/inspector/flexbox/components/Flexbox.js b/devtools/client/inspector/flexbox/components/Flexbox.js new file mode 100644 index 0000000000..a0d79d92eb --- /dev/null +++ b/devtools/client/inspector/flexbox/components/Flexbox.js @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createFactory, + Fragment, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +loader.lazyGetter(this, "FlexItemList", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemList.js") + ); +}); +loader.lazyGetter(this, "FlexItemSizingOutline", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js") + ); +}); +loader.lazyGetter(this, "FlexItemSizingProperties", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js") + ); +}); +loader.lazyGetter(this, "Header", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/Header.js") + ); +}); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class Flexbox extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexbox: PropTypes.shape(Types.flexbox).isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + renderFlexItemList() { + const { dispatch, scrollToTop, setSelectedNode } = this.props; + const { flexItems } = this.props.flexContainer; + + return FlexItemList({ + dispatch, + flexItems, + scrollToTop, + setSelectedNode, + }); + } + + renderFlexItemSizing() { + const { flexItems, flexItemShown, properties } = this.props.flexContainer; + + const flexItem = flexItems.find( + item => item.nodeFront.actorID === flexItemShown + ); + if (!flexItem) { + return null; + } + + return createElement( + Fragment, + null, + FlexItemSizingOutline({ + flexDirection: properties["flex-direction"], + flexItem, + }), + FlexItemSizingProperties({ + flexDirection: properties["flex-direction"], + flexItem, + }) + ); + } + + render() { + const { + dispatch, + flexbox, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + setSelectedNode, + } = this.props; + + if (!flexContainer.actorID) { + return dom.div( + { className: "devtools-sidepanel-no-result" }, + getStr("flexbox.noFlexboxeOnThisPage") + ); + } + + const { flexItemShown } = flexContainer; + const { color, highlighted } = flexbox; + + return dom.div( + { className: "layout-flexbox-wrapper" }, + Header({ + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + highlighted, + onSetFlexboxOverlayColor, + setSelectedNode, + }), + !flexItemShown ? this.renderFlexItemList() : null, + flexItemShown ? this.renderFlexItemSizing() : null + ); + } +} + +module.exports = Flexbox; diff --git a/devtools/client/inspector/flexbox/components/Header.js b/devtools/client/inspector/flexbox/components/Header.js new file mode 100644 index 0000000000..44b19703e3 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/Header.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createFactory, + Fragment, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +const FlexContainer = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexContainer.js") +); +const FlexItemSelector = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSelector.js") +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + toggleFlexboxHighlighter, +} = require("resource://devtools/client/inspector/flexbox/actions/flexbox-highlighter.js"); + +class Header extends PureComponent { + static get propTypes() { + return { + color: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + highlighted: PropTypes.bool.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + renderFlexboxHighlighterToggle() { + const { dispatch, flexContainer, highlighted } = this.props; + // Don't show the flexbox highlighter toggle for the parent flex container of the + // selected element. + if (flexContainer.isFlexItemContainer) { + return null; + } + + return createElement( + Fragment, + null, + dom.div({ className: "devtools-separator" }), + dom.input({ + id: "flexbox-checkbox-toggle", + className: "devtools-checkbox-toggle", + checked: highlighted, + onChange: () => + dispatch(toggleFlexboxHighlighter(flexContainer.nodeFront, "layout")), + title: getStr("flexbox.togglesFlexboxHighlighter2"), + type: "checkbox", + }) + ); + } + + renderFlexContainer() { + if (this.props.flexContainer.flexItemShown) { + return null; + } + + const { + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + } = this.props; + + return FlexContainer({ + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + }); + } + + renderFlexItemSelector() { + if (!this.props.flexContainer.flexItemShown) { + return null; + } + + const { flexContainer, setSelectedNode } = this.props; + const { flexItems, flexItemShown } = flexContainer; + const flexItem = flexItems.find( + item => item.nodeFront.actorID === flexItemShown + ); + + if (!flexItem) { + return null; + } + + return FlexItemSelector({ + flexItem, + flexItems, + setSelectedNode, + }); + } + + render() { + const { flexContainer, setSelectedNode } = this.props; + const { flexItemShown, nodeFront } = flexContainer; + + return dom.div( + { className: "flex-header devtools-monospace" }, + flexItemShown + ? dom.button({ + className: "flex-header-button-prev devtools-button", + "aria-label": getStr("flexbox.backButtonLabel"), + onClick: e => { + e.stopPropagation(); + setSelectedNode(nodeFront); + }, + }) + : null, + dom.div( + { + className: + "flex-header-content" + (flexItemShown ? " flex-item-shown" : ""), + }, + this.renderFlexContainer(), + this.renderFlexItemSelector() + ), + this.renderFlexboxHighlighterToggle() + ); + } +} + +module.exports = Header; diff --git a/devtools/client/inspector/flexbox/components/moz.build b/devtools/client/inspector/flexbox/components/moz.build new file mode 100644 index 0000000000..3e077a217f --- /dev/null +++ b/devtools/client/inspector/flexbox/components/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "Flexbox.js", + "FlexContainer.js", + "FlexItem.js", + "FlexItemList.js", + "FlexItemSelector.js", + "FlexItemSizingOutline.js", + "FlexItemSizingProperties.js", + "Header.js", +) diff --git a/devtools/client/inspector/flexbox/flexbox.js b/devtools/client/inspector/flexbox/flexbox.js new file mode 100644 index 0000000000..3947da3cf9 --- /dev/null +++ b/devtools/client/inspector/flexbox/flexbox.js @@ -0,0 +1,569 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { throttle } = require("resource://devtools/shared/throttle.js"); + +const { + clearFlexbox, + updateFlexbox, + updateFlexboxColor, + updateFlexboxHighlighted, +} = require("resource://devtools/client/inspector/flexbox/actions/flexbox.js"); +const flexboxReducer = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js"); + +loader.lazyRequireGetter( + this, + "parseURL", + "resource://devtools/client/shared/source-utils.js", + true +); +loader.lazyRequireGetter( + this, + "asyncStorage", + "resource://devtools/shared/async-storage.js" +); + +const FLEXBOX_COLOR = "#9400FF"; + +class FlexboxInspector { + constructor(inspector, window) { + this.document = window.document; + this.inspector = inspector; + this.selection = inspector.selection; + this.store = inspector.store; + + this.store.injectReducer("flexbox", flexboxReducer); + + this.onHighlighterShown = this.onHighlighterShown.bind(this); + this.onHighlighterHidden = this.onHighlighterHidden.bind(this); + this.onNavigate = this.onNavigate.bind(this); + this.onReflow = throttle(this.onReflow, 500, this); + this.onSetFlexboxOverlayColor = this.onSetFlexboxOverlayColor.bind(this); + this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.onUpdatePanel = this.onUpdatePanel.bind(this); + + this.init(); + } + + init() { + if (!this.inspector) { + return; + } + + this.inspector.highlighters.on( + "highlighter-shown", + this.onHighlighterShown + ); + this.inspector.highlighters.on( + "highlighter-hidden", + this.onHighlighterHidden + ); + this.inspector.sidebar.on("select", this.onSidebarSelect); + + this.onSidebarSelect(); + } + + destroy() { + this.selection.off("new-node-front", this.onUpdatePanel); + this.inspector.off("new-root", this.onNavigate); + this.inspector.off("reflow-in-selected-target", this.onReflow); + this.inspector.highlighters.off( + "highlighter-shown", + this.onHighlighterShown + ); + this.inspector.highlighters.off( + "highlighter-hidden", + this.onHighlighterHidden + ); + this.inspector.sidebar.off("select", this.onSidebarSelect); + + this._customHostColors = null; + this._overlayColor = null; + this.document = null; + this.inspector = null; + this.selection = null; + this.store = null; + } + + getComponentProps() { + return { + onSetFlexboxOverlayColor: this.onSetFlexboxOverlayColor, + }; + } + + /** + * Returns an object containing the custom flexbox colors for different hosts. + * + * @return {Object} that maps a host name to a custom flexbox color for a given host. + */ + async getCustomHostColors() { + if (this._customHostColors) { + return this._customHostColors; + } + + // Cache the custom host colors to avoid refetching from async storage. + this._customHostColors = + (await asyncStorage.getItem("flexboxInspectorHostColors")) || {}; + return this._customHostColors; + } + + /** + * Returns the flex container properties for a given node. If the given node is a flex + * item, it attempts to fetch the flex container of the parent node of the given node. + * + * @param {NodeFront} nodeFront + * The NodeFront to fetch the flex container properties. + * @param {Boolean} onlyLookAtParents + * Whether or not to only consider the parent node of the given node. + * @return {Object} consisting of the given node's flex container's properties. + */ + async getFlexContainerProps(nodeFront, onlyLookAtParents = false) { + const layoutFront = await nodeFront.walkerFront.getLayoutInspector(); + const flexboxFront = await layoutFront.getCurrentFlexbox( + nodeFront, + onlyLookAtParents + ); + + if (!flexboxFront) { + return null; + } + + // If the FlexboxFront doesn't yet have access to the NodeFront for its container, + // then get it from the walker. This happens when the walker hasn't seen this + // particular DOM Node in the tree yet or when we are connected to an older server. + let containerNodeFront = flexboxFront.containerNodeFront; + if (!containerNodeFront) { + containerNodeFront = await flexboxFront.walkerFront.getNodeFromActor( + flexboxFront.actorID, + ["containerEl"] + ); + } + + const flexItems = await this.getFlexItems(flexboxFront); + + // If the current selected node is a flex item, display its flex item sizing + // properties. + let flexItemShown = null; + if (onlyLookAtParents) { + flexItemShown = this.selection.nodeFront.actorID; + } else { + const selectedFlexItem = flexItems.find( + item => item.nodeFront === this.selection.nodeFront + ); + if (selectedFlexItem) { + flexItemShown = selectedFlexItem.nodeFront.actorID; + } + } + + return { + actorID: flexboxFront.actorID, + flexItems, + flexItemShown, + isFlexItemContainer: onlyLookAtParents, + nodeFront: containerNodeFront, + properties: flexboxFront.properties, + }; + } + + /** + * Returns an array of flex items object for the given flex container front. + * + * @param {FlexboxFront} flexboxFront + * A flex container FlexboxFront. + * @return {Array} of objects containing the flex item front properties. + */ + async getFlexItems(flexboxFront) { + const flexItemFronts = await flexboxFront.getFlexItems(); + const flexItems = []; + + for (const flexItemFront of flexItemFronts) { + // Fetch the NodeFront of the flex items. + let itemNodeFront = flexItemFront.nodeFront; + if (!itemNodeFront) { + itemNodeFront = await flexItemFront.walkerFront.getNodeFromActor( + flexItemFront.actorID, + ["element"] + ); + } + + flexItems.push({ + actorID: flexItemFront.actorID, + computedStyle: flexItemFront.computedStyle, + flexItemSizing: flexItemFront.flexItemSizing, + nodeFront: itemNodeFront, + properties: flexItemFront.properties, + }); + } + + return flexItems; + } + + /** + * Returns the custom overlay color for the current host or the default flexbox color. + * + * @return {String} overlay color. + */ + async getOverlayColor() { + if (this._overlayColor) { + return this._overlayColor; + } + + // Cache the overlay color for the current host to avoid repeatably parsing the host + // and fetching the custom color from async storage. + const customColors = await this.getCustomHostColors(); + const currentUrl = this.inspector.currentTarget.url; + // Get the hostname, if there is no hostname, fall back on protocol + // ex: `data:` uri, and `about:` pages + const hostname = + parseURL(currentUrl).hostname || parseURL(currentUrl).protocol; + this._overlayColor = customColors[hostname] + ? customColors[hostname] + : FLEXBOX_COLOR; + return this._overlayColor; + } + + /** + * Returns true if the layout panel is visible, and false otherwise. + */ + isPanelVisible() { + return ( + this.inspector && + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() === "layoutview" + ); + } + + /** + * Handler for "highlighter-shown" events emitted by HighlightersOverlay. + * If the event is dispatched on behalf of a flex highlighter, toggle the + * corresponding flex container's highlighted state in the Redux store. + * + * @param {Object} data + * Object with data associated with the highlighter event. + * {NodeFront} data.nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter is shown for. + * {String} data.type + * Highlighter type + */ + onHighlighterShown(data) { + if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) { + this.onHighlighterChange(true, data.nodeFront); + } + } + + /** + * Handler for "highlighter-shown" events emitted by HighlightersOverlay. + * If the event is dispatched on behalf of a flex highlighter, toggle the + * corresponding flex container's highlighted state in the Redux store. + * + * @param {Object} data + * Object with data associated with the highlighter event. + * {NodeFront} data.nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter was previously shown for. + * {String} data.type + * Highlighter type + */ + onHighlighterHidden(data) { + if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) { + this.onHighlighterChange(false, data.nodeFront); + } + } + + /** + * Updates the flex container highlighted state in the Redux store if the provided + * NodeFront is the current selected flex container. + * + * @param {Boolean} highlighted + * Whether the change is to highlight or hide the overlay. + * @param {NodeFront} nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter is shown for. + */ + onHighlighterChange(highlighted, nodeFront) { + const { flexbox } = this.store.getState(); + + if ( + flexbox.flexContainer.nodeFront === nodeFront && + flexbox.highlighted !== highlighted + ) { + this.store.dispatch(updateFlexboxHighlighted(highlighted)); + } + } + + /** + * Handler for the "new-root" event fired by the inspector. Clears the cached overlay + * color for the flexbox highlighter and updates the panel. + */ + onNavigate() { + this._overlayColor = null; + this.onUpdatePanel(); + } + + /** + * Handler for reflow events fired by the inspector when a node is selected. On reflows, + * update the flexbox panel because the shape of the flexbox on the page may have + * changed. + */ + async onReflow() { + if ( + !this.isPanelVisible() || + !this.store || + !this.selection.nodeFront || + this._isUpdating + ) { + return; + } + + try { + const flexContainer = await this.getFlexContainerProps( + this.selection.nodeFront + ); + + // Clear the flexbox panel if there is no flex container for the current node + // selection. + if (!flexContainer) { + this.store.dispatch(clearFlexbox()); + return; + } + + const { flexbox } = this.store.getState(); + + // Compare the new flexbox state of the current selected nodeFront with the old + // flexbox state to determine if we need to update. + if (hasFlexContainerChanged(flexbox.flexContainer, flexContainer)) { + this.update(flexContainer); + return; + } + + let flexItemContainer = null; + // If the current selected node is also the flex container node, check if it is + // a flex item of a parent flex container. + if (flexContainer.nodeFront === this.selection.nodeFront) { + flexItemContainer = await this.getFlexContainerProps( + this.selection.nodeFront, + true + ); + } + + // Compare the new and old state of the parent flex container properties. + if ( + hasFlexContainerChanged(flexbox.flexItemContainer, flexItemContainer) + ) { + this.update(flexContainer, flexItemContainer); + } + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + } + } + + /** + * Handler for a change in the flexbox overlay color picker for a flex container. + * + * @param {String} color + * A hex string representing the color to use. + */ + async onSetFlexboxOverlayColor(color) { + this.store.dispatch(updateFlexboxColor(color)); + + const { flexbox } = this.store.getState(); + + if (flexbox.highlighted) { + this.inspector.highlighters.showFlexboxHighlighter( + flexbox.flexContainer.nodeFront + ); + } + + this._overlayColor = color; + + const currentUrl = this.inspector.currentTarget.url; + // Get the hostname, if there is no hostname, fall back on protocol + // ex: `data:` uri, and `about:` pages + const hostname = + parseURL(currentUrl).hostname || parseURL(currentUrl).protocol; + const customColors = await this.getCustomHostColors(); + customColors[hostname] = color; + this._customHostColors = customColors; + await asyncStorage.setItem("flexboxInspectorHostColors", customColors); + } + + /** + * Handler for the inspector sidebar "select" event. Updates the flexbox panel if it + * is visible. + */ + onSidebarSelect() { + if (!this.isPanelVisible()) { + this.inspector.off("reflow-in-selected-target", this.onReflow); + this.inspector.off("new-root", this.onNavigate); + this.selection.off("new-node-front", this.onUpdatePanel); + return; + } + + this.inspector.on("reflow-in-selected-target", this.onReflow); + this.inspector.on("new-root", this.onNavigate); + this.selection.on("new-node-front", this.onUpdatePanel); + + this.update(); + } + + /** + * Handler for "new-root" event fired by the inspector and "new-node-front" event fired + * by the inspector selection. Updates the flexbox panel if it is visible. + * + * @param {Object} + * This callback is sometimes executed on "new-node-front" events which means + * that a first param is passed here (the nodeFront), which we don't care about. + * @param {String} reason + * On "new-node-front" events, a reason is passed here, and we need it to detect + * if this update was caused by a node selection from the markup-view. + */ + onUpdatePanel(_, reason) { + if (!this.isPanelVisible()) { + return; + } + + this.update(null, null, reason === "treepanel"); + } + + /** + * Updates the flexbox panel by dispatching the new flexbox data. This is called when + * the layout view becomes visible or a new node is selected and needs to be update + * with new flexbox data. + * + * @param {Object|null} flexContainer + * An object consisting of the current flex container's flex items and + * properties. + * @param {Object|null} flexItemContainer + * An object consisting of the parent flex container's flex items and + * properties. + * @param {Boolean} initiatedByMarkupViewSelection + * True if the update was due to a node selection in the markup-view. + */ + async update( + flexContainer, + flexItemContainer, + initiatedByMarkupViewSelection + ) { + this._isUpdating = true; + + // Stop refreshing if the inspector or store is already destroyed or no node is + // selected. + if (!this.inspector || !this.store || !this.selection.nodeFront) { + this._isUpdating = false; + return; + } + + try { + // Fetch the current flexbox if no flexbox front was passed into this update. + if (!flexContainer) { + flexContainer = await this.getFlexContainerProps( + this.selection.nodeFront + ); + } + + // Clear the flexbox panel if there is no flex container for the current node + // selection. + if (!flexContainer) { + this.store.dispatch(clearFlexbox()); + this._isUpdating = false; + return; + } + + if ( + !flexItemContainer && + flexContainer.nodeFront === this.selection.nodeFront + ) { + flexItemContainer = await this.getFlexContainerProps( + this.selection.nodeFront, + true + ); + } + + const highlighted = + flexContainer.nodeFront === + this.inspector.highlighters.getNodeForActiveHighlighter( + this.inspector.highlighters.TYPES.FLEXBOX + ); + const color = await this.getOverlayColor(); + + this.store.dispatch( + updateFlexbox({ + color, + flexContainer, + flexItemContainer, + highlighted, + initiatedByMarkupViewSelection, + }) + ); + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + } + + this._isUpdating = false; + } +} + +/** + * For a given flex container object, returns the flex container properties that can be + * used to check if 2 flex container objects are the same. + * + * @param {Object|null} flexContainer + * Object consisting of the flex container's properties. + * @return {Object|null} consisting of the comparable flex container's properties. + */ +function getComparableFlexContainerProperties(flexContainer) { + if (!flexContainer) { + return null; + } + + return { + flexItems: getComparableFlexItemsProperties(flexContainer.flexItems), + nodeFront: flexContainer.nodeFront.actorID, + properties: flexContainer.properties, + }; +} + +/** + * Given an array of flex item objects, returns the relevant flex item properties that can + * be compared to check if any changes has occurred. + * + * @param {Array} flexItems + * Array of objects containing the flex item properties. + * @return {Array} of objects consisting of the comparable flex item's properties. + */ +function getComparableFlexItemsProperties(flexItems) { + return flexItems.map(item => { + return { + computedStyle: item.computedStyle, + flexItemSizing: item.flexItemSizing, + nodeFront: item.nodeFront.actorID, + properties: item.properties, + }; + }); +} + +/** + * Compares the old and new flex container properties + * + * @param {Object} oldFlexContainer + * Object consisting of the old flex container's properties. + * @param {Object} newFlexContainer + * Object consisting of the new flex container's properties. + * @return {Boolean} true if the flex container properties are the same, false otherwise. + */ +function hasFlexContainerChanged(oldFlexContainer, newFlexContainer) { + return ( + JSON.stringify(getComparableFlexContainerProperties(oldFlexContainer)) !== + JSON.stringify(getComparableFlexContainerProperties(newFlexContainer)) + ); +} + +module.exports = FlexboxInspector; diff --git a/devtools/client/inspector/flexbox/moz.build b/devtools/client/inspector/flexbox/moz.build new file mode 100644 index 0000000000..b38f12db55 --- /dev/null +++ b/devtools/client/inspector/flexbox/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "actions", + "components", + "reducers", +] + +DevToolsModules( + "flexbox.js", + "types.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/devtools/client/inspector/flexbox/reducers/flexbox.js b/devtools/client/inspector/flexbox/reducers/flexbox.js new file mode 100644 index 0000000000..856294a549 --- /dev/null +++ b/devtools/client/inspector/flexbox/reducers/flexbox.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + CLEAR_FLEXBOX, + UPDATE_FLEXBOX, + UPDATE_FLEXBOX_COLOR, + UPDATE_FLEXBOX_HIGHLIGHTED, +} = require("resource://devtools/client/inspector/flexbox/actions/index.js"); + +const INITIAL_FLEXBOX = { + // The color of the flexbox highlighter overlay. + color: "", + // The flex container of the selected element. + flexContainer: { + // The actor ID of the selected flex container. + actorID: "", + // An array of flex items belonging to the selected flex container. + flexItems: [], + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: null, + // This flag specifies that the flex container data represents the selected flex + // container. + isFlexItemContainer: false, + // The NodeFront of the selected flex container. + nodeFront: null, + // The computed style properties of the selected flex container. + properties: null, + }, + // The selected flex container can also be a flex item. This object contains the + // parent flex container properties of the selected element. + flexItemContainer: { + // The actor ID of the parent flex container. + actorID: "", + // An array of flex items belonging to the parent flex container. + flexItems: [], + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: null, + // This flag specifies that the flex container data represents the parent flex + // container of the selected element. + isFlexItemContainer: true, + // The NodeFront of the parent flex container. + nodeFront: null, + // The computed styles properties of the parent flex container. + properties: null, + }, + // Whether or not the flexbox highlighter is highlighting the flex container. + highlighted: false, + // Whether or not the node selection that led to the flexbox tool being shown came from + // the user selecting a node in the markup-view (whereas, say, selecting in the flex + // items list) + initiatedByMarkupViewSelection: false, +}; + +const reducers = { + [CLEAR_FLEXBOX](flexbox, _) { + return INITIAL_FLEXBOX; + }, + + [UPDATE_FLEXBOX](_, { flexbox }) { + return flexbox; + }, + + [UPDATE_FLEXBOX_COLOR](flexbox, { color }) { + return { + ...flexbox, + color, + }; + }, + + [UPDATE_FLEXBOX_HIGHLIGHTED](flexbox, { highlighted }) { + return { + ...flexbox, + highlighted, + }; + }, +}; + +module.exports = function (flexbox = INITIAL_FLEXBOX, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return flexbox; + } + return reducer(flexbox, action); +}; diff --git a/devtools/client/inspector/flexbox/reducers/index.js b/devtools/client/inspector/flexbox/reducers/index.js new file mode 100644 index 0000000000..7f415cb163 --- /dev/null +++ b/devtools/client/inspector/flexbox/reducers/index.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +exports.flexbox = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js"); diff --git a/devtools/client/inspector/flexbox/reducers/moz.build b/devtools/client/inspector/flexbox/reducers/moz.build new file mode 100644 index 0000000000..4327940888 --- /dev/null +++ b/devtools/client/inspector/flexbox/reducers/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "flexbox.js", + "index.js", +) diff --git a/devtools/client/inspector/flexbox/test/Ahem.ttf b/devtools/client/inspector/flexbox/test/Ahem.ttf new file mode 100644 index 0000000000..ac81cb0316 Binary files /dev/null and b/devtools/client/inspector/flexbox/test/Ahem.ttf differ diff --git a/devtools/client/inspector/flexbox/test/browser.toml b/devtools/client/inspector/flexbox/test/browser.toml new file mode 100644 index 0000000000..764dd1ffba --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser.toml @@ -0,0 +1,91 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "Ahem.ttf", + "doc_flexbox_CSS_property_with_!important.html", + "doc_flexbox_pseudos.html", + "doc_flexbox_specific_cases.html", + "doc_flexbox_text_nodes.html", + "doc_flexbox_unauthored_min_dimension.html", + "doc_flexbox_writing_modes.html", + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_flexbox_accordion_state.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_flexbox_container_and_item.js"] + +["browser_flexbox_container_and_item_accordion_state.js"] + +["browser_flexbox_container_and_item_updates_on_change.js"] + +["browser_flexbox_container_element_rep.js"] + +["browser_flexbox_container_properties.js"] + +["browser_flexbox_empty_state.js"] + +["browser_flexbox_grand_parent_flex.js"] + +["browser_flexbox_highlighter_color_picker_on_ESC.js"] + +["browser_flexbox_highlighter_color_picker_on_RETURN.js"] + +["browser_flexbox_highlighter_opened_telemetry.js"] + +["browser_flexbox_item_list_01.js"] + +["browser_flexbox_item_list_02.js"] + +["browser_flexbox_item_list_updates_on_change.js"] + +["browser_flexbox_item_outline_exists.js"] + +["browser_flexbox_item_outline_has_correct_layout.js"] + +["browser_flexbox_item_outline_hidden_when_useless.js"] + +["browser_flexbox_item_outline_renders_basisfinal_points_correctly.js"] + +["browser_flexbox_item_outline_rotates_for_column.js"] + +["browser_flexbox_item_outline_rotates_for_different_writing_modes.js"] + +["browser_flexbox_non_flex_item_is_not_shown.js"] + +["browser_flexbox_pseudo_elements_are_listed.js"] + +["browser_flexbox_sizing_flexibility_not_displayed_when_useless.js"] + +["browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js"] + +["browser_flexbox_sizing_info_exists.js"] + +["browser_flexbox_sizing_info_for_different_writing_modes.js"] + +["browser_flexbox_sizing_info_for_pseudos.js"] + +["browser_flexbox_sizing_info_for_text_nodes.js"] + +["browser_flexbox_sizing_info_has_correct_sections.js"] + +["browser_flexbox_sizing_info_matches_properties_with_!important.js"] + +["browser_flexbox_sizing_info_updates_on_change.js"] + +["browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js"] + +["browser_flexbox_text_nodes_are_listed.js"] + +["browser_flexbox_text_nodes_are_not_inlined.js"] + +["browser_flexbox_toggle_flexbox_highlighter_01.js"] + +["browser_flexbox_toggle_flexbox_highlighter_02.js"] diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js new file mode 100644 index 0000000000..eb4acd8a1b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexbox accordions state is persistent through hide/show in the layout +// view. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; +const ACCORDION_HEADER_SELECTOR = ".accordion-header"; +const ACCORDION_CONTENT_SELECTOR = ".accordion-content"; + +add_task(async function () { + await addTab(TEST_URI); + + await testAccordionState( + ":root", + FLEXBOX_OPENED_PREF, + "#layout-section-flex" + ); + await testAccordionState( + "#container-only", + FLEX_CONTAINER_OPENED_PREF, + "#layout-section-flex-container" + ); + await testAccordionState( + "#item-only", + FLEX_ITEM_OPENED_PREF, + "#layout-section-flex-item" + ); +}); + +async function testAccordionState(target, pref, selector) { + const context = await openLayoutViewAndSelectNode(target); + + await testAccordionStateAfterClickingHeader(pref, selector, context); + await testAccordionStateAfterSwitchingSidebars(pref, selector, context); + await testAccordionStateAfterReopeningLayoutView(pref, selector, context); + + Services.prefs.clearUserPref(pref); +} + +async function testAccordionStateAfterClickingHeader(pref, selector, { doc }) { + info("Checking initial state of the flexbox panel."); + + const item = await waitFor(() => doc.querySelector(selector)); + const header = item.querySelector(ACCORDION_HEADER_SELECTOR); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok( + !content.hidden && content.childElementCount > 0, + "The flexbox panel content is visible." + ); + ok(Services.prefs.getBoolPref(pref), `${pref} is pref on by default.`); + + info("Clicking the flexbox header to hide the flexbox panel."); + header.click(); + + info("Checking the new state of the flexbox panel."); + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function testAccordionStateAfterSwitchingSidebars( + pref, + selector, + { doc, inspector } +) { + info( + "Checking the flexbox accordion state is persistent after switching sidebars." + ); + + const item = await waitFor(() => doc.querySelector(selector)); + + info("Selecting the computed view."); + inspector.sidebar.select("computedview"); + + info("Selecting the layout view."); + inspector.sidebar.select("layoutview"); + + info("Checking the state of the flexbox panel."); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function testAccordionStateAfterReopeningLayoutView( + pref, + selector, + { target, toolbox } +) { + info( + "Checking the flexbox accordion state is persistent after closing and re-opening the layout view." + ); + + info("Closing the toolbox."); + await toolbox.destroy(); + + info("Re-opening the layout view."); + const { doc } = await openLayoutViewAndSelectNode(target); + const item = await waitFor(() => doc.querySelector(selector)); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking the state of the flexbox panel."); + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function openLayoutViewAndSelectNode(target) { + const { inspector, flexboxInspector, toolbox } = await openLayoutView(); + await selectNode(target, inspector); + + return { + doc: flexboxInspector.document, + inspector, + target, + toolbox, + }; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js new file mode 100644 index 0000000000..d77ec6d9e4 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container accordion and flex item accordion are both rendered when +// the selected element is both a flex container and item. + +const TEST_URI = ` + +
    +
    +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onAccordionsRendered = waitForDOM(doc, ".accordion-item", 4); + await selectNode("#item", inspector); + const [flexItemPane, flexContainerPane] = await onAccordionsRendered; + + ok(flexItemPane, "The flex item accordion pane is rendered."); + ok(flexContainerPane, "The flex container accordion pane is rendered."); + is( + flexItemPane.children[0].textContent, + "Flex Item of div#container.container", + "Got the correct header for the flex item pane." + ); + is( + flexContainerPane.children[0].textContent, + "Flex Container", + "Got the correct header for the flex container pane." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js new file mode 100644 index 0000000000..0339845618 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the order in which accordion items are shown for container-item elements. +// For those combined types, the container accordion is shown first if the selection came +// from the markup-view, because we assume in this case that users do want to see the +// element selected as a container first. +// However when users select an item in the list of items in the container accordion (or +// in the item selector dropdown), then the item accordion should be shown first. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select a flex container-only node"); + await selectNode("#container-only", inspector); + await waitUntil( + () => doc.querySelectorAll(".flex-header-container-properties").length + ); + + info("Check that there is only 1 accordion for displayed"); + let accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 1, "There's only 1 accordion"); + is( + accordions[0].id, + "layout-section-flex-container", + "The accordion is the container type" + ); + + info("Select a flex container+item node by clicking in the markup-view"); + await clickOnNodeInMarkupView("#container-and-item", inspector); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 2); + + info( + "Check that the 2 accordions are displayed, with container type being first" + ); + accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 2, "There are 2 accordions"); + is( + accordions[0].id, + "layout-section-flex-container", + "The first accordion is the container type" + ); + is( + accordions[1].id, + "layout-section-flex-item", + "The second accordion is the item type" + ); + + info("Select the container-only node again"); + await selectNode("#container-only", inspector); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 1); + + info("Wait until the accordion item list points to the correct item"); + await waitUntil(() => + doc + .querySelector(".flex-item-list button") + .textContent.includes("container-and-item") + ); + info( + "Click on the container+item node right there in the accordion item list" + ); + doc.querySelector(".flex-item-list button").click(); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 2); + + info( + "Check that the 2 accordions are displayed again, with item type being first" + ); + accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 2, "There are 2 accordions again"); + is( + accordions[0].id, + "layout-section-flex-item", + "The first accordion is the item type" + ); + is( + accordions[1].id, + "layout-section-flex-container", + "The second accordion is the container type" + ); +}); + +async function clickOnNodeInMarkupView(selector, inspector) { + const { selection, markup } = inspector; + + await markup.expandAll(selection.nodeFront); + const nodeFront = await getNodeFront(selector, inspector); + const markupContainer = markup.getContainer(nodeFront); + + const onSelected = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter( + markupContainer.tagLine, + { type: "mousedown" }, + markup.doc.defaultView + ); + EventUtils.synthesizeMouseAtCenter( + markupContainer.tagLine, + { type: "mouseup" }, + markup.doc.defaultView + ); + await onSelected; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js new file mode 100644 index 0000000000..af62ac2ff0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container accordion is rendered when a flex item is updated to +// also be a flex container. + +const TEST_URI = ` + +
    +
    +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("#item", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + ok(flexSizingContainer, "The flex sizing info is rendered."); + + info("Changing the flexbox in the page."); + const onAccordionsChanged = waitForDOM(doc, ".accordion-item", 4); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => (content.document.getElementById("item").className = "container") + ); + const [flexItemPane, flexContainerPane] = await onAccordionsChanged; + + ok(flexItemPane, "The flex item accordion pane is rendered."); + ok(flexContainerPane, "The flex container accordion pane is rendered."); + is( + flexItemPane.children[0].textContent, + "Flex Item of div#container.container", + "Got the correct header for the flex item pane." + ); + is( + flexContainerPane.children[0].textContent, + "Flex Container", + "Got the correct header for the flex container pane." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js new file mode 100644 index 0000000000..d2d83527b9 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container's element rep will display the box model highlighter on +// hover. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + const onFlexContainerRepRendered = waitForDOM( + doc, + ".flex-header-content .objectBox" + ); + await selectNode("#container", inspector); + const [flexContainerRep] = await onFlexContainerRepRendered; + + ok(flexContainerRep, "The flex container element rep is rendered."); + + info("Listen to node-highlight event and mouse over the rep"); + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + EventUtils.synthesizeMouse( + flexContainerRep, + 10, + 5, + { type: "mouseover" }, + doc.defaultView + ); + const { nodeFront } = await onHighlight; + + ok(nodeFront, "nodeFront was returned from highlighting the node."); + is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName."); + is( + nodeFront.attributes[0].name, + "id", + "The highlighted node has the correct attributes." + ); + is( + nodeFront.attributes[0].value, + "container", + "The highlighted node has the correct id." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js new file mode 100644 index 0000000000..cf75d74ffe --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container properties are shown when a flex container is selected. + +const TEST_URI = ` + +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Selecting the flex container #container1 and checking the values of the flex " + + "container properties for #container1." + ); + const onFlexContainerPropertiesRendered = waitForDOM( + doc, + ".flex-header-container-properties" + ); + await selectNode("#container1", inspector); + const [flexContainerProperties] = await onFlexContainerPropertiesRendered; + + ok(flexContainerProperties, "The flex container properties is rendered."); + is( + flexContainerProperties.children[0].textContent, + "row", + "Got expected flex-direction." + ); + is( + flexContainerProperties.children[1].textContent, + "nowrap", + "Got expected flex-wrap." + ); + + info( + "Selecting a flex item and expecting the flex container properties to not be " + + "shown." + ); + const onFlexHeaderRendered = waitForDOM(doc, ".flex-header"); + await selectNode("#item", inspector); + const [flexHeader] = await onFlexHeaderRendered; + + ok( + !flexHeader.querySelector(".flex-header-container-properties"), + "The flex container properties is not shown in the header." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js new file mode 100644 index 0000000000..82e523ebe0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a message is displayed when no flex container is selected. + +const TEST_URI = ` +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Checking the initial state of the Flexbox Inspector."); + ok( + doc.querySelector( + ".flex-accordion .devtools-sidepanel-no-result", + "A message is shown when no flex container is selected." + ) + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js b/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js new file mode 100644 index 0000000000..8617def08b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex container is not shown as a flex item of its grandparent flex +// container. + +const TEST_URI = ` + +
    +
    +
    + This is a flex item of a flex container. + Its parent isn't a flex container, but its grandparent is. +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the flex container's grandchild."); + const onFlexContainerHeaderRendered = waitForDOM( + doc, + ".flex-header-container-label" + ); + await selectNode("#grandchild", inspector); + await onFlexContainerHeaderRendered; + + info("Check that only the Flex Container accordion item is showing."); + const flexPanes = doc.querySelectorAll(".flex-accordion"); + is( + flexPanes.length, + 1, + "There should only be one flex accordion item showing." + ); + + info("Check that the container header shows Flex Container."); + const flexAccordionHeader = flexPanes[0].querySelector( + ".accordion-header-label" + ); + is( + flexAccordionHeader.textContent, + "Flex Container", + "The flexbox pane shows a flex container accordion item." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js new file mode 100644 index 0000000000..59b24ba512 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +// Test that the flexbox highlighter color change in the color picker is reverted when +// ESCAPE is pressed. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + // Make sure there are no custom highlighter colors stored before starting. + await asyncStorage.removeItem("flexboxInspectorHostColors"); + + await addTab(TEST_URI); + const { inspector, flexboxInspector, layoutView } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const cPicker = layoutView.swatchColorPickerTooltip; + const spectrum = cPicker.spectrum; + + const onColorSwatchRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .layout-color-swatch" + ); + await selectNode("#container", inspector); + const [swatch] = await onColorSwatchRendered; + + info("Checking the initial state of the Flexbox Inspector color picker."); + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background is correct." + ); + is( + store.getState().flexbox.color, + "#9400FF", + "The flexbox color state is correct." + ); + + info("Opening the color picker by clicking on the color swatch."); + const onColorPickerReady = cPicker.once("ready"); + swatch.click(); + await onColorPickerReady; + + await simulateColorPickerChange(cPicker, [0, 255, 0, 0.5]); + + is( + swatch.style.backgroundColor, + "rgba(0, 255, 0, 0.5)", + "The color swatch's background was updated." + ); + + info("Pressing ESCAPE to close the tooltip."); + const onColorUpdate = waitUntilState( + store, + state => state.flexbox.color === "#9400FF" + ); + const onColorPickerHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "ESCAPE"); + await onColorPickerHidden; + await onColorUpdate; + + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background was reverted after ESCAPE." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js new file mode 100644 index 0000000000..46c6a7022c --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +// Test that the flexbox highlighter color change in the color picker is committed when +// RETURN is pressed. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + // Make sure there are no custom highlighter colors stored before starting. + await asyncStorage.removeItem("flexboxInspectorHostColors"); + + await addTab(TEST_URI); + const { inspector, flexboxInspector, layoutView } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + const cPicker = layoutView.swatchColorPickerTooltip; + const spectrum = cPicker.spectrum; + + const onColorSwatchRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .layout-color-swatch" + ); + await selectNode("#container", inspector); + const [swatch] = await onColorSwatchRendered; + + const checkbox = doc.getElementById("flexbox-checkbox-toggle"); + + info("Checking the initial state of the Flexbox Inspector color picker."); + ok(!checkbox.checked, "Flexbox highlighter toggle is unchecked."); + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background is correct." + ); + is( + store.getState().flexbox.color, + "#9400FF", + "The flexbox color state is correct." + ); + + info("Toggling ON the flexbox highlighter."); + const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + const onCheckboxChange = waitUntilState( + store, + state => state.flexbox.highlighted + ); + checkbox.click(); + await onHighlighterShown; + await onCheckboxChange; + + info("Opening the color picker by clicking on the color swatch."); + const onColorPickerReady = cPicker.once("ready"); + swatch.click(); + await onColorPickerReady; + + await simulateColorPickerChange(cPicker, [0, 255, 0, 0.5]); + + is( + swatch.style.backgroundColor, + "rgba(0, 255, 0, 0.5)", + "The color swatch's background was updated." + ); + + info("Pressing RETURN to commit the color change."); + const onColorUpdate = waitUntilState( + store, + state => state.flexbox.color === "#00FF0080" + ); + const onColorPickerHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + await onColorPickerHidden; + await onColorUpdate; + + is( + swatch.style.backgroundColor, + "rgba(0, 255, 0, 0.5)", + "The color swatch's background was kept after RETURN." + ); + + info("Toggling OFF the flexbox highlighter."); + const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + checkbox.click(); + await onHighlighterHidden; +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js new file mode 100644 index 0000000000..62c63e8b9f --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry is correct when the flexbox highlighter is activated from +// the layout view. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + startTelemetry(); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + await toggleHighlighterON(flexHighlighterToggle, inspector); + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.layout.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry( + "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", + "", + null, + "hasentries" + ); +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js new file mode 100644 index 0000000000..d7a8ae184a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +// Test the flex item list is empty when there are no flex items for the selected flex +// container. + +const TEST_URI = ` +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter } = getHighlighterTestHelpers(inspector); + + const onFlexHeaderRendered = waitForDOM(doc, ".flex-header"); + await selectNode("#container", inspector); + const [flexHeader] = await onFlexHeaderRendered; + const flexHighlighterToggle = flexHeader.querySelector( + "#flexbox-checkbox-toggle" + ); + const flexItemListHeader = doc.querySelector(".flex-item-list-header"); + + info("Checking the state of the Flexbox Inspector."); + ok(flexHeader, "The flex container header is rendered."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + is( + flexItemListHeader.textContent, + getStr("flexbox.noFlexItems"), + "The flex item list header shows 'No flex items' when there are no items." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js new file mode 100644 index 0000000000..5433727c6a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item list can be used to navigated to the selected flex item. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemListRendered = waitForDOM(doc, ".flex-item-list"); + await selectNode("#container", inspector); + const [flexItemList] = await onFlexItemListRendered; + + info("Checking the initial state of the flex item list."); + ok(flexItemList, "The flex item list is rendered."); + is( + flexItemList.querySelectorAll("button").length, + 1, + "Got the correct number of flex items in the list." + ); + + info("Clicking on the first flex item to navigate to the flex item."); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + flexItemList.querySelector("button").click(); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + info("Checking the selected flex item state."); + ok(flexOutlineContainer, "The flex outline is rendered."); + ok(!doc.querySelector(".flex-item-list"), "The flex item list is not shown."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js new file mode 100644 index 0000000000..95cbb2da2d --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item list updates on changes to the number of flex items in the +// flex container. + +const TEST_URI = ` + +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemListRendered = waitForDOM(doc, ".flex-item-list"); + await selectNode("#container", inspector); + const [flexItemList] = await onFlexItemListRendered; + + info("Checking the initial state of the flex item list."); + ok(flexItemList, "The flex item list is rendered."); + is( + flexItemList.querySelectorAll("button").length, + 1, + "Got the correct number of flex items in the list." + ); + + info("Changing the flexbox in the page."); + const onFlexItemListChanged = waitForDOM(doc, ".flex-item-list > button", 2); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const div = content.document.createElement("div"); + content.document.getElementById("container").appendChild(div); + }); + const elements = await onFlexItemListChanged; + + info("Checking the flex item list is correct."); + is(elements.length, 2, "Flex item list was changed."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js new file mode 100644 index 0000000000..eba6ec03f0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline exists when a flex item is selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the test document and wait for the outline to be rendered. + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(".item", inspector); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + + ok(basis, "The basis outline exists"); + ok(final, "The final outline exists"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js new file mode 100644 index 0000000000..e81d6f5677 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline has a correct layout. This outline is built using css +// grid under the hood to position everything. So we want to check that the template for +// this grid has been correctly generated depending on the item that is currently +// selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +const TEST_DATA = [ + { + selector: ".shrinking .item", + expectedGridTemplate: + "[basis-start final-start] 300fr [final-end delta-start] " + + "200fr [basis-end delta-end]", + }, + { + selector: ".shrinking.is-clamped .item", + expectedGridTemplate: + "[basis-start final-start] 300fr [delta-start] " + + "50fr [final-end min] 150fr [basis-end delta-end]", + }, + { + selector: ".growing .item", + expectedGridTemplate: + "[basis-start final-start] 200fr [basis-end delta-start] " + + "100fr [final-end delta-end]", + }, + { + selector: ".growing.is-clamped .item", + expectedGridTemplate: + "[basis-start final-start] 200fr [basis-end delta-start] " + + "50fr [final-end max] 50fr [delta-end]", + }, + { + selector: "#wanted-to-shrink-more-than-basis div:first-child", + expectedGridTemplate: + "[delta-start] 63fr [basis-start final-start] " + + "60fr [final-end min] 140fr [basis-end delta-end]", + }, +]; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + for (const { selector, expectedGridTemplate } of TEST_DATA) { + info( + `Checking the grid template for the flex item outline for ${selector}` + ); + + await selectNode(selector, inspector); + await waitUntil(() => { + const flexOutline = doc.querySelector(".flex-outline"); + return ( + flexOutline && + flexOutline.style.gridTemplateColumns === expectedGridTemplate + ); + }); + + ok(true, "Grid template is correct"); + } +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js new file mode 100644 index 0000000000..e0c1cab942 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is not rendered when it isn't useful. +// For now, that means when the item's base, delta and final sizes are all 0. + +const TEST_URI = ` +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Select the item in the document and wait for the sizing section to appear" + ); + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("div > div", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + const outlineEls = doc.querySelectorAll(".flex-outline-container"); + is(outlineEls.length, 0, "The outline has not been rendered for this item"); + + info("Also check that the sizing section shows the correct information"); + const allSections = [ + ...flexSizingContainer.querySelectorAll(".section .name"), + ]; + + is(allSections.length, 2, "There are 2 parts in the sizing section"); + is( + allSections[0].textContent, + "Content Size", + "The first part is the content size" + ); + is( + allSections[1].textContent, + "Final Size", + "The second part is the final size" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js new file mode 100644 index 0000000000..d2a2ce94fd --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline renders the basis and final points as a single point +// if their sizes are equal. If not, then render as separate points. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + info("Select a flex item whose basis size matches its final size."); + let onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(".item", inspector); + await onUpdate; + + const [basisFinalPoint] = [ + ...doc.querySelectorAll(".flex-outline-point.basisfinal"), + ]; + + ok(basisFinalPoint, "The basis/final point exists"); + + info("Select a flex item whose basis size is different than its final size."); + onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(".shrinking .item", inspector); + await onUpdate; + + const [basis, final] = [ + ...doc.querySelectorAll( + ".flex-outline-point.basis, .flex-outline-point.final" + ), + ]; + + ok(basis, "The basis point exists"); + ok(final, "The final point exists"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js new file mode 100644 index 0000000000..f26ca751cd --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is rotated for flex items in a column flexbox layout. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the row flexbox layout. + let onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".container .item", inspector); + let [flexOutline] = await onFlexItemOutlineRendered; + + ok( + flexOutline.classList.contains("horizontal-lr"), + "The flex outline has the horizontal-lr class" + ); + + // Check that the outline is wider than it is tall in the configuration. + let bounds = flexOutline.getBoxQuads()[0].getBounds(); + Assert.greater(bounds.width, bounds.height, "The outline looks like a row"); + + // Select a flex item in the column flexbox layout. + onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".container.column .item", inspector); + await waitUntil(() => { + flexOutline = doc.querySelector( + ".flex-outline-container .flex-outline.vertical-tb" + ); + return flexOutline; + }); + ok(true, "The flex outline has the vertical-tb class"); + + // Check that the outline is taller than it is wide in the configuration. + bounds = flexOutline.getBoxQuads()[0].getBounds(); + Assert.greater( + bounds.height, + bounds.width, + "The outline looks like a column" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js new file mode 100644 index 0000000000..860f2c2337 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is rotated to match its main axis direction. + +const TEST_URI = URL_ROOT + "doc_flexbox_writing_modes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Check that the row flex item rotated to vertical-bt."); + let onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".row.vertical-bt.item", inspector); + let [flexOutline] = await onFlexItemOutlineRendered; + + ok( + flexOutline.classList.contains("vertical-bt"), + "Row outline has been rotated to vertical-bt." + ); + + info("Check that the column flex item rotated to horizontal-rl."); + onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".column.horizontal-rl.item", inspector); + await waitUntil(() => { + flexOutline = doc.querySelector( + ".flex-outline-container .flex-outline.horizontal-rl" + ); + return flexOutline; + }); + + ok(true, "Column outline has been rotated to horizontal-rl."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js b/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js new file mode 100644 index 0000000000..92422ea490 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that an element that is the child of a flex container but is not actually a flex +// item is not shown in the sidebar when selected. + +const TEST_URI = ` +
    +
    test
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the container's supposed flex item."); + await selectNode("#item", inspector); + const noFlexContainerOrItemSelected = doc.querySelector( + ".flex-accordion .devtools-sidepanel-no-result" + ); + + ok( + noFlexContainerOrItemSelected, + "The flexbox pane shows a message to select a flex container or item to continue." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js b/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js new file mode 100644 index 0000000000..c69e4c92b6 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that pseudo-elements that are flex items do appear in the list of items. + +const TEST_URI = URL_ROOT + "doc_flexbox_pseudos.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 2, "There are 2 items displayed in the list"); + + is(items[0].textContent, "::before", "The first item is ::before"); + is(items[1].textContent, "::after", "The second item is ::after"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js new file mode 100644 index 0000000000..e25ebd5a6a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexibility section in the flex item sizing properties is not displayed +// when the item did not grow or shrink. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + info( + "Select an item with flex:0 and wait for the sizing info to be rendered" + ); + let onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode("#did-not-grow-or-shrink div", inspector); + await onUpdate; + + let flexSections = doc.querySelectorAll( + ".flex-item-sizing .section.flexibility" + ); + is( + flexSections.length, + 0, + "The flexibility section was not found in the DOM" + ); + + info( + "Select a more complex item which also doesn't flex and wait for the sizing info" + ); + onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode( + "#just-enough-space-for-clamped-items div:last-child", + inspector + ); + await onUpdate; + + flexSections = doc.querySelectorAll(".flex-item-sizing .section.flexibility"); + is( + flexSections.length, + 0, + "The flexibility section was not found in the DOM" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js new file mode 100644 index 0000000000..45cae70d7b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex item's min width/height value is not displayed if it's unspecified in +// the CSS. + +const TEST_URI = URL_ROOT + "doc_flexbox_unauthored_min_dimension.html"; + +async function checkFlexItemCSSProperty(inspector, store, doc, selector) { + info("Select the container's flex item sizing info."); + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info( + "Check that the minimum size section does not display minimum dimension text." + ); + const [sectionMinRowItem] = [ + ...doc.querySelectorAll(".flex-item-sizing .section.min"), + ]; + const minDimension = sectionMinRowItem.querySelector(".css-property-link"); + + ok(!minDimension, "Minimum dimension property should not be displayed."); +} + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + await checkFlexItemCSSProperty( + inspector, + store, + doc, + "#flex-item-with-unauthored-min-width" + ); + await checkFlexItemCSSProperty( + inspector, + store, + doc, + "#flex-item-with-unauthored-min-height" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js new file mode 100644 index 0000000000..e353eea0db --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing information exists when a flex item is selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the test document and wait for the sizing info to be rendered. + // Note that we select an item that has base, delta and final sizes, so we can check + // those sections exists. + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode(".container.growing .item", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + + info("Check that the base, flexibility and final sizes are displayed"); + const allSections = [ + ...flexSizingContainer.querySelectorAll(".section .name"), + ]; + const allSectionTitles = allSections.map(el => el.textContent); + + ["Base Size", "Flexibility", "Final Size"].forEach((expectedTitle, i) => { + ok( + allSectionTitles[i].includes(expectedTitle), + `Sizing section #${i + 1} (${expectedTitle}) was found` + ); + }); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js new file mode 100644 index 0000000000..91a99d3da5 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing info shows the correct dimension values for different +// writing modes. For vertical writing modes, row items should display height values and +// column items should display width values. The opposite is true for horizontal mode +// where rows display width values and columns display height. + +const TEST_URI = URL_ROOT + "doc_flexbox_writing_modes.html"; + +async function checkFlexItemDimension( + inspector, + store, + doc, + selector, + expected +) { + info("Select the container's flex item."); + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info("Check that the minimum size section shows the correct dimension."); + const sectionMinRowItem = doc.querySelector(".flex-item-sizing .section.min"); + const minDimension = sectionMinRowItem.querySelector(".css-property-link"); + + ok( + minDimension.textContent.includes(expected), + "The flex item sizing has the correct dimension value." + ); +} + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.vertical-rl.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".column.vertical-tb.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.vertical-bt.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".column.horizontal-rl.item", + "min-width" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.horizontal-lr.item", + "min-width" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js new file mode 100644 index 0000000000..75b3a00a4f --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI also appears for pseudo elements. + +const TEST_URI = URL_ROOT + "doc_flexbox_pseudos.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the ::before pseudo-element in the inspector"); + const containerNode = await getNodeFront(".container", inspector); + const { nodes } = await inspector.walker.children(containerNode); + const beforeNode = nodes[0]; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(beforeNode, inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + info("Check that the various sizing sections are displayed"); + const allSections = [...flexSizingContainer.querySelectorAll(".section")]; + ok(allSections.length, "Sizing sections are displayed"); + + info("Check that the various parts of the outline are displayed"); + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + ok(basis && final, "The final and basis parts of the outline exist"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js new file mode 100644 index 0000000000..fc96505208 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI also appears for text nodes. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the first text node in the flex container"); + const containerNode = await getNodeFront(".container", inspector); + const { nodes } = await inspector.walker.children(containerNode); + const firstTextNode = nodes[0]; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(firstTextNode, inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + info("Check that the various sizing sections are displayed"); + const allSections = [...flexSizingContainer.querySelectorAll(".section")]; + ok(allSections.length, "Sizing sections are displayed"); + + info("Check that the various parts of the outline are displayed"); + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + ok(basis && final, "The final and basis parts of the outline exist"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js new file mode 100644 index 0000000000..e31366da15 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI contains the right sections, depending on which +// element is selected. Some items may be clamped, others not, so not all sections are +// visible at all times. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +const TEST_DATA = [ + { + selector: ".shrinking .item", + expectedSections: ["Base Size", "Flexibility", "Final Size"], + }, + { + selector: ".shrinking.is-clamped .item", + expectedSections: [ + "Base Size", + "Flexibility", + "Minimum Size", + "Final Size", + ], + }, + { + selector: ".growing .item", + expectedSections: ["Base Size", "Flexibility", "Final Size"], + }, + { + selector: ".growing.is-clamped .item", + expectedSections: [ + "Base Size", + "Flexibility", + "Maximum Size", + "Final Size", + ], + }, +]; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + for (const { selector, expectedSections } of TEST_DATA) { + info(`Checking the list of sections for the flex item ${selector}`); + const sections = await selectNodeAndGetFlexSizingSections( + selector, + store, + inspector, + doc + ); + + is( + sections.length, + expectedSections.length, + "Correct number of sections found" + ); + expectedSections.forEach((expectedSection, i) => { + ok( + sections[i].includes(expectedSection), + `The ${expectedSection} section was found` + ); + }); + } +}); + +async function selectNodeAndGetFlexSizingSections( + selector, + store, + inspector, + doc +) { + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info(`Getting the list of displayed sections for ${selector}`); + const allSections = [ + ...doc.querySelectorAll("ul.flex-item-sizing .section .name"), + ]; + const allSectionTitles = allSections.map(el => el.textContent); + + return allSectionTitles; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js new file mode 100644 index 0000000000..ba14ef7a62 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex item's sizing section shows the highest value of specificity for its +// CSS property. + +const TEST_URI = URL_ROOT + "doc_flexbox_CSS_property_with_!important.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the container's flex item sizing info."); + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode(".item", inspector); + const [flexItemSizingContainer] = await onFlexItemSizingRendered; + + info( + "Check that the max and flexibility sections show correct CSS property value." + ); + const flexSection = flexItemSizingContainer.querySelector( + ".section.flexibility" + ); + const maxSection = flexItemSizingContainer.querySelector(".section.max"); + const flexGrow = flexSection.querySelector(".css-property-link"); + const maxSize = maxSection.querySelector(".css-property-link"); + + is( + maxSize.textContent, + "(max-width: 400px !important)", + "Maximum size section shows CSS property value with highest specificity." + ); + is( + flexGrow.textContent, + "(flex-grow: 5 !important)", + "Flexibility size section shows CSS property value with highest specificity." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js new file mode 100644 index 0000000000..9218022b02 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexbox sizing info updates on changes to the flex item properties. + +const TEST_URI = ` + +
    +
    +
    +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("#item", inspector); + const [flexItemSizingContainer] = await onFlexItemSizingRendered; + + info("Checking the initial state of the flex item list."); + is( + flexItemSizingContainer.querySelectorAll("li").length, + 2, + "Got the correct number of flex item sizing properties in the list." + ); + + info("Changing the flexbox in the page."); + const onFlexItemSizingChanged = waitForDOM( + doc, + "ul.flex-item-sizing > li", + 3 + ); + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => (content.document.getElementById("item").style.minWidth = "100px") + ); + const elements = await onFlexItemSizingChanged; + + info("Checking the flex item sizing info is correct."); + is(elements.length, 3, "Flex item sizing info was changed."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js new file mode 100644 index 0000000000..72e2861ef3 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +// Test the specific max-clamping scenario where an item wants to grow a certain amount +// but its max-size prevents it from growing that much. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Select the test item in the document and wait for the sizing info to render" + ); + const onRendered = waitForDOM(doc, ".flex-outline, .flex-item-sizing", 2); + await selectNode("#want-to-grow-more-than-max div", inspector); + const [outlineContainer, sizingContainer] = await onRendered; + + info( + "Check that the outline contains the max point and that it's equal to final" + ); + const maxPoint = outlineContainer.querySelector(".flex-outline-point.max"); + ok(maxPoint, "The max point is displayed"); + ok( + outlineContainer.style.gridTemplateColumns.includes("[final-end max]"), + "The final and max points are at the same position" + ); + + info("Check that the maximum sizing section displays the right info"); + const reasons = [...sizingContainer.querySelectorAll(".reasons li")]; + const expectedReason = getStr("flexbox.itemSizing.clampedToMax"); + ok( + reasons.some(r => r.textContent === expectedReason), + "The clampedToMax reason was found" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js new file mode 100644 index 0000000000..374ec77962 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that text nodes that are flex items do appear in the list of items. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 3, "There are 3 items displayed in the list"); + + is(items[0].textContent, "#text", "The first item is a text node"); + is(items[2].textContent, "#text", "The third item is a text node"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js new file mode 100644 index 0000000000..2494ac1dcb --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that single child text nodes that are also flex items can be selected in the +// flexbox inspector. +// This means that they are not inlined like normal single child text nodes, since +// selecting them in the flexbox inspector also means selecting them in the markup view. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container.single-child", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 1, "There is 1 item displayed in the list"); + is(items[0].textContent, "#text", "The item in the list is a text node"); + + info("Click on the item to select it"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + items[0].closest("button").click(); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + ok( + flexOutlineContainer, + "The flex outline is displayed for a single child short text node too" + ); + + ok( + inspector.selection.isTextNode(), + "The current inspector selection is the text node" + ); + + const markupContainer = inspector.markup.getContainer( + inspector.selection.nodeFront + ); + is( + markupContainer.elt.textContent, + "short text", + "This is the right text node" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js new file mode 100644 index 0000000000..2fbfccb162 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling ON/OFF the flexbox highlighter from the flexbox inspector panel. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter } = getHighlighterTestHelpers(inspector); + + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + info("Checking the initial state of the Flexbox Inspector."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created."); + ok(getActiveHighlighter(HIGHLIGHTER_TYPE), "Flexbox highlighter is shown."); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is not shown."); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js new file mode 100644 index 0000000000..c7d7c35d9a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling ON/OFF the flexbox highlighter on different flex containers from the +// flexbox inspector panel. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter, getNodeForActiveHighlighter } = + getHighlighterTestHelpers(inspector); + + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + info("Checking the #container state of the Flexbox Inspector."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + + info( + "Toggling ON the flexbox highlighter for #container from the layout panel." + ); + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created for #container."); + const highlightedNodeFront = store.getState().flexbox.flexContainer.nodeFront; + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + highlightedNodeFront, + "Flexbox highlighter is shown for #container." + ); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + info("Switching the selected flex container to .container.column"); + const onToggleChange = waitUntilState( + store, + state => !state.flexbox.highlighted + ); + await selectNode(".container.column", inspector); + await onToggleChange; + + info("Checking the .container.column state of the Flexbox Inspector."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + highlightedNodeFront, + "Flexbox highlighter is still shown for #container." + ); + + info( + "Toggling ON the flexbox highlighter for .container.column from the layout " + + "panel." + ); + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created for .container.column"); + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + store.getState().flexbox.flexContainer.nodeFront, + "Flexbox highlighter is shown for .container.column." + ); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is not shown."); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html b/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html new file mode 100644 index 0000000000..097d91215b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html @@ -0,0 +1,22 @@ + + + +
    +
    Item
    +
    diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html b/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html new file mode 100644 index 0000000000..76474cbc37 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html @@ -0,0 +1,27 @@ + + + +
    diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html b/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html new file mode 100644 index 0000000000..f5a6aaad24 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html @@ -0,0 +1,121 @@ + + + +
    +
    flex item in a row flex container
    +
    +
    +
    flex item in a column flex container
    +
    +
    +
    Shrinking flex item
    +
    +
    +
    Shrinking and clamped flex item
    +
    +
    +
    Growing flex item
    +
    +
    +
    Growing and clamped flex item
    +
    +
    +
    item wants to grow more
    +
    +
    +
    item did not grow or shrink
    +
    +
    +
    +
    +
    +
    +
    item wants to shrink more than its basis
    +
    +
    +
    +
    +
    This item is inside a container-item element
    +
    +
    diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html b/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html new file mode 100644 index 0000000000..626d3aa47e --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html @@ -0,0 +1,20 @@ + + + +
    + A text node will be wrapped into an anonymous block container +
    + Here is yet another text node +
    +
    short text
    diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html b/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html new file mode 100644 index 0000000000..2f05dbb7f8 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html @@ -0,0 +1,29 @@ + + + +
    +
    +
    +

    AAA

    +
    +
    +
    +
    +
    +

    BBB

    +
    +
    diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html b/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html new file mode 100644 index 0000000000..b518c39631 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html @@ -0,0 +1,40 @@ + + + +
    + Vertical-tb Row content +
    +
    + Vertical-tb Column Content +
    +
    + Vertical-bt Row Content +
    +
    + Horizontal-rl Column Content +
    +
    + Horizontal-lr Row Content +
    diff --git a/devtools/client/inspector/flexbox/test/head.js b/devtools/client/inspector/flexbox/test/head.js new file mode 100644 index 0000000000..269dcea904 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/head.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +const FLEXBOX_OPENED_PREF = "devtools.layout.flexbox.opened"; +const FLEX_CONTAINER_OPENED_PREF = "devtools.layout.flex-container.opened"; +const FLEX_ITEM_OPENED_PREF = "devtools.layout.flex-item.opened"; +const GRID_OPENED_PREF = "devtools.layout.grid.opened"; +const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened"; + +// Make sure only the flexbox layout accordions are opened, and the others are closed. +Services.prefs.setBoolPref(FLEXBOX_OPENED_PREF, true); +Services.prefs.setBoolPref(FLEX_CONTAINER_OPENED_PREF, true); +Services.prefs.setBoolPref(FLEX_ITEM_OPENED_PREF, true); +Services.prefs.setBoolPref(BOXMODEL_OPENED_PREF, false); +Services.prefs.setBoolPref(GRID_OPENED_PREF, false); + +// Clear all set prefs. +registerCleanupFunction(() => { + Services.prefs.clearUserPref(FLEXBOX_OPENED_PREF); + Services.prefs.clearUserPref(FLEX_CONTAINER_OPENED_PREF); + Services.prefs.clearUserPref(FLEX_ITEM_OPENED_PREF); + Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF); + Services.prefs.clearUserPref(GRID_OPENED_PREF); +}); + +/** + * Toggles ON the flexbox highlighter given the flexbox highlighter button from the + * layout panel. + * + * @param {DOMNode} button + * The flexbox highlighter toggle button in the flex container panel. + * @param {Inspector} inspector + * Inspector panel instance. + */ +async function toggleHighlighterON(button, inspector) { + info("Toggling ON the flexbox highlighter from the layout panel."); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.FLEXBOX + ); + const { store } = inspector; + const onToggleChange = waitUntilState( + store, + state => state.flexbox.highlighted + ); + button.click(); + await Promise.all([onHighlighterShown, onToggleChange]); +} + +/** + * Toggles OFF the flexbox highlighter given the flexbox highlighter button from the + * layout panel. + * + * @param {DOMNode} button + * The flexbox highlighter toggle button in the flex container panel. + * @param {Inspector} inspector + * Inspector panel instance. + */ +async function toggleHighlighterOFF(button, inspector) { + info("Toggling OFF the flexbox highlighter from the layout panel."); + const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector); + const onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.FLEXBOX + ); + const { store } = inspector; + const onToggleChange = waitUntilState( + store, + state => !state.flexbox.highlighted + ); + button.click(); + await Promise.all([onHighlighterHidden, onToggleChange]); +} diff --git a/devtools/client/inspector/flexbox/types.js b/devtools/client/inspector/flexbox/types.js new file mode 100644 index 0000000000..34f45ed8ae --- /dev/null +++ b/devtools/client/inspector/flexbox/types.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +/** + * A flex item computed style properties. + */ +const flexItemProperties = (exports.flexItemProperties = { + // The computed value of flex-basis. + "flex-basis": PropTypes.string, + + // The computed value of flex-grow. + "flex-grow": PropTypes.string, + + // The computed value of min-height. + "min-height": PropTypes.string, + + // The computed value of min-width. + "min-width": PropTypes.string, + + // The computed value of max-height. + "max-height": PropTypes.string, + + // The computed value of max-width. + "max-width": PropTypes.string, + + // The computed height of the flex item element. + height: PropTypes.string, + + // The computed width of the flex item element. + width: PropTypes.string, +}); + +/** + * A flex item sizing data. + */ +const flexItemSizing = (exports.flexItemSizing = { + // The max size of the flex item in the cross axis. + crossMaxSize: PropTypes.number, + + // The min size of the flex item in the cross axis. + crossMinSize: PropTypes.number, + + // The size of the flex item in the main axis before the flex sizing algorithm is + // applied. This is also called the "flex base size" in the spec. + mainBaseSize: PropTypes.number, + + // The value that the flex sizing algorithm "wants" to use to stretch or shrink the + // item, before clamping to the item's main min and max sizes. + mainDeltaSize: PropTypes.number, + + // The max size of the flex item in the main axis. + mainMaxSize: PropTypes.number, + + // The min size of the flex item in the maxin axis. + mainMinSize: PropTypes.number, +}); + +/** + * A flex item data. + */ +const flexItem = (exports.flexItem = { + // The actor ID of the flex item. + actorID: PropTypes.string, + + // The computed style properties of the flex item. + computedStyle: PropTypes.object, + + // The flex item sizing data. + flexItemSizing: PropTypes.shape(flexItemSizing), + + // The NodeFront of the flex item. + nodeFront: PropTypes.object, + + // The authored style properties of the flex item. + properties: PropTypes.shape(flexItemProperties), +}); + +/** + * A flex container computed style properties. + */ +const flexContainerProperties = (exports.flexContainerProperties = { + // The computed value of align-content. + "align-content": PropTypes.string, + + // The computed value of align-items. + "align-items": PropTypes.string, + + // The computed value of flex-direction. + "flex-direction": PropTypes.string, + + // The computed value of flex-wrap. + "flex-wrap": PropTypes.string, + + // The computed value of justify-content. + "justify-content": PropTypes.string, +}); + +/** + * A flex container data. + */ +const flexContainer = (exports.flexContainer = { + // The actor ID of the flex container. + actorID: PropTypes.string, + + // An array of flex items belonging to the flex container. + flexItems: PropTypes.arrayOf(PropTypes.shape(flexItem)), + + // Whether or not the flex container data represents the selected flex container + // or the parent flex container. This is true if the flex container data represents + // the parent flex container. + isFlexItemContainer: PropTypes.bool, + + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: PropTypes.string, + + // The NodeFront of the flex container. + nodeFront: PropTypes.object, + + // The computed style properties of the flex container. + properties: PropTypes.shape(flexContainerProperties), +}); + +/** + * The Flexbox UI state. + */ +exports.flexbox = { + // The color of the flexbox highlighter overlay. + color: PropTypes.string, + + // The selected flex container. + flexContainer: PropTypes.shape(flexContainer), + + // The selected flex container can also be a flex item. This object contains the + // parent flex container properties. + flexItemContainer: PropTypes.shape(flexContainer), + + // Whether or not the flexbox highlighter is highlighting the flex container. + highlighted: PropTypes.bool, +}; diff --git a/devtools/client/inspector/fonts/actions/font-editor.js b/devtools/client/inspector/fonts/actions/font-editor.js new file mode 100644 index 0000000000..0542604d7b --- /dev/null +++ b/devtools/client/inspector/fonts/actions/font-editor.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + APPLY_FONT_VARIATION_INSTANCE, + RESET_EDITOR, + SET_FONT_EDITOR_DISABLED, + UPDATE_AXIS_VALUE, + UPDATE_EDITOR_STATE, + UPDATE_PROPERTY_VALUE, + UPDATE_WARNING_MESSAGE, +} = require("resource://devtools/client/inspector/fonts/actions/index.js"); + +module.exports = { + resetFontEditor() { + return { + type: RESET_EDITOR, + }; + }, + + setEditorDisabled(disabled = false) { + return { + type: SET_FONT_EDITOR_DISABLED, + disabled, + }; + }, + + applyInstance(name, values) { + return { + type: APPLY_FONT_VARIATION_INSTANCE, + name, + values, + }; + }, + + updateAxis(axis, value) { + return { + type: UPDATE_AXIS_VALUE, + axis, + value, + }; + }, + + updateFontEditor(fonts, properties = {}, id = "") { + return { + type: UPDATE_EDITOR_STATE, + fonts, + properties, + id, + }; + }, + + updateFontProperty(property, value) { + return { + type: UPDATE_PROPERTY_VALUE, + property, + value, + }; + }, + + updateWarningMessage(warning) { + return { + type: UPDATE_WARNING_MESSAGE, + warning, + }; + }, +}; diff --git a/devtools/client/inspector/fonts/actions/font-options.js b/devtools/client/inspector/fonts/actions/font-options.js new file mode 100644 index 0000000000..8dfe101edd --- /dev/null +++ b/devtools/client/inspector/fonts/actions/font-options.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_PREVIEW_TEXT, +} = require("resource://devtools/client/inspector/fonts/actions/index.js"); + +module.exports = { + /** + * Update the preview text in the font inspector + */ + updatePreviewText(previewText) { + return { + type: UPDATE_PREVIEW_TEXT, + previewText, + }; + }, +}; diff --git a/devtools/client/inspector/fonts/actions/fonts.js b/devtools/client/inspector/fonts/actions/fonts.js new file mode 100644 index 0000000000..5682e65521 --- /dev/null +++ b/devtools/client/inspector/fonts/actions/fonts.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UPDATE_FONTS, +} = require("resource://devtools/client/inspector/fonts/actions/index.js"); + +module.exports = { + /** + * Update the list of fonts in the font inspector + */ + updateFonts(allFonts) { + return { + type: UPDATE_FONTS, + allFonts, + }; + }, +}; diff --git a/devtools/client/inspector/fonts/actions/index.js b/devtools/client/inspector/fonts/actions/index.js new file mode 100644 index 0000000000..21597b8c41 --- /dev/null +++ b/devtools/client/inspector/fonts/actions/index.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Reset font editor to intial state. + "RESET_EDITOR", + + // Set the font editor disabled state which prevents users from interacting with inputs. + "SET_FONT_EDITOR_DISABLED", + + // Apply the variation settings of a font instance. + "APPLY_FONT_VARIATION_INSTANCE", + + // Update the custom font variation instance with the current axes values. + "UPDATE_CUSTOM_INSTANCE", + + // Update the value of a variable font axis. + "UPDATE_AXIS_VALUE", + + // Update font editor with applicable fonts and user-defined CSS font properties. + "UPDATE_EDITOR_STATE", + + // Update the list of fonts. + "UPDATE_FONTS", + + // Update the preview text. + "UPDATE_PREVIEW_TEXT", + + // Update the value of a CSS font property + "UPDATE_PROPERTY_VALUE", + + // Update the warning message with the reason for not showing the font editor + "UPDATE_WARNING_MESSAGE", + ], + module.exports +); diff --git a/devtools/client/inspector/fonts/actions/moz.build b/devtools/client/inspector/fonts/actions/moz.build new file mode 100644 index 0000000000..31452af580 --- /dev/null +++ b/devtools/client/inspector/fonts/actions/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "font-editor.js", + "font-options.js", + "fonts.js", + "index.js", +) diff --git a/devtools/client/inspector/fonts/components/Font.js b/devtools/client/inspector/fonts/components/Font.js new file mode 100644 index 0000000000..c761a3d2f1 --- /dev/null +++ b/devtools/client/inspector/fonts/components/Font.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontName = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontName.js") +); +const FontOrigin = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontOrigin.js") +); +const FontPreview = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPreview.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class Font extends PureComponent { + static get propTypes() { + return { + font: PropTypes.shape(Types.font).isRequired, + onPreviewClick: PropTypes.func, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + isFontFaceRuleExpanded: false, + }; + + this.onFontFaceRuleToggle = this.onFontFaceRuleToggle.bind(this); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(newProps) { + if (this.props.font.name === newProps.font.name) { + return; + } + + this.setState({ + isFontFaceRuleExpanded: false, + }); + } + + onFontFaceRuleToggle(event) { + this.setState({ + isFontFaceRuleExpanded: !this.state.isFontFaceRuleExpanded, + }); + event.stopPropagation(); + } + + renderFontCSSCode(rule, ruleText) { + if (!rule) { + return null; + } + + // Cut the rule text in 3 parts: the selector, the declarations, the closing brace. + // This way we can collapse the declarations by default and display an expander icon + // to expand them again. + const leading = ruleText.substring(0, ruleText.indexOf("{") + 1); + const body = ruleText.substring( + ruleText.indexOf("{") + 1, + ruleText.lastIndexOf("}") + ); + const trailing = ruleText.substring(ruleText.lastIndexOf("}")); + + const { isFontFaceRuleExpanded } = this.state; + + return dom.pre( + { + className: "font-css-code", + }, + this.renderFontCSSCodeTwisty(), + leading, + isFontFaceRuleExpanded + ? body + : dom.span({ + className: "font-css-code-expander", + onClick: this.onFontFaceRuleToggle, + }), + trailing + ); + } + + renderFontCSSCodeTwisty() { + const { isFontFaceRuleExpanded } = this.state; + + const attributes = { + className: "theme-twisty", + onClick: this.onFontFaceRuleToggle, + }; + if (isFontFaceRuleExpanded) { + attributes.open = "true"; + } + + return dom.span(attributes); + } + + renderFontFamilyName(family) { + if (!family) { + return null; + } + + return dom.div({ className: "font-family-name" }, family); + } + + render() { + const { font, onPreviewClick, onToggleFontHighlight } = this.props; + + const { CSSFamilyName, previewUrl, rule, ruleText } = font; + + return dom.li( + { + className: "font", + }, + dom.div( + {}, + this.renderFontFamilyName(CSSFamilyName), + FontName({ font, onToggleFontHighlight }) + ), + FontOrigin({ font }), + FontPreview({ onPreviewClick, previewUrl }), + this.renderFontCSSCode(rule, ruleText) + ); + } +} + +module.exports = Font; diff --git a/devtools/client/inspector/fonts/components/FontAxis.js b/devtools/client/inspector/fonts/components/FontAxis.js new file mode 100644 index 0000000000..f695c06b29 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontAxis.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontAxis extends PureComponent { + static get propTypes() { + return { + axis: PropTypes.shape(Types.fontVariationAxis), + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.number.isRequired, + }; + } + + /** + * Naive implementation to get increment step for variable font axis that ensures + * fine grained control based on range of values between min and max. + * + * @param {Number} min + * Minumum value for range. + * @param {Number} max + * Maximum value for range. + * @return {Number} + * Step value used in range input for font axis. + */ + getAxisStep(min, max) { + let step = 1; + const delta = parseInt(max, 10) - parseInt(min, 10); + + if (delta <= 1) { + step = 0.001; + } else if (delta <= 10) { + step = 0.01; + } else if (delta <= 100) { + step = 0.1; + } + + return step; + } + + render() { + const { axis, value, onChange } = this.props; + + return FontPropertyValue({ + className: "font-control-axis", + disabled: this.props.disabled, + label: axis.name, + min: axis.minValue, + minLabel: true, + max: axis.maxValue, + maxLabel: true, + name: axis.tag, + nameLabel: true, + onChange, + step: this.getAxisStep(axis.minValue, axis.maxValue), + value, + }); + } +} + +module.exports = FontAxis; diff --git a/devtools/client/inspector/fonts/components/FontEditor.js b/devtools/client/inspector/fonts/components/FontEditor.js new file mode 100644 index 0000000000..b98118c9fc --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontEditor.js @@ -0,0 +1,357 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontAxis = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontAxis.js") +); +const FontName = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontName.js") +); +const FontSize = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontSize.js") +); +const FontStyle = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontStyle.js") +); +const FontWeight = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontWeight.js") +); +const LetterSpacing = createFactory( + require("resource://devtools/client/inspector/fonts/components/LetterSpacing.js") +); +const LineHeight = createFactory( + require("resource://devtools/client/inspector/fonts/components/LineHeight.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +// Maximum number of font families to be shown by default. Any others will be hidden +// under a collapsed
    element with a toggle to reveal them. +const MAX_FONTS = 3; + +class FontEditor extends PureComponent { + static get propTypes() { + return { + fontEditor: PropTypes.shape(Types.fontEditor).isRequired, + onInstanceChange: PropTypes.func.isRequired, + onPropertyChange: PropTypes.func.isRequired, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + /** + * Get an array of FontAxis components with editing controls for of the given variable + * font axes. If no axes were given, return null. + * If an axis' value was declared on the font-variation-settings CSS property or was + * changed using the font editor, use that value, otherwise use the axis default. + * + * @param {Array} fontAxes + * Array of font axis instances + * @param {Object} editedAxes + * Object with axes and values edited by the user or defined in the CSS + * declaration for font-variation-settings. + * @return {Array|null} + */ + renderAxes(fontAxes = [], editedAxes) { + if (!fontAxes.length) { + return null; + } + + return fontAxes.map(axis => { + return FontAxis({ + key: axis.tag, + axis, + disabled: this.props.fontEditor.disabled, + onChange: this.props.onPropertyChange, + minLabel: true, + maxLabel: true, + value: editedAxes[axis.tag] || axis.defaultValue, + }); + }); + } + + /** + * Render fonts used on the selected node grouped by font-family. + * + * @param {Array} fonts + * Fonts used on selected node. + * @return {DOMNode} + */ + renderUsedFonts(fonts) { + if (!fonts.length) { + return null; + } + + // Group fonts by family name. + const fontGroups = fonts.reduce((acc, font) => { + const family = font.CSSFamilyName.toString(); + acc[family] = acc[family] || []; + acc[family].push(font); + return acc; + }, {}); + + const renderedFontGroups = Object.keys(fontGroups).map(family => { + return this.renderFontGroup(family, fontGroups[family]); + }); + + const topFontsList = renderedFontGroups.slice(0, MAX_FONTS); + const moreFontsList = renderedFontGroups.slice( + MAX_FONTS, + renderedFontGroups.length + ); + + const moreFonts = !moreFontsList.length + ? null + : dom.details( + {}, + dom.summary( + {}, + dom.span( + { className: "label-open" }, + getStr("fontinspector.showMore") + ), + dom.span( + { className: "label-close" }, + getStr("fontinspector.showLess") + ) + ), + moreFontsList + ); + + return dom.label( + { + className: "font-control font-control-used-fonts", + }, + dom.span( + { + className: "font-control-label", + }, + getStr("fontinspector.fontsUsedLabel") + ), + dom.div( + { + className: "font-control-box", + }, + topFontsList, + moreFonts + ) + ); + } + + renderFontGroup(family, fonts = []) { + const group = fonts.map(font => { + return FontName({ + key: font.name, + font, + onToggleFontHighlight: this.props.onToggleFontHighlight, + }); + }); + + return dom.div( + { + key: family, + className: "font-group", + }, + dom.div( + { + className: "font-family-name", + }, + family + ), + group + ); + } + + renderFontSize(value) { + return ( + value !== null && + FontSize({ + key: `${this.props.fontEditor.id}:font-size`, + disabled: this.props.fontEditor.disabled, + onChange: this.props.onPropertyChange, + value, + }) + ); + } + + renderLineHeight(value) { + return ( + value !== null && + LineHeight({ + key: `${this.props.fontEditor.id}:line-height`, + disabled: this.props.fontEditor.disabled, + onChange: this.props.onPropertyChange, + value, + }) + ); + } + + renderLetterSpacing(value) { + return ( + value !== null && + LetterSpacing({ + key: `${this.props.fontEditor.id}:letter-spacing`, + disabled: this.props.fontEditor.disabled, + onChange: this.props.onPropertyChange, + value, + }) + ); + } + + renderFontStyle(value) { + return ( + value && + FontStyle({ + onChange: this.props.onPropertyChange, + disabled: this.props.fontEditor.disabled, + value, + }) + ); + } + + renderFontWeight(value) { + return ( + value !== null && + FontWeight({ + onChange: this.props.onPropertyChange, + disabled: this.props.fontEditor.disabled, + value, + }) + ); + } + + /** + * Get a dropdown which allows selecting between variation instances defined by a font. + * + * @param {Array} fontInstances + * Named variation instances as provided with the font file. + * @param {Object} selectedInstance + * Object with information about the currently selected variation instance. + * Example: + * { + * name: "Custom", + * values: [] + * } + * @return {DOMNode} + */ + renderInstances(fontInstances = [], selectedInstance = {}) { + // Append a "Custom" instance entry which represents the latest manual axes changes. + const customInstance = { + name: getStr("fontinspector.customInstanceName"), + values: this.props.fontEditor.customInstanceValues, + }; + fontInstances = [...fontInstances, customInstance]; + + // Generate the