diff --git a/.stylelintignore b/.stylelintignore index b00803ac6e5a..9ffc1c4a5003 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -66,7 +66,7 @@ dom/xml/test/old/toc/book.css dom/xml/test/old/toc/toc.css # Tests we don't want to modify at this point: -layout/base/tests/bug839103.css +layout/base/tests/stylesheet_change_events.css layout/inspector/tests/bug856317.css layout/inspector/tests/chrome/test_bug467669.css layout/inspector/tests/chrome/test_bug708874.css diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index f4f15710e8aa..deb86afce641 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -225,6 +225,8 @@ #include "mozilla/dom/StyleSheetApplicableStateChangeEvent.h" #include "mozilla/dom/StyleSheetApplicableStateChangeEventBinding.h" #include "mozilla/dom/StyleSheetList.h" +#include "mozilla/dom/StyleSheetRemovedEvent.h" +#include "mozilla/dom/StyleSheetRemovedEventBinding.h" #include "mozilla/dom/TimeoutManager.h" #include "mozilla/dom/ToggleEvent.h" #include "mozilla/dom/Touch.h" @@ -7450,6 +7452,26 @@ void Document::PostStyleSheetApplicableStateChangeEvent(StyleSheet& aSheet) { asyncDispatcher->PostDOMEvent(); } +void Document::PostStyleSheetRemovedEvent(StyleSheet& aSheet) { + if (!StyleSheetChangeEventsEnabled()) { + return; + } + + StyleSheetRemovedEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mStylesheet = &aSheet; + + RefPtr event = + StyleSheetRemovedEvent::Constructor(this, u"StyleSheetRemoved"_ns, init); + event->SetTrusted(true); + event->SetTarget(this); + RefPtr asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes; + asyncDispatcher->PostDOMEvent(); +} + static int32_t FindSheet(const nsTArray>& aSheets, nsIURI* aSheetURI) { for (int32_t i = aSheets.Length() - 1; i >= 0; i--) { diff --git a/dom/base/Document.h b/dom/base/Document.h index 68545205cac5..9b9793bf4ebe 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -1687,8 +1687,8 @@ class Document : public nsINode, * and that observers should be notified and style sets updated */ void StyleSheetApplicableStateChanged(StyleSheet&); - void PostStyleSheetApplicableStateChangeEvent(StyleSheet&); + void PostStyleSheetRemovedEvent(StyleSheet&); enum additionalSheetType { eAgentSheet, diff --git a/dom/base/DocumentOrShadowRoot.cpp b/dom/base/DocumentOrShadowRoot.cpp index b43edb0275ad..9f9299cd312e 100644 --- a/dom/base/DocumentOrShadowRoot.cpp +++ b/dom/base/DocumentOrShadowRoot.cpp @@ -93,6 +93,7 @@ void DocumentOrShadowRoot::RemoveStyleSheet(StyleSheet& aSheet) { mStyleSheets.RemoveElementAt(index); RemoveSheetFromStylesIfApplicable(*sheet); sheet->ClearAssociatedDocumentOrShadowRoot(); + AsNode().OwnerDoc()->PostStyleSheetRemovedEvent(aSheet); } void DocumentOrShadowRoot::RemoveSheetFromStylesIfApplicable( diff --git a/dom/webidl/StyleSheetApplicableStateChangeEvent.webidl b/dom/chrome-webidl/StyleSheetApplicableStateChangeEvent.webidl similarity index 100% rename from dom/webidl/StyleSheetApplicableStateChangeEvent.webidl rename to dom/chrome-webidl/StyleSheetApplicableStateChangeEvent.webidl diff --git a/dom/chrome-webidl/StyleSheetRemovedEvent.webidl b/dom/chrome-webidl/StyleSheetRemovedEvent.webidl new file mode 100644 index 000000000000..17d2c8b2f9ed --- /dev/null +++ b/dom/chrome-webidl/StyleSheetRemovedEvent.webidl @@ -0,0 +1,17 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +[ChromeOnly, Exposed=Window] +interface StyleSheetRemovedEvent : Event { + constructor(DOMString type, + optional StyleSheetRemovedEventInit eventInitDict = {}); + + readonly attribute CSSStyleSheet? stylesheet; +}; + +dictionary StyleSheetRemovedEventInit : EventInit { + CSSStyleSheet? stylesheet = null; +}; diff --git a/dom/chrome-webidl/moz.build b/dom/chrome-webidl/moz.build index 9601c1255d7b..947e5ea88b23 100644 --- a/dom/chrome-webidl/moz.build +++ b/dom/chrome-webidl/moz.build @@ -81,6 +81,8 @@ WEBIDL_FILES = [ "SessionStoreUtils.webidl", "StripOnShareRule.webidl", "StructuredCloneHolder.webidl", + "StyleSheetApplicableStateChangeEvent.webidl", + "StyleSheetRemovedEvent.webidl", "TelemetryStopwatch.webidl", "UserInteraction.webidl", "WebExtensionContentScript.webidl", @@ -92,6 +94,11 @@ WEBIDL_FILES = [ "XULTreeElement.webidl", ] +GENERATED_EVENTS_WEBIDL_FILES = [ + "StyleSheetApplicableStateChangeEvent.webidl", + "StyleSheetRemovedEvent.webidl", +] + if CONFIG["MOZ_BUILD_APP"] != "mobile/android": WEBIDL_FILES += [ "UniFFI.webidl", diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html index b88f273a832f..a6853238a8f6 100644 --- a/dom/events/test/test_all_synthetic_events.html +++ b/dom/events/test/test_all_synthetic_events.html @@ -350,6 +350,11 @@ const kEventConstructors = { }, chromeOnly: true, }, + StyleSheetRemovedEvent: { create (aName, aProps) { + return new StyleSheetRemovedEvent(aName, aProps); + }, + chromeOnly: true, + }, SubmitEvent: { create (aName, aProps) { return new SubmitEvent(aName, aProps); }, diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index f671ca77599e..9a151c6f232f 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -1108,7 +1108,6 @@ WEBIDL_FILES += [ "PopStateEvent.webidl", "PopupBlockedEvent.webidl", "ProgressEvent.webidl", - "StyleSheetApplicableStateChangeEvent.webidl", ] # WebExtensions API. @@ -1188,7 +1187,6 @@ GENERATED_EVENTS_WEBIDL_FILES = [ "PromiseRejectionEvent.webidl", "ScrollViewChangeEvent.webidl", "SecurityPolicyViolationEvent.webidl", - "StyleSheetApplicableStateChangeEvent.webidl", "SubmitEvent.webidl", "TaskPriorityChangeEvent.webidl", "TCPServerSocketEvent.webidl", diff --git a/layout/base/tests/browser.ini b/layout/base/tests/browser.ini index a05e516f401e..5b9e53de2f4b 100644 --- a/layout/base/tests/browser.ini +++ b/layout/base/tests/browser.ini @@ -1,8 +1,3 @@ -[browser_bug617076.js] -[browser_bug839103.js] -support-files = - file_bug839103.html - bug839103.css [browser_bug1701027-1.js] support-files = helper_bug1701027-1.html @@ -13,6 +8,9 @@ support-files = run-if = (((os == 'mac') || (os == 'win' && os_version != '6.1' && processor == 'x86_64')) && debug) [browser_bug1787079.js] run-if = ((os == 'win' && os_version != '6.1' && processor == 'x86_64') && debug) +[browser_bug1791083.js] +skip-if = !sessionHistoryInParent +[browser_bug617076.js] [browser_disableDialogs_onbeforeunload.js] [browser_onbeforeunload_only_after_interaction.js] [browser_onbeforeunload_only_after_interaction_in_frame.js] @@ -20,10 +18,6 @@ run-if = ((os == 'win' && os_version != '6.1' && processor == 'x86_64') && debug support-files = test_scroll_into_view_in_oopif.html scroll_into_view_in_child.html -[browser_visual_viewport_iframe.js] -support-files = - test_visual_viewport_in_oopif.html - visual_viewport_in_child.html [browser_select_popup_position_in_out_of_process_iframe.js] skip-if = (verify && (os == 'mac')) # bug 1627874 @@ -32,5 +26,11 @@ skip-if = support-files = !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js !/browser/base/content/test/forms/head.js -[browser_bug1791083.js] -skip-if = !sessionHistoryInParent +[browser_stylesheet_change_events.js] +support-files = + file_stylesheet_change_events.html + stylesheet_change_events.css +[browser_visual_viewport_iframe.js] +support-files = + test_visual_viewport_in_oopif.html + visual_viewport_in_child.html diff --git a/layout/base/tests/browser_bug839103.js b/layout/base/tests/browser_bug839103.js deleted file mode 100644 index fd20b8202943..000000000000 --- a/layout/base/tests/browser_bug839103.js +++ /dev/null @@ -1,82 +0,0 @@ -const gTestRoot = getRootDirectory(gTestPath).replace( - "chrome://mochitests/content/", - "http://127.0.0.1:8888/" -); - -add_task(async function test() { - await BrowserTestUtils.withNewTab( - { gBrowser, url: gTestRoot + "file_bug839103.html" }, - async function (browser) { - await SpecialPowers.spawn(browser, [gTestRoot], testBody); - } - ); -}); - -// This function runs entirely in the content process. It doesn't have access -// any free variables in this file. -async function testBody(testRoot) { - const gStyleSheet = "bug839103.css"; - - function unexpectedContentEvent(event) { - ok(false, "Received a " + event.type + " event on content"); - } - - // We've seen the original stylesheet in the document. - // Now add a stylesheet on the fly and make sure we see it. - let doc = content.document; - doc.styleSheetChangeEventsEnabled = true; - doc.addEventListener( - "StyleSheetApplicableStateChanged", - unexpectedContentEvent - ); - doc.defaultView.addEventListener( - "StyleSheetApplicableStateChanged", - unexpectedContentEvent - ); - - let link = doc.createElement("link"); - link.setAttribute("rel", "stylesheet"); - link.setAttribute("type", "text/css"); - link.setAttribute("href", testRoot + gStyleSheet); - - let stateChanged = ContentTaskUtils.waitForEvent( - docShell.chromeEventHandler, - "StyleSheetApplicableStateChanged", - true - ); - doc.body.appendChild(link); - - info("waiting for applicable state change event"); - let evt = await stateChanged; - info("received dynamic style sheet applicable state change event"); - is( - evt.type, - "StyleSheetApplicableStateChanged", - "evt.type has expected value" - ); - is(evt.target, doc, "event targets correct document"); - is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value"); - is(evt.applicable, true, "evt.applicable has the right value"); - - stateChanged = ContentTaskUtils.waitForEvent( - docShell.chromeEventHandler, - "StyleSheetApplicableStateChanged", - true - ); - link.sheet.disabled = true; - - evt = await stateChanged; - is( - evt.type, - "StyleSheetApplicableStateChanged", - "evt.type has expected value" - ); - info( - 'received dynamic style sheet applicable state change event after media="" changed' - ); - is(evt.target, doc, "event targets correct document"); - is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value"); - is(evt.applicable, false, "evt.applicable has the right value"); - - doc.body.removeChild(link); -} diff --git a/layout/base/tests/browser_stylesheet_change_events.js b/layout/base/tests/browser_stylesheet_change_events.js new file mode 100644 index 000000000000..c86719705070 --- /dev/null +++ b/layout/base/tests/browser_stylesheet_change_events.js @@ -0,0 +1,227 @@ +const gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://127.0.0.1:8888/" +); + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: gTestRoot + "file_stylesheet_change_events.html" }, + async function (browser) { + await SpecialPowers.spawn( + browser, + [gTestRoot], + testApplicableStateChangeEvent + ); + } + ); +}); + +// This function runs entirely in the content process. It doesn't have access +// any free variables in this file. +async function testApplicableStateChangeEvent(testRoot) { + // We've seen the original stylesheet in the document. + // Now add a stylesheet on the fly and make sure we see it. + let doc = content.document; + doc.styleSheetChangeEventsEnabled = true; + + const unexpectedContentEvent = event => + ok(false, "Received a " + event.type + " event on content"); + doc.addEventListener( + "StyleSheetApplicableStateChanged", + unexpectedContentEvent + ); + doc.defaultView.addEventListener( + "StyleSheetApplicableStateChanged", + unexpectedContentEvent + ); + doc.addEventListener("StyleSheetRemoved", unexpectedContentEvent); + doc.defaultView.addEventListener("StyleSheetRemoved", unexpectedContentEvent); + + function shouldIgnoreEvent(e) { + // accessiblecaret.css might be reported, interfering with the test + // assertions, so let's ignore it + return ( + e.stylesheet?.href === "resource://content-accessible/accessiblecaret.css" + ); + } + + function waitForStyleApplicableStateChanged() { + return ContentTaskUtils.waitForEvent( + docShell.chromeEventHandler, + "StyleSheetApplicableStateChanged", + true, + e => !shouldIgnoreEvent(e) + ); + } + + function waitForStyleSheetRemovedEvent() { + return ContentTaskUtils.waitForEvent( + docShell.chromeEventHandler, + "StyleSheetRemoved", + true, + e => !shouldIgnoreEvent(e) + ); + } + + function checkApplicableStateChangeEvent(event, { applicable, stylesheet }) { + is( + event.type, + "StyleSheetApplicableStateChanged", + "event.type has expected value" + ); + is(event.target, doc, "event targets correct document"); + is(event.stylesheet, stylesheet, "event.stylesheet has the expected value"); + is(event.applicable, applicable, "event.applicable has the expected value"); + } + + function checkStyleSheetRemovedEvent(event, { stylesheet }) { + is(event.type, "StyleSheetRemoved", "event.type has expected value"); + is(event.target, doc, "event targets correct document"); + is(event.stylesheet, stylesheet, "event.stylesheet has the expected value"); + } + + // Updating the text content will actually create a new StyleSheet instance, + // and so we should get one event for the new instance, and another one for + // the removal of the "previous"one. + function waitForTextContentChange() { + return Promise.all([ + waitForStyleSheetRemovedEvent(), + waitForStyleApplicableStateChanged(), + ]); + } + + let stateChanged, evt; + + { + const gStyleSheet = "stylesheet_change_events.css"; + + info("Add and wait for applicable state change event"); + let linkEl = doc.createElement("link"); + linkEl.setAttribute("rel", "stylesheet"); + linkEl.setAttribute("type", "text/css"); + linkEl.setAttribute("href", testRoot + gStyleSheet); + + stateChanged = waitForStyleApplicableStateChanged(); + doc.body.appendChild(linkEl); + evt = await stateChanged; + + ok(true, "received dynamic style sheet applicable state change event"); + checkApplicableStateChangeEvent(evt, { + stylesheet: linkEl.sheet, + applicable: true, + }); + + stateChanged = waitForStyleApplicableStateChanged(); + linkEl.sheet.disabled = true; + evt = await stateChanged; + + ok(true, "received dynamic style sheet applicable state change event"); + checkApplicableStateChangeEvent(evt, { + stylesheet: linkEl.sheet, + applicable: false, + }); + + info("Remove stylesheet and wait for removed event"); + const removedStylesheet = linkEl.sheet; + const onStyleSheetRemoved = waitForStyleSheetRemovedEvent(); + doc.body.removeChild(linkEl); + const removedStyleSheetEvt = await onStyleSheetRemoved; + + ok(true, "received removed sheet event"); + checkStyleSheetRemovedEvent(removedStyleSheetEvt, { + stylesheet: removedStylesheet, + }); + } + + { + info("Add `; + evt = await stateChanged; + + ok(true, "received dynamic style sheet applicable state change event"); + const shadowStyleEl = shadowRoot.querySelector("style"); + checkApplicableStateChangeEvent(evt, { + stylesheet: shadowStyleEl.sheet, + applicable: true, + }); + + info("Updating