Skip to content

Commit

Permalink
MutationObserver now fires also when attributes added/removed
Browse files Browse the repository at this point in the history
  • Loading branch information
rbri committed Aug 15, 2024
1 parent a3c4e15 commit d1eb495
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

<body>
<release version="4.5.0" date="xxxx, 2024" description="WebWorker, Bugfixes">
<action type="fix" dev="asashour" issue="1691">
MutationObserver now fires also when attributes added/removed.
</action>
<action type="update" dev="RhinoTeam">
core-js: Obsolete class ObjArray removed; depending on context, class replaced
with either ArrayList or ArrayDeque.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/htmlunit/html/HtmlElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ protected void setAttributeNS(final String namespaceURI, final String qualifiedN
*/
protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
final List<HtmlAttributeChangeListener> listeners = element.attributeListeners_;
final List<HtmlAttributeChangeListener> listeners = new ArrayList<>(element.attributeListeners_);
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
synchronized (listeners) {
for (final HtmlAttributeChangeListener listener : listeners) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,27 @@ public void execute() {
*/
@Override
public void attributeAdded(final HtmlAttributeChangeEvent event) {
// nothing to do
attributeChanged(event, "MutationObserver.attributeAdded", false);
}

/**
* {@inheritDoc}
*/
@Override
public void attributeRemoved(final HtmlAttributeChangeEvent event) {
// nothing to do
attributeChanged(event, "MutationObserver.attributeRemoved", true);
}

/**
* {@inheritDoc}
*/
@Override
public void attributeReplaced(final HtmlAttributeChangeEvent event) {
attributeChanged(event, "MutationObserver.attributeReplaced", true);
}

private void attributeChanged(final HtmlAttributeChangeEvent event, final String actionTitle,
final boolean includeOldValue) {
final HtmlElement target = event.getHtmlElement();
if (subtree_ || target == node_.getDomNodeOrDie()) {
final String attributeName = event.getName();
Expand All @@ -190,15 +195,15 @@ public void attributeReplaced(final HtmlAttributeChangeEvent event) {
mutationRecord.setAttributeName(attributeName);
mutationRecord.setType("attributes");
mutationRecord.setTarget(target.getScriptableObject());
if (attributeOldValue_) {
if (includeOldValue && attributeOldValue_) {
mutationRecord.setOldValue(event.getValue());
}

final Window window = getWindow();
final HtmlPage owningPage = (HtmlPage) window.getDocument().getPage();
final JavaScriptEngine jsEngine =
(JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
jsEngine.addPostponedAction(new PostponedAction(owningPage, "MutationObserver.attributeReplaced") {
jsEngine.addPostponedAction(new PostponedAction(owningPage, actionTitle) {
@Override
public void execute() {
final Scriptable array = JavaScriptEngine.newArray(scope, new Object[] {mutationRecord});
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/htmlunit/javascript/host/dom/MutationRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
*/
package org.htmlunit.javascript.host.dom;

import java.util.ArrayList;

import org.htmlunit.corejs.javascript.ScriptableObject;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxGetter;
import org.htmlunit.javascript.host.html.HTMLElement;

/**
* A JavaScript object for {@code MutationRecord}.
Expand Down Expand Up @@ -128,6 +131,13 @@ void setAddedNodes(final NodeList addedNodes) {
*/
@JsxGetter
public NodeList getAddedNodes() {
if (addedNodes_ == null && target_ instanceof HTMLElement) {
final NodeList addedNodes = new NodeList(((HTMLElement) target_).getDomNodeOrDie(), new ArrayList<>());
addedNodes.setParentScope(getParentScope());
addedNodes.setPrototype(getPrototype(addedNodes.getClass()));

addedNodes_ = addedNodes;
}
return addedNodes_;
}

Expand All @@ -144,6 +154,13 @@ void setRemovedNodes(final NodeList removedNodes) {
*/
@JsxGetter
public NodeList getRemovedNodes() {
if (removedNodes_ == null && target_ instanceof HTMLElement) {
final NodeList removedNodes = new NodeList(((HTMLElement) target_).getDomNodeOrDie(), new ArrayList<>());
removedNodes.setParentScope(getParentScope());
removedNodes.setPrototype(getPrototype(removedNodes.getClass()));

removedNodes_ = removedNodes;
}
return removedNodes_;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,63 @@ public void attributeValue() throws Exception {
verifyTitle2(driver, expected);
}


/**
* Test case for issue #1811.
* @throws Exception if the test fails
*/
@Test
@Alerts({"heho", "attributes", "value", "null", "x", "abc", "0", "0",
"heho", "attributes", "value", "null", "null", "abc", "0", "0"})
public void attributeValueAddRemove() throws Exception {
final String html
= "<html>\n"
+ "<head><script>\n"
+ LOG_TITLE_FUNCTION
+ " function test() {\n"
+ " var config = { attributes: true, childList: true, characterData: true, subtree: true };\n"
+ " var observer = new MutationObserver(function(mutations) {\n"
+ " mutations.forEach(function(mutation) {\n"
+ " log(mutation.type);\n"
+ " log(mutation.attributeName);\n"
+ " log(mutation.oldValue);\n"
+ " log(mutation.target.getAttribute(\"value\"));\n"
+ " log(mutation.target.value);\n"
+ " log(mutation.addedNodes.length);\n"
+ " log(mutation.removedNodes.length);\n"
+ " });\n"
+ " });\n"
+ " observer.observe(document.getElementById('tester'), config);\n"
+ " }\n"
+ "</script></head>\n"
+ "<body onload='test()'>\n"
+ " <input id='tester'>\n"
+ " <button id='doAlert' onclick='log(\"heho\");'>DoAlert</button>\n"
+ " <button id='doIt' "
+ "onclick='document.getElementById(\"tester\").setAttribute(\"value\", \"x\")'>"
+ "DoIt</button>\n"
+ " <button id='doItAgain' "
+ " onclick='document.getElementById(\"tester\").removeAttribute(\"value\")'>"
+ "DoItAgain</button>\n"
+ "</body></html>";
final WebDriver driver = loadPage2(html);
driver.findElement(By.id("tester")).sendKeys("abc");
verifyTitle2(driver, new String[] {});

driver.findElement(By.id("doAlert")).click();
verifyTitle2(driver, new String[] {"heho"});

final String[] expected = getExpectedAlerts();
driver.findElement(By.id("doIt")).click();
verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 8));

driver.findElement(By.id("doAlert")).click();
verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 9));

driver.findElement(By.id("doItAgain")).click();
verifyTitle2(driver, expected);
}

/**
* @throws Exception if an error occurs
*/
Expand Down

0 comments on commit d1eb495

Please sign in to comment.