[tiler] Unittests

fx run-host-tests tiler_unittests

Change-Id: Ie3f52e65751ab3f935d18eee77d3ab079ceceecf
diff --git a/lib/tiler/BUILD.gn b/lib/tiler/BUILD.gn
index d834a12..5d28ccd 100644
--- a/lib/tiler/BUILD.gn
+++ b/lib/tiler/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/dart/dart_library.gni")
+import("//topaz/runtime/dart/flutter_test.gni")
 
 dart_library("tiler") {
   package_name = "tiler"
@@ -15,3 +16,15 @@
     "//third_party/dart-pkg/git/flutter/packages/flutter",
   ]
 }
+
+flutter_test("tiler_unittests") {
+  sources = [
+    "tiler_test.dart",
+  ]
+
+  deps = [
+    ":tiler",
+    "//third_party/dart-pkg/git/flutter/packages/flutter_test",
+  ]
+}
+
diff --git a/lib/tiler/lib/src/tile.dart b/lib/tiler/lib/src/tile.dart
index db89c43..37fa7a9 100644
--- a/lib/tiler/lib/src/tile.dart
+++ b/lib/tiler/lib/src/tile.dart
@@ -210,6 +210,7 @@
   void notify() => notifyListeners();
 
   @override
-  String toString() =>
-      type == TileType.content ? '$type($flex)' : '$type $tiles';
+  String toString() => type == TileType.content
+      ? '($content)'
+      : type == TileType.row ? 'row $tiles' : 'column $tiles';
 }
