Bug 1771113 - Add chromeOnly StyleSheetRemoved event, emitted when a stylesheet is removed. r=emilio.

This new event will be consumed by DevTools to update style information.

As for `StyleSheetApplicableStateChanged`, this event is emitted from the document.
It's only enabled when `document.styleSheetChangeEventsEnabled` is set to true.

The existing test around the `StyleSheetApplicableStateChanged` is renamed and
modified to assert both events when stylesheets are added, modified and removed.

Differential Revision: https://phabricator.services.mozilla.com/D147271
This commit is contained in:
Nicolas Chevobbe 2023-08-10 14:36:03 +00:00
parent 1dbe69a36a
commit f086602a6f
15 changed files with 293 additions and 97 deletions

View File

@ -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

View File

@ -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<StyleSheetRemovedEvent> event =
StyleSheetRemovedEvent::Constructor(this, u"StyleSheetRemoved"_ns, init);
event->SetTrusted(true);
event->SetTarget(this);
RefPtr<AsyncEventDispatcher> asyncDispatcher =
new AsyncEventDispatcher(this, event);
asyncDispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;
asyncDispatcher->PostDOMEvent();
}
static int32_t FindSheet(const nsTArray<RefPtr<StyleSheet>>& aSheets,
nsIURI* aSheetURI) {
for (int32_t i = aSheets.Length() - 1; i >= 0; i--) {

View File

@ -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,

View File

@ -93,6 +93,7 @@ void DocumentOrShadowRoot::RemoveStyleSheet(StyleSheet& aSheet) {
mStyleSheets.RemoveElementAt(index);
RemoveSheetFromStylesIfApplicable(*sheet);
sheet->ClearAssociatedDocumentOrShadowRoot();
AsNode().OwnerDoc()->PostStyleSheetRemovedEvent(aSheet);
}
void DocumentOrShadowRoot::RemoveSheetFromStylesIfApplicable(

View File

@ -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;
};

View File

@ -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",

View File

@ -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);
},

View File

@ -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",

View File

@ -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

View File

@ -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);
}

View File

@ -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 <link> 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 <style> node and wait for applicable state changed event");
let styleEl = doc.createElement("style");
styleEl.textContent = `body { background: tomato; }`;
stateChanged = waitForStyleApplicableStateChanged();
doc.head.appendChild(styleEl);
evt = await stateChanged;
ok(true, "received dynamic style sheet applicable state change event");
checkApplicableStateChangeEvent(evt, {
stylesheet: styleEl.sheet,
applicable: true,
});
info("Updating <style> text content");
stateChanged = waitForTextContentChange();
const inlineStyleSheetBeforeChange = styleEl.sheet;
styleEl.textContent = `body { background: gold; }`;
const [inlineRemovedEvt, inlineAddedEvt] = await stateChanged;
ok(true, "received expected style sheet events");
checkStyleSheetRemovedEvent(inlineRemovedEvt, {
stylesheet: inlineStyleSheetBeforeChange,
});
checkApplicableStateChangeEvent(inlineAddedEvt, {
stylesheet: styleEl.sheet,
applicable: true,
});
info("Remove stylesheet and wait for removed event");
const onStyleSheetRemoved = waitForStyleSheetRemovedEvent();
const removedInlineStylesheet = styleEl.sheet;
styleEl.remove();
const removedStyleSheetEvt = await onStyleSheetRemoved;
ok(true, "received removed style sheet event");
checkStyleSheetRemovedEvent(removedStyleSheetEvt, {
stylesheet: removedInlineStylesheet,
});
}
{
info(
"Create a custom element and check we get an event for its stylesheet"
);
stateChanged = waitForStyleApplicableStateChanged();
const el = doc.createElement("div");
const shadowRoot = el.attachShadow({ mode: "open" });
doc.body.appendChild(el);
shadowRoot.innerHTML = `
<span>custom</span>
<style>
span { color: salmon; }
</style>`;
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 <style> text content");
stateChanged = waitForTextContentChange();
const styleSheetBeforeChange = shadowStyleEl.sheet;
shadowStyleEl.textContent = `span { color: cyan; }`;
const [removedEvt, addedEvt] = await stateChanged;
ok(true, "received expected style sheet events");
checkStyleSheetRemovedEvent(removedEvt, {
stylesheet: styleSheetBeforeChange,
});
checkApplicableStateChangeEvent(addedEvt, {
stylesheet: shadowStyleEl.sheet,
applicable: true,
});
info("Remove stylesheet and wait for removed event");
const onStyleSheetRemoved = waitForStyleSheetRemovedEvent();
const removedShadowStylesheet = shadowStyleEl.sheet;
shadowStyleEl.remove();
const removedStyleSheetEvt = await onStyleSheetRemoved;
ok(true, "received removed style sheet event");
checkStyleSheetRemovedEvent(removedStyleSheetEvt, {
stylesheet: removedShadowStylesheet,
});
}
}

View File

@ -648,6 +648,7 @@ module.exports = {
StyleSheet: false,
StyleSheetApplicableStateChangeEvent: false,
StyleSheetList: false,
StyleSheetRemovedEvent: false,
SubtleCrypto: false,
SyncMessageSender: false,
TCPServerSocket: false,