summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/resource/tests/browser_resources_stylesheets.js')
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets.js713
1 files changed, 713 insertions, 0 deletions
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js
new file mode 100644
index 0000000000..ec81e8118d
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js
@@ -0,0 +1,713 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around STYLESHEET.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const STYLE_TEST_URL = URL_ROOT_SSL + "style_document.html";
+
+const EXISTING_RESOURCES = [
+ {
+ styleText: "body { color: lime; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { margin: 1px; }",
+ href: "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.css",
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "",
+ href: null,
+ nodeHref: null,
+ isNew: false,
+ disabled: false,
+ constructed: true,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { background-color: pink; }",
+ href: null,
+ nodeHref:
+ "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { padding: 1px; }",
+ href: "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.css",
+ nodeHref:
+ "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+];
+
+const ADDITIONAL_INLINE_RESOURCE = {
+ styleText:
+ "@media all { body { color: red; } } @media print { body { color: cyan; } } body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 5,
+ atRules: [
+ {
+ type: "media",
+ conditionText: "all",
+ matches: true,
+ line: 1,
+ column: 1,
+ },
+ {
+ type: "media",
+ conditionText: "print",
+ matches: false,
+ line: 1,
+ column: 37,
+ },
+ ],
+};
+
+const ADDITIONAL_CONSTRUCTED_RESOURCE = {
+ styleText: "",
+ href: null,
+ nodeHref: null,
+ isNew: false,
+ disabled: false,
+ constructed: true,
+ ruleCount: 2,
+ atRules: [],
+};
+
+const ADDITIONAL_FROM_ACTOR_RESOURCE = {
+ styleText: "body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: true,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+};
+
+add_task(async function () {
+ await testResourceAvailableDestroyedFeature();
+ await testResourceUpdateFeature();
+ await testNestedResourceUpdateFeature();
+});
+
+function pushAvailableResource(availableResources) {
+ // TODO(bug 1826538): Find a better way of dealing with these.
+ return function (resources) {
+ for (const resource of resources) {
+ if (resource.href?.startsWith("resource://")) {
+ continue;
+ }
+ availableResources.push(resource);
+ }
+ };
+}
+
+async function testResourceAvailableDestroyedFeature() {
+ info("Check resource available feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+ let resourceTimingEntryCounts = await getResourceTimingCount(tab);
+ is(
+ resourceTimingEntryCounts,
+ 2,
+ "Should have two entires for resource timing"
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check whether ResourceCommand gets existing stylesheet");
+ const availableResources = [];
+ const destroyedResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onDestroyed: resources => destroyedResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+ for (let i = 0; i < availableResources.length; i++) {
+ const availableResource = availableResources[i];
+ // We can not expect the resources to always be forwarded in the same order.
+ // See intermittent Bug 1655016.
+ const expectedResource = findMatchingExpectedResource(availableResource);
+ ok(expectedResource, "Found a matching expected resource for the resource");
+ await assertResource(availableResource, expectedResource);
+ }
+
+ resourceTimingEntryCounts = await getResourceTimingCount(tab);
+ is(
+ resourceTimingEntryCounts,
+ 2,
+ "Should still have two entires for resource timing after devtools APIs have been triggered"
+ );
+
+ info("Check whether ResourceCommand gets additonal stylesheet");
+ await ContentTask.spawn(
+ tab.linkedBrowser,
+ ADDITIONAL_INLINE_RESOURCE.styleText,
+ text => {
+ const document = content.document;
+ const stylesheet = document.createElement("style");
+ stylesheet.id = "inline-from-test";
+ stylesheet.textContent = text;
+ document.body.appendChild(stylesheet);
+ }
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 1
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_INLINE_RESOURCE
+ );
+
+ info("Check whether ResourceCommand gets additonal constructed stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ const document = content.document;
+ const s = new content.CSSStyleSheet();
+ // We use the different number of rules to meaningfully differentiate
+ // between constructed stylesheets.
+ s.replaceSync("foo { color: red } bar { color: blue }");
+ // TODO(bug 1751346): wrappedJSObject should be unnecessary.
+ document.wrappedJSObject.adoptedStyleSheets.push(s);
+ });
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 2
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_CONSTRUCTED_RESOURCE
+ );
+
+ info(
+ "Check whether ResourceCommand gets additonal stylesheet which is added by DevTools"
+ );
+ const styleSheetsFront = await targetCommand.targetFront.getFront(
+ "stylesheets"
+ );
+ await styleSheetsFront.addStyleSheet(
+ ADDITIONAL_FROM_ACTOR_RESOURCE.styleText
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 3
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_FROM_ACTOR_RESOURCE
+ );
+
+ info("Check resource destroyed feature of the ResourceCommand");
+ is(destroyedResources.length, 0, "There was no removed stylesheets yet");
+
+ info("Remove inline stylesheet added in the test");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("#inline-from-test").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 1);
+ assertDestroyed(destroyedResources[0], {
+ resourceId: availableResources.at(-3).resourceId,
+ });
+
+ info("Remove existing top-level inline stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("style").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 2);
+ assertDestroyed(destroyedResources[1], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[0]
+ ).resourceId,
+ });
+
+ info("Remove existing top-level <link> stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("link").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 3);
+ assertDestroyed(destroyedResources[2], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[1]
+ ).resourceId,
+ });
+
+ info("Remove existing iframe inline stylesheet");
+ const iframeBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+
+ await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
+ content.document.querySelector("style").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 4);
+ assertDestroyed(destroyedResources[3], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[3]
+ ).resourceId,
+ });
+
+ info("Remove existing iframe <link> stylesheet");
+ await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
+ content.document.querySelector("link").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 5);
+ assertDestroyed(destroyedResources[4], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[4]
+ ).resourceId,
+ });
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testResourceUpdateFeature() {
+ info("Check resource update feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+ is(updates.length, 0, "there's no update yet");
+
+ info("Check toggleDisabled function");
+ // Retrieve the stylesheet of the top-level target
+ const resource = availableResources.find(
+ innerResource => innerResource.targetFront.isTopLevel
+ );
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.toggleDisabled(resource.resourceId);
+ await waitUntil(() => updates.length === 1);
+
+ // Check the content of the update object.
+ assertUpdate(updates[0].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[0].update.resourceUpdates.disabled,
+ true,
+ "resourceUpdates is correct"
+ );
+
+ // Check whether the cached resource is updated correctly.
+ is(
+ updates[0].resource.disabled,
+ true,
+ "cached resource is updated correctly"
+ );
+
+ // Check whether the actual stylesheet is updated correctly.
+ const styleSheetDisabled = await ContentTask.spawn(
+ tab.linkedBrowser,
+ null,
+ () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ return stylesheet.disabled;
+ }
+ );
+ is(styleSheetDisabled, true, "actual stylesheet was updated correctly");
+
+ info("Check update function");
+ const expectedAtRules = [
+ {
+ type: "media",
+ conditionText: "screen",
+ matches: true,
+ },
+ {
+ type: "media",
+ conditionText: "print",
+ matches: false,
+ },
+ ];
+
+ const updateCause = "updated-by-test";
+ await styleSheetsFront.update(
+ resource.resourceId,
+ "@media screen { color: red; } @media print { color: green; } body { color: cyan; }",
+ false,
+ updateCause
+ );
+ await waitUntil(() => updates.length === 4);
+
+ assertUpdate(updates[1].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[1].update.resourceUpdates.ruleCount,
+ 3,
+ "resourceUpdates is correct"
+ );
+ is(updates[1].resource.ruleCount, 3, "cached resource is updated correctly");
+
+ assertUpdate(updates[2].update, {
+ resourceId: resource.resourceId,
+ updateType: "style-applied",
+ event: {
+ cause: updateCause,
+ },
+ });
+ is(
+ updates[2].update.resourceUpdates,
+ undefined,
+ "resourceUpdates is correct"
+ );
+
+ assertUpdate(updates[3].update, {
+ resourceId: resource.resourceId,
+ updateType: "at-rules-changed",
+ });
+ assertAtRules(updates[3].update.resourceUpdates.atRules, expectedAtRules);
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+
+ is(
+ styleSheetResult.ruleCount,
+ 3,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertAtRules(styleSheetResult.atRules, expectedAtRules);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testNestedResourceUpdateFeature() {
+ info("Check nested resource update feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } =
+ tab.ownerGlobal;
+
+ registerCleanupFunction(() => {
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+ });
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+
+ info("Apply new media query");
+ // In order to avoid applying the media query (min-height: 400px).
+ if (originalWindowHeight !== 300) {
+ await new Promise(resolve => {
+ tab.ownerGlobal.addEventListener("resize", resolve, { once: true });
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 300);
+ });
+ }
+
+ // Retrieve the stylesheet of the top-level target
+ const resource = availableResources.find(
+ innerResource => innerResource.targetFront.isTopLevel
+ );
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.update(
+ resource.resourceId,
+ `@media (min-height: 400px) {
+ html {
+ color: red;
+ }
+ @layer myLayer {
+ @supports (container-type) {
+ :root {
+ color: gold;
+ container: root inline-size;
+ }
+
+ @container root (width > 10px) {
+ body {
+ color: gold;
+ }
+ }
+ }
+ }
+ }`,
+ false
+ );
+ await waitUntil(() => updates.length === 3);
+ is(
+ updates.at(-1).resource.ruleCount,
+ 7,
+ "Resource in update has expected ruleCount"
+ );
+
+ is(resource.atRules[0].matches, false, "Media query is not matched yet");
+
+ info("Change window size to fire matches-change event");
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 500);
+ await waitUntil(() => updates.length === 4);
+
+ // Check the update content.
+ const targetUpdate = updates[3];
+ assertUpdate(targetUpdate.update, {
+ resourceId: resource.resourceId,
+ updateType: "matches-change",
+ });
+ Assert.strictEqual(
+ resource,
+ targetUpdate.resource,
+ "Update object has the same resource"
+ );
+
+ is(
+ JSON.stringify(targetUpdate.update.nestedResourceUpdates[0].path),
+ JSON.stringify(["atRules", 0, "matches"]),
+ "path of nestedResourceUpdates is correct"
+ );
+ is(
+ targetUpdate.update.nestedResourceUpdates[0].value,
+ true,
+ "value of nestedResourceUpdates is correct"
+ );
+
+ // Check the resource.
+ const expectedAtRules = [
+ {
+ type: "media",
+ conditionText: "(min-height: 400px)",
+ matches: true,
+ },
+ {
+ type: "layer",
+ layerName: "myLayer",
+ },
+ {
+ type: "support",
+ conditionText: "(container-type)",
+ },
+ {
+ type: "container",
+ conditionText: "root (width > 10px)",
+ },
+ ];
+
+ assertAtRules(targetUpdate.resource.atRules, expectedAtRules);
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+ is(
+ styleSheetResult.ruleCount,
+ 7,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertAtRules(styleSheetResult.atRules, expectedAtRules);
+
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+function findMatchingExpectedResource(resource) {
+ return EXISTING_RESOURCES.find(
+ expected =>
+ resource.href === expected.href &&
+ resource.nodeHref === expected.nodeHref &&
+ resource.ruleCount === expected.ruleCount &&
+ resource.constructed == expected.constructed
+ );
+}
+
+async function getStyleSheetResult(tab) {
+ const result = await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ let ruleCount = 0;
+ const atRules = [];
+
+ const traverseRules = ruleList => {
+ for (const rule of ruleList) {
+ ruleCount++;
+
+ if (rule.media) {
+ let matches = false;
+ try {
+ const mql = content.matchMedia(rule.media.mediaText);
+ matches = mql.matches;
+ } catch (e) {
+ // Ignored
+ }
+
+ atRules.push({
+ type: "media",
+ conditionText: rule.conditionText,
+ matches,
+ });
+ } else if (rule instanceof content.CSSContainerRule) {
+ atRules.push({
+ type: "container",
+ conditionText: rule.conditionText,
+ });
+ } else if (rule instanceof content.CSSLayerBlockRule) {
+ atRules.push({ type: "layer", layerName: rule.name });
+ } else if (rule instanceof content.CSSSupportsRule) {
+ atRules.push({
+ type: "support",
+ conditionText: rule.conditionText,
+ });
+ }
+
+ if (rule.cssRules) {
+ traverseRules(rule.cssRules);
+ }
+ }
+ };
+ traverseRules(stylesheet.cssRules);
+
+ return { ruleCount, atRules };
+ });
+
+ return result;
+}
+
+function assertAtRules(atRules, expectedAtRules) {
+ is(
+ atRules.length,
+ expectedAtRules.length,
+ "Length of the atRules is correct"
+ );
+
+ for (let i = 0; i < atRules.length; i++) {
+ const atRule = atRules[i];
+ const expected = expectedAtRules[i];
+ is(atRule.type, expected.type, "at-rule is of expected type");
+ is(
+ atRules[i].conditionText,
+ expected.conditionText,
+ "conditionText is correct"
+ );
+ if (expected.type === "media") {
+ is(atRule.matches, expected.matches, "matches is correct");
+ } else if (expected.type === "layer") {
+ is(atRule.layerName, expected.layerName, "layerName is correct");
+ }
+
+ if (expected.line !== undefined) {
+ is(atRule.line, expected.line, "line is correct");
+ }
+
+ if (expected.column !== undefined) {
+ is(atRule.column, expected.column, "column is correct");
+ }
+ }
+}
+
+async function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ const styleText = (await getStyleSheetResourceText(resource)).trim();
+ is(styleText, expected.styleText, "Style text is correct");
+ is(resource.href, expected.href, "href is correct");
+ is(resource.nodeHref, expected.nodeHref, "nodeHref is correct");
+ is(resource.isNew, expected.isNew, "isNew is correct");
+ is(resource.disabled, expected.disabled, "disabled is correct");
+ is(resource.constructed, expected.constructed, "constructed is correct");
+ is(resource.ruleCount, expected.ruleCount, "ruleCount is correct");
+ assertAtRules(resource.atRules, expected.atRules);
+}
+
+function assertUpdate(update, expected) {
+ is(
+ update.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ is(update.resourceId, expected.resourceId, "resourceId is correct");
+ is(update.updateType, expected.updateType, "updateType is correct");
+ if (expected.event?.cause) {
+ is(update.event?.cause, expected.event.cause, "cause is correct");
+ }
+}
+
+function assertDestroyed(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ is(resource.resourceId, expected.resourceId, "resourceId is correct");
+}
+
+function getResourceTimingCount(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, [], () => {
+ return content.performance.getEntriesByType("resource").length;
+ });
+}