diff --git a/lib/tiler/lib/src/tiler.dart b/lib/tiler/lib/src/tiler.dart
index 258a4ec..96c803e 100644
--- a/lib/tiler/lib/src/tiler.dart
+++ b/lib/tiler/lib/src/tiler.dart
@@ -77,14 +77,13 @@
   TileModel split({
     @required TileModel tile,
     Object content,
-    AxisDirection direction,
+    AxisDirection direction = AxisDirection.right,
     double flex = 0.5,
   }) {
     assert(tile != null);
     assert(flex > 0 && flex < 1);
 
     final parent = tile.parent;
-    int index = parent?.tiles?.indexOf(tile) ?? 0;
 
     final newTile = TileModel(
       type: TileType.content,
@@ -92,48 +91,32 @@
       flex: flex,
     );
 
-    // If we are splitting in the same axis as the parent, just add the
-    // tile to the parent's tiles.
-    if (parent?.type == TileType.row && _isVertical(direction) ||
-        parent?.type == TileType.column && _isHorizontal(direction)) {
-      parent.tiles.insert(
-        axisDirectionIsReversed(direction) ? index : index + 1,
-        newTile,
-      );
-      newTile.parent = parent;
+    final newParent = TileModel(
+      type: _isHorizontal(direction) ? TileType.column : TileType.row,
+      tiles: axisDirectionIsReversed(direction)
+          ? [newTile, tile]
+          : [tile, newTile],
+    );
 
-      double oldFlex = tile.flex;
-      newTile.flex = oldFlex - oldFlex * flex;
-      tile.flex = oldFlex - newTile.flex;
+    // If parent is null, tile should be the root tile.
+    if (parent == null) {
+      assert(tile == root);
+      root = newParent;
     } else {
-      // Create a new parent that holds the tile being split and the new tile.
-      final newParent = TileModel(
-        type: _isHorizontal(direction) ? TileType.column : TileType.row,
-        tiles: axisDirectionIsReversed(direction)
-            ? [newTile, tile]
-            : [tile, newTile],
-      );
-
-      if (parent == null) {
-        // If parent is null, tile should be the root tile.
-        assert(tile == root);
-        root = newParent;
-      } else {
-        parent.tiles.remove(tile);
-
-        // Copy existing flex and resize offsets from tile.
-        newParent.copy(tile);
-        tile.reset();
-
-        newParent.parent = parent;
-        parent.tiles.insert(index, newParent);
-      }
-      newTile.parent = newParent;
-      tile
-        ..parent = newParent
-        ..flex = 1 - flex;
+      int index = parent?.tiles?.indexOf(tile) ?? 0;
+      parent?.tiles?.remove(tile);
+      // Copy existing flex and resize offsets from tile.
+      newParent.copy(tile);
+      tile.reset();
+      newParent.parent = parent;
+      parent.tiles.insert(index, newParent);
     }
 
+    newTile.parent = newParent;
+    tile
+      ..parent = newParent
+      ..flex = 1 - flex;
+
     notifyListeners();
 
     return newTile;
@@ -155,10 +138,8 @@
 
     if (root == null) {
       root = tile;
-    } else if (nearTile == null) {
-      tile.parent = root;
-      root.tiles.add(tile);
     } else {
+      nearTile ??= root;
       _insert(nearTile, tile, direction);
     }
 
@@ -194,17 +175,22 @@
       if (parent.tiles.isEmpty) {
         // Remove empty parent.
         _remove(parent);
-      } else if (parent.tiles.length == 1 && parent.parent != null) {
+      } else if (parent.tiles.length == 1) {
         // For parent with only one child, move the child to it's grand parent.
         // and remove the parent.
         final grandParent = parent.parent;
-        final index = grandParent.tiles.indexOf(parent);
-        var child = parent.tiles.first;
-        parent.tiles.remove(child);
-        grandParent.tiles.remove(parent);
+        if (grandParent != null) {
+          final index = grandParent.tiles.indexOf(parent);
+          var child = parent.tiles.first;
+          parent.tiles.remove(child);
+          grandParent.tiles.remove(parent);
 
-        child.parent = grandParent;
-        grandParent.tiles.insert(index, child);
+          child.parent = grandParent;
+          grandParent.tiles.insert(index, child);
+        } else {
+          assert(parent == root);
+          root = parent.tiles.first..parent = null;
+        }
       }
       tile.parent = null;
     }
diff --git a/lib/tiler/test/tiler_test.dart b/lib/tiler/test/tiler_test.dart
new file mode 100644
index 0000000..3d37418
--- /dev/null
+++ b/lib/tiler/test/tiler_test.dart
@@ -0,0 +1,307 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:tiler/tiler.dart';
+
+void main() {
+  group('init', () {
+    test('should create an instance of tiler', () {
+      expect(
+          Tiler(
+            model: null,
+            chromeBuilder: null,
+            sizerBuilder: null,
+          ),
+          isNotNull);
+    });
+    test('should create an instance of TilerModel with null root', () {
+      final tiler = TilerModel();
+      expect(tiler, isNotNull);
+      expect(tiler.root, isNull);
+    });
+    test('should create an instance of TilerModel with content tile', () {
+      final tiler = TilerModel()..add(content: 'x');
+      expect(tiler.root.type, TileType.content);
+      expect(tiler.root.content, 'x');
+    });
+    test('should allow initializing with empty row or column', () {
+      final tiler = TilerModel(root: TileModel(type: TileType.row));
+      expect(tiler.root.type, TileType.row);
+      expect(tiler.root.tiles.isEmpty, true);
+    });
+  });
+
+  group('add tile', () {
+    test('should allow specifying nearTile = null with non-null root', () {
+      final tiler =
+          TilerModel(root: TileModel(type: TileType.content, content: 'root'))
+            ..add(content: 'new');
+      expect(tiler.root.type, TileType.column);
+      expect(tiler.root.tiles.length, 2);
+      expect(tiler.root.tiles.first.content, 'root');
+      expect(tiler.root.tiles.last.content, 'new');
+    });
+    test('should allow specifying nearTile = null with null root', () {
+      final tiler = TilerModel()..add(content: 'new');
+      expect(tiler.root.type, TileType.content);
+      expect(tiler.root.tiles.length, 0);
+      expect(tiler.root.content, 'new');
+    });
+    test('should add to the right of given tile in a column', () {
+      final tiler = TilerModel(
+        root: TileModel(type: TileType.column, tiles: [
+          TileModel(
+            type: TileType.content,
+            content: 'near',
+          ),
+        ]),
+      );
+      final nearTile = tiler.root.tiles.first;
+      tiler.add(
+        nearTile: nearTile,
+        direction: AxisDirection.right,
+        content: 'right',
+      );
+
+      expect(tiler.root.tiles.first.content, 'near');
+      expect(tiler.root.tiles.last.content, 'right');
+    });
+    test('should add to the left of given tile in a column', () {
+      final tiler = TilerModel(
+        root: TileModel(type: TileType.column, tiles: [
+          TileModel(
+            type: TileType.content,
+            content: 'near',
+          ),
+        ]),
+      );
+      final nearTile = tiler.root.tiles.first;
+      tiler.add(
+        nearTile: nearTile,
+        direction: AxisDirection.left,
+        content: 'left',
+      );
+
+      expect(tiler.root.tiles.last.content, 'near');
+      expect(tiler.root.tiles.first.content, 'left');
+    });
+    test('should add to the top of given tile in a column', () {
+      final tiler = TilerModel(
+        root: TileModel(type: TileType.column, tiles: [
+          TileModel(
+            type: TileType.content,
+            content: 'near',
+          ),
+        ]),
+      );
+      final nearTile = tiler.root.tiles.first;
+      tiler.add(
+        nearTile: nearTile,
+        direction: AxisDirection.up,
+        content: 'up',
+      );
+
+      // Expected layout:
+      // TileType.row [
+      //   TileType.content('up'),
+      //   TileType.column [
+      //     TileType.content('near')
+      //   ]
+      // ]
+      expect(tiler.root.tiles.last.tiles.first.content, 'near');
+      expect(tiler.root.tiles.first.content, 'up');
+    });
+    test('should add to the bottom of given tile in a column', () {
+      final tiler = TilerModel(
+        root: TileModel(type: TileType.column, tiles: [
+          TileModel(
+            type: TileType.content,
+            content: 'near',
+          ),
+        ]),
+      );
+      final nearTile = tiler.root.tiles.first;
+      tiler.add(
+        nearTile: nearTile,
+        direction: AxisDirection.down,
+        content: 'down',
+      );
+
+      // Expected layout:
+      // TileType.row [
+      //   TileType.column [
+      //     TileType.content('near')
+      //   ],
+      //   TileType.content('down')
+      // ]
+      expect(tiler.root.tiles.first.tiles.first.content, 'near');
+      expect(tiler.root.tiles.last.content, 'down');
+    });
+  });
+
+  group('split tile', () {
+    test('should not allow splitting a null tile', () {
+      expect(() => TilerModel()..split(tile: null), throwsAssertionError);
+    });
+    test('should reparent a tile being split', () {
+      final tiler = TilerModel(
+          root: TileModel(
+        type: TileType.content,
+        content: 'root',
+      ));
+      tiler.split(tile: tiler.root, content: 'new');
+      expect(tiler.root.type, TileType.column);
+      expect(tiler.root.tiles.length, 2);
+      expect(tiler.root.tiles.first.content, 'root');
+      expect(tiler.root.tiles.last.content, 'new');
+    });
+    test('should reparent tile if split in same direction', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.column,
+          tiles: [
+            TileModel(
+              type: TileType.content,
+              content: 'first',
+            )
+          ],
+        ),
+      );
+      tiler.split(
+        tile: tiler.root.tiles.first,
+        content: 'new',
+        direction: AxisDirection.right,
+      );
+      // Expected layout:
+      // column [column [(first), (new)]]
+      expect(tiler.root.type, TileType.column);
+      expect(tiler.root.tiles.length, 1);
+      expect(tiler.root.tiles.first.type, TileType.column);
+      expect(tiler.root.tiles.first.tiles.length, 2);
+      expect(tiler.root.tiles.first.tiles.first.content, 'first');
+      expect(tiler.root.tiles.first.tiles.last.content, 'new');
+    });
+    test('should reparent tile if split in cross direction(up)', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.column,
+          tiles: [
+            TileModel(
+              type: TileType.content,
+              content: 'first',
+            )
+          ],
+        ),
+      );
+      tiler.split(
+        tile: tiler.root.tiles.first,
+        content: 'new',
+        direction: AxisDirection.up,
+      );
+      // Expected layout:
+      // column [row [content(new), content(first)]]
+      expect(tiler.root.type, TileType.column);
+      expect(tiler.root.tiles.length, 1);
+      expect(tiler.root.tiles.first.type, TileType.row);
+      expect(tiler.root.tiles.first.tiles.length, 2);
+      expect(tiler.root.tiles.first.tiles.first.content, 'new');
+      expect(tiler.root.tiles.first.tiles.last.content, 'first');
+    });
+    test('should reparent tile if split in cross direction(down)', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.column,
+          tiles: [
+            TileModel(
+              type: TileType.content,
+              content: 'first',
+            )
+          ],
+        ),
+      );
+      tiler.split(
+        tile: tiler.root.tiles.first,
+        content: 'new',
+        direction: AxisDirection.down,
+      );
+      // Expected layout:
+      // column [row [content(first), content(new)]]
+      expect(tiler.root.type, TileType.column);
+      expect(tiler.root.tiles.length, 1);
+      expect(tiler.root.tiles.first.type, TileType.row);
+      expect(tiler.root.tiles.first.tiles.length, 2);
+      expect(tiler.root.tiles.first.tiles.first.content, 'first');
+      expect(tiler.root.tiles.first.tiles.last.content, 'new');
+    });
+  });
+
+  group('remove tile', () {
+    test('should not allow removing a null tile', () {
+      expect(() => TilerModel()..remove(null), throwsAssertionError);
+    });
+    test('should allow removing the root', () {
+      final tiler = TilerModel(
+          root: TileModel(
+        type: TileType.content,
+        content: 'root',
+      ));
+      tiler.remove(tiler.root);
+      expect(tiler.root, isNull);
+    });
+    test('should remove empty parent, including root', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.row,
+          tiles: [
+            TileModel(type: TileType.content),
+          ],
+        ),
+      );
+
+      tiler.remove(tiler.root.tiles.first);
+      expect(tiler.root, isNull);
+    });
+    test('should set root to last remaining tile', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.row,
+          tiles: [
+            TileModel(type: TileType.content, content: 'first'),
+            TileModel(type: TileType.content, content: 'second'),
+          ],
+        ),
+      );
+
+      tiler.remove(tiler.root.tiles.last);
+      expect(tiler.root.type, TileType.content);
+      expect(tiler.root.content, 'first');
+    });
+    test('should remove parent if left with one tile', () {
+      final tiler = TilerModel(
+        root: TileModel(
+          type: TileType.row,
+          tiles: [
+            TileModel(type: TileType.content, content: 'first'),
+            TileModel(
+              type: TileType.row,
+              tiles: [
+                TileModel(type: TileType.content, content: 'second'),
+                TileModel(type: TileType.content, content: 'third'),
+              ],
+            ),
+          ],
+        ),
+      );
+
+      tiler.remove(tiler.root.tiles.last.tiles.last);
+      expect(tiler.root.type, TileType.row);
+      expect(tiler.root.tiles.length, 2);
+      expect(tiler.root.tiles.first.content, 'first');
+      expect(tiler.root.tiles.last.content, 'second');
+    });
+  });
+}
diff --git a/packages/tests/BUILD.gn b/packages/tests/BUILD.gn
index 4d39c45..f623cb5 100644
--- a/packages/tests/BUILD.gn
+++ b/packages/tests/BUILD.gn
@@ -30,6 +30,7 @@
     "//topaz/lib/setui/settings/common:lib_setui_settings_common_test($host_toolchain)",
     "//topaz/lib/setui/settings/service:lib_setui_service_test($host_toolchain)",
     "//topaz/lib/setui/settings/testing:lib_setui_settings_testing_test($host_toolchain)",
+    "//topaz/lib/tiler:tiler_unittests($host_toolchain)",
     "//topaz/public/dart/composition_delegate:composition_delegate_tests($host_toolchain)",
     "//topaz/public/dart/fuchsia_inspect:fuchsia_inspect_package_unittests($host_toolchain)",
     "//topaz/public/dart/fuchsia_logger:fuchsia_logger_package_unittests($host_toolchain)",