diff options
Diffstat (limited to 'devtools/client/inspector/changes/test')
23 files changed, 1466 insertions, 0 deletions
diff --git a/devtools/client/inspector/changes/test/browser.ini b/devtools/client/inspector/changes/test/browser.ini new file mode 100644 index 0000000000..9b1b44d56e --- /dev/null +++ b/devtools/client/inspector/changes/test/browser.ini @@ -0,0 +1,28 @@ +[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_at_rules.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] +[browser_changes_declaration_remove_ahead.js] +[browser_changes_declaration_remove_disabled.js] +[browser_changes_declaration_remove.js] +[browser_changes_declaration_rename.js] +[browser_changes_rule_add.js] +[browser_changes_rule_selector.js] diff --git a/devtools/client/inspector/changes/test/browser_changes_at_rules.js b/devtools/client/inspector/changes/test/browser_changes_at_rules.js new file mode 100644 index 0000000000..53551f9822 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_at_rules.js @@ -0,0 +1,98 @@ +/* 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 at-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 { + +const divRule = `div { + color: tomato; +}`; +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</div> +`; + +const EXPECTED = [ + { + text: "@media screen and (height > 5px) {", + copyRuleClipboard: mediaRule.replace("tomato", "cyan"), + }, + { + text: "@layer myLayer {", + copyRuleClipboard: layerRule.replace("tomato", "cyan"), + }, + { + text: "@container myContainer (width > 10px) {", + copyRuleClipboard: containerRule.replace("tomato", "cyan"), + }, + { text: "div {", copyRuleClipboard: divRule.replace("tomato", "cyan") }, +]; + +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); + const onTrackChange = waitForDispatch(store, "TRACK_CHANGE"); + await updateDeclaration(ruleView, 1, { color: "tomato" }, { color: "cyan" }); + await onTrackChange; + + 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.text, + expectedItem.innerText, + `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_background_tracking.js b/devtools/client/inspector/changes/test/browser_changes_background_tracking.js new file mode 100644 index 0000000000..613e2d9062 --- /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]); + ok(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..258c3100d3 --- /dev/null +++ b/devtools/client/inspector/changes/test/browser_changes_declaration_remove_disabled.js @@ -0,0 +1,106 @@ +/* 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 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_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.ini b/devtools/client/inspector/changes/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..886645a708 --- /dev/null +++ b/devtools/client/inspector/changes/test/xpcshell/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = devtools +firefox-appdir = browser +head = head.js +support-files = + ./mocks.js + +[test_changes_stylesheet.js] |