Account for changed sibling position in modified check.

Fixes: 435708117
Test: npm run test:unit:ci
Change-Id: Ib94f2db30f848c1deb3716082df0f7fbdefa66f6
diff --git a/tools/winscope/src/trace/formatters.ts b/tools/winscope/src/trace/formatters.ts
index e6d524e..4f453c1 100644
--- a/tools/winscope/src/trace/formatters.ts
+++ b/tools/winscope/src/trace/formatters.ts
@@ -276,6 +276,7 @@
   EMPTY_OBJ_STRING,
   EnumFormatter,
   FixedStringFormatter,
+  formatAsHex,
   HEX_FORMATTER,
   LAYER_ID_FORMATTER,
   MATRIX_FORMATTER,
@@ -287,5 +288,4 @@
   TIMESTAMP_NODE_FORMATTER,
   TRANSFORM_FORMATTER,
   UPPER_CASE_FORMATTER,
-  formatAsHex,
 };
diff --git a/tools/winscope/src/viewers/common/abstract_add_diffs_test.ts b/tools/winscope/src/viewers/common/abstract_add_diffs_test.ts
new file mode 100644
index 0000000..2f9604e
--- /dev/null
+++ b/tools/winscope/src/viewers/common/abstract_add_diffs_test.ts
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {UiTreeNodeUtils} from 'test/unit/ui_tree_node_utils';
+import {DiffType} from 'viewers/common/diff_type';
+import {AddDiffs} from './add_diffs';
+import {DiffNode} from './diff_node';
+
+export abstract class AbstractAddDiffsTest<T extends DiffNode> {
+  execute() {
+    describe('AddDiffs', () => {
+      let newRoot: T;
+      let oldRoot: T;
+      let expectedRoot: T;
+      let addDiffs: AddDiffs<T>;
+
+      beforeAll(() => {
+        addDiffs = this.makeAddDiffsOperation();
+      });
+
+      beforeEach(() => {
+        jasmine.addCustomEqualityTester(UiTreeNodeUtils.treeNodeEqualityTester);
+        newRoot = this.makeRoot();
+        oldRoot = this.makeRoot();
+        expectedRoot = this.makeRoot();
+      });
+
+      it('handles two identical trees', async () => {
+        await addDiffs.executeInPlace(newRoot, newRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds MODIFIED', async () => {
+        this.makeChildAndAddToRoot(newRoot);
+        this.makeChildAndAddToRoot(oldRoot, 'oldValue');
+
+        const expectedChild = this.makeChildAndAddToRoot(expectedRoot);
+        expectedChild.setDiff(DiffType.MODIFIED);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds MODIFIED if old node comes before new node in siblings', async () => {
+        this.makeChildAndAddToRoot(newRoot, 'value', 'child1');
+        this.makeChildAndAddToRoot(newRoot, 'newValue', 'child2');
+
+        this.makeChildAndAddToRoot(oldRoot, 'oldValue', 'child2');
+
+        const expectedChild1 = this.makeChildAndAddToRoot(
+          expectedRoot,
+          'value',
+          'child1',
+        );
+        expectedChild1.setDiff(DiffType.ADDED);
+        const expectedChild2 = this.makeChildAndAddToRoot(
+          expectedRoot,
+          'newValue',
+          'child2',
+        );
+        expectedChild2.setDiff(DiffType.MODIFIED);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds MODIFIED if old node comes after new node in siblings', async () => {
+        this.makeChildAndAddToRoot(newRoot, 'newValue', 'child2');
+
+        this.makeChildAndAddToRoot(oldRoot, 'value', 'child1');
+        this.makeChildAndAddToRoot(oldRoot, 'oldValue', 'child2');
+
+        const expectedChild1 = this.makeChildAndAddToRoot(
+          expectedRoot,
+          'value',
+          'child1',
+        );
+        expectedChild1.setDiff(DiffType.DELETED);
+        const expectedChild2 = this.makeChildAndAddToRoot(
+          expectedRoot,
+          'newValue',
+          'child2',
+        );
+        expectedChild2.setDiff(DiffType.MODIFIED);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('does not add MODIFIED to root', async () => {
+        oldRoot = this.makeRoot('oldValue');
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds ADDED', async () => {
+        this.makeChildAndAddToRoot(newRoot);
+
+        const expectedChild = this.makeChildAndAddToRoot(expectedRoot);
+        expectedChild.setDiff(DiffType.ADDED);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds DELETED', async () => {
+        const oldChild1 = this.makeChildAndAddToRoot(
+          oldRoot,
+          undefined,
+          'child1',
+        );
+        this.makeChildAndAddToRoot(oldChild1, undefined, 'child2');
+
+        const expectedChild1 = this.makeChildAndAddToRoot(
+          expectedRoot,
+          undefined,
+          'child1',
+        );
+        const expectedChild2 = this.makeChildAndAddToRoot(
+          expectedChild1,
+          undefined,
+          'child2',
+        );
+        expectedChild1.setDiff(DiffType.DELETED);
+        expectedChild2.setDiff(DiffType.DELETED);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      this.executeSpecializedTests();
+    });
+  }
+
+  abstract makeAddDiffsOperation(): AddDiffs<T>;
+  abstract makeRoot(value?: string): T;
+  abstract makeChildAndAddToRoot(rootNode: T, value?: string, name?: string): T;
+  abstract executeSpecializedTests(): void;
+}
diff --git a/tools/winscope/src/viewers/common/add_diffs.ts b/tools/winscope/src/viewers/common/add_diffs.ts
index a6ecb24..65f4d44 100644
--- a/tools/winscope/src/viewers/common/add_diffs.ts
+++ b/tools/winscope/src/viewers/common/add_diffs.ts
@@ -53,113 +53,133 @@
     newNodeSiblingIds: string[],
     oldNodeSiblingIds: string[],
   ): Promise<T[]> {
-    const diffNodes: T[] = [];
-
-    if (!oldNode && !newNode) {
+    if (newNode === undefined && oldNode === undefined) {
       console.error('both new and old trees undefined');
-      return diffNodes;
+      return [];
     }
 
-    if (!newNode && oldNode) {
-      if (!newNodeSiblingIds.includes(oldNode.id)) {
-        //oldNode deleted or moved
-        if (this.newIdNodeMap.get(oldNode.id)) {
-          oldNode.setDiff(DiffType.DELETED_MOVE);
-        } else {
-          oldNode.setDiff(DiffType.DELETED);
-        }
-        const newChildren = await this.visitChildren(undefined, oldNode);
-        oldNode.removeAllChildren();
-        newChildren.forEach((child) => {
-          assertDefined(oldNode).addOrReplaceChild(child);
-        });
-        this.processOldNode(oldNode);
-        diffNodes.push(oldNode);
+    if (newNode === undefined && oldNode !== undefined) {
+      if (newNodeSiblingIds.includes(oldNode.id)) {
+        // node still at same hierarchy level - no diffs
+        return [];
       }
-      return diffNodes;
+      await this.processDeletedOrMovedNode(oldNode);
+      return [oldNode];
     }
 
     newNode = assertDefined(newNode);
 
-    if (!newNode.isRoot() && newNode.id !== oldNode?.id) {
-      let nextOldNode: T | undefined;
-
-      if (!oldNodeSiblingIds.includes(newNode.id)) {
-        if (this.oldIdNodeMap.get(newNode.id)) {
-          if (this.addDiffsToNewRoot) {
-            newNode.setDiff(DiffType.ADDED_MOVE);
-            nextOldNode = this.oldIdNodeMap.get(newNode.id);
-          }
-        } else {
-          newNode.setDiff(DiffType.ADDED);
-          nextOldNode = undefined; //newNode has no equiv in old tree
-        }
-      }
-
-      if (oldNode && !newNodeSiblingIds.includes(oldNode.id)) {
-        if (this.newIdNodeMap.get(oldNode.id)) {
-          //oldNode still exists
-          oldNode.setDiff(DiffType.DELETED_MOVE);
-          nextOldNode = undefined;
-        } else {
-          oldNode.setDiff(DiffType.DELETED);
-
-          const newChildren = await this.visitChildren(undefined, oldNode);
-          oldNode.removeAllChildren();
-
-          newChildren.forEach((child) => {
-            assertDefined(oldNode).addOrReplaceChild(child);
-          });
-        }
-        this.processOldNode(oldNode);
-        diffNodes.push(oldNode);
-      }
-
-      oldNode = nextOldNode;
-    } else if (!newNode.isRoot()) {
-      if (
-        oldNode &&
-        oldNode.id === newNode.id &&
-        (await this.isModified(newNode, oldNode, this.denylistProperties))
-      ) {
-        this.processModifiedNodes(newNode, oldNode);
-      }
+    if (!newNode.isRoot()) {
+      const diffNodes = await this.generateDiffNodesForNonRoot(
+        newNode,
+        oldNode,
+        newNodeSiblingIds,
+        oldNodeSiblingIds,
+      );
+      return diffNodes;
     }
 
+    await this.processNewNodeChildren(newNode, oldNode);
+
+    return [newNode];
+  }
+
+  private async processNewNodeChildren(newNode: T, oldNode: T | undefined) {
     const newChildren = await this.visitChildren(newNode, oldNode);
     newNode.removeAllChildren();
     newChildren.forEach((child) =>
       assertDefined(newNode).addOrReplaceChild(child),
     );
+  }
 
+  private async generateDiffNodesForNonRoot(
+    newNode: T,
+    oldNode: T | undefined,
+    newNodeSiblingIds: string[],
+    oldNodeSiblingIds: string[],
+  ) {
+    const diffNodes: T[] = [];
+    let nextOldNode = oldNode;
+
+    if (newNode.id !== oldNode?.id) {
+      if (!oldNodeSiblingIds.includes(newNode.id)) {
+        // newNode not in oldNode at same level
+        nextOldNode = this.processAddedOrMovedNode(newNode);
+      }
+
+      if (oldNode !== undefined && !newNodeSiblingIds.includes(oldNode.id)) {
+        // oldNode not in newNode at same level
+        await this.processDeletedOrMovedNode(oldNode);
+        diffNodes.push(oldNode);
+      }
+
+      if (oldNodeSiblingIds.includes(newNode.id)) {
+        // oldNode is in newNode at same level
+        nextOldNode = assertDefined(this.oldIdNodeMap.get(newNode.id));
+      }
+    }
+
+    if (
+      newNode.id === nextOldNode?.id &&
+      (await this.isModified(newNode, nextOldNode, this.denylistProperties))
+    ) {
+      this.processModifiedNodes(newNode, nextOldNode);
+    }
+
+    await this.processNewNodeChildren(newNode, nextOldNode);
     diffNodes.push(newNode);
     return diffNodes;
   }
 
-  async visitChildren(
+  private processAddedOrMovedNode(newNode: T): T | undefined {
+    if (this.oldIdNodeMap.get(newNode.id) && this.addDiffsToNewRoot) {
+      newNode.setDiff(DiffType.ADDED_MOVE);
+      return this.oldIdNodeMap.get(newNode.id);
+    }
+    newNode.setDiff(DiffType.ADDED);
+    return undefined;
+  }
+
+  private async processDeletedOrMovedNode(oldNode: T): Promise<void> {
+    if (this.newIdNodeMap.get(oldNode.id)) {
+      oldNode.setDiff(DiffType.DELETED_MOVE);
+    } else {
+      oldNode.setDiff(DiffType.DELETED);
+    }
+    const newChildren = await this.visitChildren(undefined, oldNode);
+    oldNode.removeAllChildren();
+    newChildren.forEach((child) => {
+      assertDefined(oldNode).addOrReplaceChild(child);
+    });
+    this.processOldNode(oldNode);
+  }
+
+  private async visitChildren(
     newNode: T | undefined,
     oldNode: T | undefined,
   ): Promise<T[]> {
     const diffChildren: T[] = [];
+
+    const newNodeChildren = newNode?.getAllChildren() ?? [];
+    const oldNodeChildren = oldNode?.getAllChildren() ?? [];
+
     const numOfChildren = Math.max(
-      newNode?.getAllChildren().length ?? 0,
-      oldNode?.getAllChildren().length ?? 0,
+      newNodeChildren.length,
+      oldNodeChildren.length,
     );
     for (let i = 0; i < numOfChildren; i++) {
-      const newChild = newNode?.getAllChildren()[i];
-      let oldChild = oldNode?.getAllChildren()[i];
+      const newChild = newNodeChildren[i];
+      let oldChild: T | undefined = oldNodeChildren[i];
 
       if (!oldChild && newChild) {
-        oldChild = oldNode
-          ?.getAllChildren()
-          .find((node) => node.name === newChild.name);
+        oldChild = oldNodeChildren.find((node) => node.name === newChild.name);
       }
 
       const childDiffTrees = await this.generateDiffNodes(
         newChild,
         oldChild,
-        newNode?.getAllChildren().map((child) => child.id) ?? [],
-        oldNode?.getAllChildren().map((child) => child.id) ?? [],
+        newNodeChildren.map((child) => child.id),
+        oldNodeChildren.map((child) => child.id),
       );
       childDiffTrees.forEach((child) => diffChildren.push(child));
     }
diff --git a/tools/winscope/src/viewers/common/add_diffs_hierarchy_tree_test.ts b/tools/winscope/src/viewers/common/add_diffs_hierarchy_tree_test.ts
index f1ccf3c..61d705e 100644
--- a/tools/winscope/src/viewers/common/add_diffs_hierarchy_tree_test.ts
+++ b/tools/winscope/src/viewers/common/add_diffs_hierarchy_tree_test.ts
@@ -17,92 +17,30 @@
 import {UiTreeNodeUtils} from 'test/unit/ui_tree_node_utils';
 import {TreeNode} from 'tree_node/tree_node';
 import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
+import {AbstractAddDiffsTest} from './abstract_add_diffs_test';
+import {AddDiffs} from './add_diffs';
 import {AddDiffsHierarchyTree} from './add_diffs_hierarchy_tree';
-import {executeAddDiffsTests} from './add_diffs_test_utils';
 import {DiffType} from './diff_type';
 
-describe('AddDiffsHierarchyTree', () => {
-  let newRoot: UiHierarchyTreeNode;
-  let oldRoot: UiHierarchyTreeNode;
-  let expectedRoot: UiHierarchyTreeNode;
+class AddDiffsHierarchyTreeTest extends AbstractAddDiffsTest<UiHierarchyTreeNode> {
+  override makeAddDiffsOperation(): AddDiffs<UiHierarchyTreeNode> {
+    const isModified = async (
+      newTree: TreeNode | undefined,
+      oldTree: TreeNode | undefined,
+    ) => {
+      return (
+        (newTree as UiHierarchyTreeNode)
+          .getEagerPropertyByName('exampleProperty')
+          ?.getValue() !==
+        (oldTree as UiHierarchyTreeNode)
+          .getEagerPropertyByName('exampleProperty')
+          ?.getValue()
+      );
+    };
+    return new AddDiffsHierarchyTree(isModified, []);
+  }
 
-  const isModified = async (
-    newTree: TreeNode | undefined,
-    oldTree: TreeNode | undefined,
-    denylistProperties: string[],
-  ) => {
-    return (
-      (newTree as UiHierarchyTreeNode)
-        .getEagerPropertyByName('exampleProperty')
-        ?.getValue() !==
-      (oldTree as UiHierarchyTreeNode)
-        .getEagerPropertyByName('exampleProperty')
-        ?.getValue()
-    );
-  };
-  const addDiffs = new AddDiffsHierarchyTree(isModified, []);
-
-  describe('AddDiffs tests', () => {
-    executeAddDiffsTests(
-      UiTreeNodeUtils.treeNodeEqualityTester,
-      makeRoot,
-      makeChildAndAddToRoot,
-      addDiffs,
-    );
-  });
-
-  describe('Hierarchy tree tests', () => {
-    beforeEach(() => {
-      jasmine.addCustomEqualityTester(UiTreeNodeUtils.treeNodeEqualityTester);
-      newRoot = makeRoot();
-      oldRoot = makeRoot();
-      expectedRoot = makeRoot();
-    });
-
-    it('does not add MODIFIED to hierarchy root', async () => {
-      oldRoot = makeRoot('oldValue');
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('adds ADDED_MOVE and DELETED_MOVE', async () => {
-      const newParent = makeParentAndAddToRoot(newRoot);
-      makeChildAndAddToRoot(newParent);
-      makeParentAndAddToRoot(oldRoot);
-      makeChildAndAddToRoot(oldRoot);
-
-      const expectedParent = makeParentAndAddToRoot(expectedRoot);
-
-      const expectedNewChild = makeChildAndAddToRoot(expectedParent);
-      expectedNewChild.setDiff(DiffType.ADDED_MOVE);
-
-      const expectedOldChild = makeChildAndAddToRoot(expectedRoot);
-      expectedOldChild.setDiff(DiffType.DELETED_MOVE);
-
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('adds ADDED, ADDED_MOVE and DELETED_MOVE', async () => {
-      const newParent = makeParentAndAddToRoot(newRoot);
-      makeChildAndAddToRoot(newParent);
-      makeChildAndAddToRoot(oldRoot);
-
-      const expectedOldChild = makeChildAndAddToRoot(expectedRoot);
-      expectedOldChild.setDiff(DiffType.DELETED_MOVE);
-
-      const expectedParent = makeParentAndAddToRoot(expectedRoot);
-      expectedParent.setDiff(DiffType.ADDED);
-
-      const expectedNewChild = makeChildAndAddToRoot(expectedParent);
-      expectedNewChild.setDiff(DiffType.ADDED_MOVE);
-
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-  });
-
-  function makeRoot(value = 'value'): UiHierarchyTreeNode {
+  override makeRoot(value = 'value'): UiHierarchyTreeNode {
     return UiTreeNodeUtils.makeUiHierarchyNode({
       id: 'test',
       name: 'root',
@@ -110,13 +48,14 @@
     });
   }
 
-  function makeChildAndAddToRoot(
+  override makeChildAndAddToRoot(
     rootNode: UiHierarchyTreeNode,
     value = 'value',
+    name = 'child',
   ): UiHierarchyTreeNode {
     const child = UiTreeNodeUtils.makeUiHierarchyNode({
       id: 'test node',
-      name: 'child',
+      name,
       exampleProperty: value,
     });
     rootNode.addOrReplaceChild(child);
@@ -124,7 +63,63 @@
     return child;
   }
 
-  function makeParentAndAddToRoot(
+  override executeSpecializedTests(): void {
+    describe('Specialized tests', () => {
+      let newRoot: UiHierarchyTreeNode;
+      let oldRoot: UiHierarchyTreeNode;
+      let expectedRoot: UiHierarchyTreeNode;
+      let addDiffs: AddDiffs<UiHierarchyTreeNode>;
+
+      beforeAll(() => {
+        addDiffs = this.makeAddDiffsOperation();
+      });
+
+      beforeEach(() => {
+        jasmine.addCustomEqualityTester(UiTreeNodeUtils.treeNodeEqualityTester);
+        newRoot = this.makeRoot();
+        oldRoot = this.makeRoot();
+        expectedRoot = this.makeRoot();
+      });
+
+      it('adds ADDED_MOVE and DELETED_MOVE', async () => {
+        const newParent = this.makeParentAndAddToRoot(newRoot);
+        this.makeChildAndAddToRoot(newParent);
+        this.makeParentAndAddToRoot(oldRoot);
+        this.makeChildAndAddToRoot(oldRoot);
+
+        const expectedParent = this.makeParentAndAddToRoot(expectedRoot);
+
+        const expectedNewChild = this.makeChildAndAddToRoot(expectedParent);
+        expectedNewChild.setDiff(DiffType.ADDED_MOVE);
+
+        const expectedOldChild = this.makeChildAndAddToRoot(expectedRoot);
+        expectedOldChild.setDiff(DiffType.DELETED_MOVE);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('adds ADDED, ADDED_MOVE and DELETED_MOVE', async () => {
+        const newParent = this.makeParentAndAddToRoot(newRoot);
+        this.makeChildAndAddToRoot(newParent);
+        this.makeChildAndAddToRoot(oldRoot);
+
+        const expectedOldChild = this.makeChildAndAddToRoot(expectedRoot);
+        expectedOldChild.setDiff(DiffType.DELETED_MOVE);
+
+        const expectedParent = this.makeParentAndAddToRoot(expectedRoot);
+        expectedParent.setDiff(DiffType.ADDED);
+
+        const expectedNewChild = this.makeChildAndAddToRoot(expectedParent);
+        expectedNewChild.setDiff(DiffType.ADDED_MOVE);
+
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+    });
+  }
+
+  private makeParentAndAddToRoot(
     rootNode: UiHierarchyTreeNode,
   ): UiHierarchyTreeNode {
     const parent = UiTreeNodeUtils.makeUiHierarchyNode({
@@ -136,4 +131,8 @@
     parent.setParent(rootNode);
     return parent;
   }
+}
+
+describe('AddDiffsHierarchyTree', () => {
+  new AddDiffsHierarchyTreeTest().execute();
 });
diff --git a/tools/winscope/src/viewers/common/add_diffs_properties_tree_test.ts b/tools/winscope/src/viewers/common/add_diffs_properties_tree_test.ts
index 2869183..014e947 100644
--- a/tools/winscope/src/viewers/common/add_diffs_properties_tree_test.ts
+++ b/tools/winscope/src/viewers/common/add_diffs_properties_tree_test.ts
@@ -16,72 +16,73 @@
 
 import {UiTreeNodeUtils} from 'test/unit/ui_tree_node_utils';
 import {TreeNode} from 'tree_node/tree_node';
+import {AbstractAddDiffsTest} from './abstract_add_diffs_test';
+import {AddDiffs} from './add_diffs';
 import {AddDiffsPropertiesTree} from './add_diffs_properties_tree';
-import {executeAddDiffsTests} from './add_diffs_test_utils';
 import {UiPropertyTreeNode} from './ui_property_tree_node';
 
-describe('AddDiffsPropertiesTree', () => {
-  let newRoot: UiPropertyTreeNode;
-  let oldRoot: UiPropertyTreeNode;
-  let expectedRoot: UiPropertyTreeNode;
+class AddDiffsPropertiesTreeTest extends AbstractAddDiffsTest<UiPropertyTreeNode> {
+  override makeAddDiffsOperation(): AddDiffs<UiPropertyTreeNode> {
+    const isModified = async (
+      newTree: TreeNode | undefined,
+      oldTree: TreeNode | undefined,
+    ) => {
+      return (
+        (newTree as UiPropertyTreeNode)?.getValue() !==
+        (oldTree as UiPropertyTreeNode)?.getValue()
+      );
+    };
+    return new AddDiffsPropertiesTree(isModified, []);
+  }
 
-  const isModified = async (
-    newTree: TreeNode | undefined,
-    oldTree: TreeNode | undefined,
-    denylistProperties: string[],
-  ) => {
-    return (
-      (newTree as UiPropertyTreeNode)?.getValue() !==
-      (oldTree as UiPropertyTreeNode)?.getValue()
-    );
-  };
-  const addDiffs = new AddDiffsPropertiesTree(isModified, []);
-
-  describe('AddDiffs tests', () => {
-    executeAddDiffsTests(
-      UiTreeNodeUtils.treeNodeEqualityTester,
-      makeRoot,
-      makeChildAndAddToRoot,
-      addDiffs,
-    );
-  });
-
-  describe('Property tree tests', () => {
-    beforeEach(() => {
-      jasmine.addCustomEqualityTester(UiTreeNodeUtils.treeNodeEqualityTester);
-      newRoot = makeRoot();
-      oldRoot = makeRoot();
-      expectedRoot = makeRoot();
-    });
-
-    it('does not add MODIFIED to property tree root', async () => {
-      oldRoot = makeRoot('oldValue');
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('does not add any diffs to property tree that has no old tree', async () => {
-      await addDiffs.executeInPlace(newRoot, undefined);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-  });
-
-  function makeRoot(value = 'value'): UiPropertyTreeNode {
+  makeRoot(value = 'value'): UiPropertyTreeNode {
     const root = UiTreeNodeUtils.makeUiPropertyNode('test', 'root', value);
     root.setIsRoot(true);
     return root;
   }
 
-  function makeChildAndAddToRoot(
+  makeChildAndAddToRoot(
     rootNode: UiPropertyTreeNode,
     value = 'value',
+    name = 'child',
   ): UiPropertyTreeNode {
-    const child = UiTreeNodeUtils.makeUiPropertyNode(
-      'test node',
-      'child',
-      value,
-    );
+    const child = UiTreeNodeUtils.makeUiPropertyNode('test node', name, value);
     rootNode.addOrReplaceChild(child);
     return child;
   }
+
+  override executeSpecializedTests(): void {
+    describe('Specialized tests', () => {
+      let newRoot: UiPropertyTreeNode;
+      let oldRoot: UiPropertyTreeNode;
+      let expectedRoot: UiPropertyTreeNode;
+      let addDiffs: AddDiffs<UiPropertyTreeNode>;
+
+      beforeAll(() => {
+        addDiffs = this.makeAddDiffsOperation();
+      });
+
+      beforeEach(() => {
+        jasmine.addCustomEqualityTester(UiTreeNodeUtils.treeNodeEqualityTester);
+        newRoot = this.makeRoot();
+        oldRoot = this.makeRoot();
+        expectedRoot = this.makeRoot();
+      });
+
+      it('does not add MODIFIED to property tree root', async () => {
+        oldRoot = this.makeRoot('oldValue');
+        await addDiffs.executeInPlace(newRoot, oldRoot);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+
+      it('does not add any diffs to property tree that has no old tree', async () => {
+        await addDiffs.executeInPlace(newRoot, undefined);
+        expect(newRoot).toEqual(expectedRoot);
+      });
+    });
+  }
+}
+
+describe('AddDiffsPropertiesTree', () => {
+  new AddDiffsPropertiesTreeTest().execute();
 });
diff --git a/tools/winscope/src/viewers/common/add_diffs_test_utils.ts b/tools/winscope/src/viewers/common/add_diffs_test_utils.ts
deleted file mode 100644
index 933ff21..0000000
--- a/tools/winscope/src/viewers/common/add_diffs_test_utils.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {DiffType} from 'viewers/common/diff_type';
-import {AddDiffs} from './add_diffs';
-import {DiffNode} from './diff_node';
-
-export function executeAddDiffsTests<T extends DiffNode>(
-  nodeEqualityTester: (first: any, second: any) => boolean | undefined,
-  makeRoot: (value?: string) => T,
-  makeChildAndAddToRoot: (rootNode: T, value?: string) => T,
-  addDiffs: AddDiffs<T>,
-) {
-  describe('AddDiffs', () => {
-    let newRoot: T;
-    let oldRoot: T;
-    let expectedRoot: T;
-
-    beforeEach(() => {
-      jasmine.addCustomEqualityTester(nodeEqualityTester);
-      newRoot = makeRoot();
-      oldRoot = makeRoot();
-      expectedRoot = makeRoot();
-    });
-
-    it('handles two identical trees', async () => {
-      await addDiffs.executeInPlace(newRoot, newRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('adds MODIFIED', async () => {
-      makeChildAndAddToRoot(newRoot);
-      makeChildAndAddToRoot(oldRoot, 'oldValue');
-
-      const expectedChild = makeChildAndAddToRoot(expectedRoot);
-      expectedChild.setDiff(DiffType.MODIFIED);
-
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('adds ADDED', async () => {
-      makeChildAndAddToRoot(newRoot);
-
-      const expectedChild = makeChildAndAddToRoot(expectedRoot);
-      expectedChild.setDiff(DiffType.ADDED);
-
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-
-    it('adds DELETED', async () => {
-      makeChildAndAddToRoot(oldRoot);
-
-      const expectedChild = makeChildAndAddToRoot(expectedRoot);
-      expectedChild.setDiff(DiffType.DELETED);
-
-      await addDiffs.executeInPlace(newRoot, oldRoot);
-      expect(newRoot).toEqual(expectedRoot);
-    });
-  });
-}