NodeTest.php 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. <?php
  2. use Illuminate\Database\Capsule\Manager as Capsule;
  3. use Kalnoy\Nestedset\NestedSet;
  4. class NodeTest extends PHPUnit\Framework\TestCase
  5. {
  6. public static function setUpBeforeClass(): void
  7. {
  8. $schema = Capsule::schema();
  9. $schema->dropIfExists('categories');
  10. Capsule::disableQueryLog();
  11. $schema->create('categories', function (\Illuminate\Database\Schema\Blueprint $table) {
  12. $table->increments('id');
  13. $table->string('name');
  14. $table->softDeletes();
  15. NestedSet::columns($table);
  16. });
  17. Capsule::enableQueryLog();
  18. }
  19. public function setUp(): void
  20. {
  21. $data = include __DIR__.'/data/categories.php';
  22. Capsule::table('categories')->insert($data);
  23. Capsule::flushQueryLog();
  24. Category::resetActionsPerformed();
  25. date_default_timezone_set('America/Denver');
  26. }
  27. public function tearDown(): void
  28. {
  29. Capsule::table('categories')->truncate();
  30. }
  31. // public static function tearDownAfterClass()
  32. // {
  33. // $log = Capsule::getQueryLog();
  34. // foreach ($log as $item) {
  35. // echo $item['query']." with ".implode(', ', $item['bindings'])."\n";
  36. // }
  37. // }
  38. public function assertTreeNotBroken($table = 'categories')
  39. {
  40. $checks = array();
  41. $connection = Capsule::connection();
  42. $table = $connection->getQueryGrammar()->wrapTable($table);
  43. // Check if lft and rgt values are ok
  44. $checks[] = "from $table where _lft >= _rgt or (_rgt - _lft) % 2 = 0";
  45. // Check if lft and rgt values are unique
  46. $checks[] = "from $table c1, $table c2 where c1.id <> c2.id and ".
  47. "(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)";
  48. // Check if parent_id is set correctly
  49. $checks[] = "from $table c, $table p, $table m where c.parent_id=p.id and m.id <> p.id and m.id <> c.id and ".
  50. "(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)";
  51. foreach ($checks as $i => $check) {
  52. $checks[$i] = 'select 1 as error '.$check;
  53. }
  54. $sql = 'select max(error) as errors from ('.implode(' union ', $checks).') _';
  55. $actual = $connection->selectOne($sql);
  56. $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!");
  57. $actual = (array)Capsule::connection()->selectOne($sql);
  58. $this->assertEquals(array('errors' => null), $actual, "The tree structure of $table is broken!");
  59. }
  60. public function dumpTree($items = null)
  61. {
  62. if ( ! $items) $items = Category::withTrashed()->defaultOrder()->get();
  63. foreach ($items as $item) {
  64. echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getKey().' '.$item->getLft()." ".$item->getRgt().' '.$item->getParentId();
  65. }
  66. }
  67. public function assertNodeReceivesValidValues($node)
  68. {
  69. $lft = $node->getLft();
  70. $rgt = $node->getRgt();
  71. $nodeInDb = $this->findCategory($node->name);
  72. $this->assertEquals(
  73. [ $nodeInDb->getLft(), $nodeInDb->getRgt() ],
  74. [ $lft, $rgt ],
  75. 'Node is not synced with database after save.'
  76. );
  77. }
  78. /**
  79. * @param $name
  80. *
  81. * @return \Category
  82. */
  83. public function findCategory($name, $withTrashed = false)
  84. {
  85. $q = new Category;
  86. $q = $withTrashed ? $q->withTrashed() : $q->newQuery();
  87. return $q->whereName($name)->first();
  88. }
  89. public function testTreeNotBroken()
  90. {
  91. $this->assertTreeNotBroken();
  92. $this->assertFalse(Category::isBroken());
  93. }
  94. public function nodeValues($node)
  95. {
  96. return array($node->_lft, $node->_rgt, $node->parent_id);
  97. }
  98. public function testGetsNodeData()
  99. {
  100. $data = Category::getNodeData(3);
  101. $this->assertEquals([ '_lft' => 3, '_rgt' => 4 ], $data);
  102. }
  103. public function testGetsPlainNodeData()
  104. {
  105. $data = Category::getPlainNodeData(3);
  106. $this->assertEquals([ 3, 4 ], $data);
  107. }
  108. public function testReceivesValidValuesWhenAppendedTo()
  109. {
  110. $node = new Category([ 'name' => 'test' ]);
  111. $root = Category::root();
  112. $accepted = array($root->_rgt, $root->_rgt + 1, $root->id);
  113. $root->appendNode($node);
  114. $this->assertTrue($node->hasMoved());
  115. $this->assertEquals($accepted, $this->nodeValues($node));
  116. $this->assertTreeNotBroken();
  117. $this->assertFalse($node->isDirty());
  118. $this->assertTrue($node->isDescendantOf($root));
  119. }
  120. public function testReceivesValidValuesWhenPrependedTo()
  121. {
  122. $root = Category::root();
  123. $node = new Category([ 'name' => 'test' ]);
  124. $root->prependNode($node);
  125. $this->assertTrue($node->hasMoved());
  126. $this->assertEquals(array($root->_lft + 1, $root->_lft + 2, $root->id), $this->nodeValues($node));
  127. $this->assertTreeNotBroken();
  128. $this->assertTrue($node->isDescendantOf($root));
  129. $this->assertTrue($root->isAncestorOf($node));
  130. $this->assertTrue($node->isChildOf($root));
  131. }
  132. public function testReceivesValidValuesWhenInsertedAfter()
  133. {
  134. $target = $this->findCategory('apple');
  135. $node = new Category([ 'name' => 'test' ]);
  136. $node->afterNode($target)->save();
  137. $this->assertTrue($node->hasMoved());
  138. $this->assertEquals(array($target->_rgt + 1, $target->_rgt + 2, $target->parent->id), $this->nodeValues($node));
  139. $this->assertTreeNotBroken();
  140. $this->assertFalse($node->isDirty());
  141. $this->assertTrue($node->isSiblingOf($target));
  142. }
  143. public function testReceivesValidValuesWhenInsertedBefore()
  144. {
  145. $target = $this->findCategory('apple');
  146. $node = new Category([ 'name' => 'test' ]);
  147. $node->beforeNode($target)->save();
  148. $this->assertTrue($node->hasMoved());
  149. $this->assertEquals(array($target->_lft, $target->_lft + 1, $target->parent->id), $this->nodeValues($node));
  150. $this->assertTreeNotBroken();
  151. }
  152. public function testCategoryMovesDown()
  153. {
  154. $node = $this->findCategory('apple');
  155. $target = $this->findCategory('mobile');
  156. $target->appendNode($node);
  157. $this->assertTrue($node->hasMoved());
  158. $this->assertNodeReceivesValidValues($node);
  159. $this->assertTreeNotBroken();
  160. }
  161. public function testCategoryMovesUp()
  162. {
  163. $node = $this->findCategory('samsung');
  164. $target = $this->findCategory('notebooks');
  165. $target->appendNode($node);
  166. $this->assertTrue($node->hasMoved());
  167. $this->assertTreeNotBroken();
  168. $this->assertNodeReceivesValidValues($node);
  169. }
  170. public function testFailsToInsertIntoChild()
  171. {
  172. $this->expectException(Exception::class);
  173. $node = $this->findCategory('notebooks');
  174. $target = $node->children()->first();
  175. $node->afterNode($target)->save();
  176. }
  177. public function testFailsToAppendIntoItself()
  178. {
  179. $this->expectException(Exception::class);
  180. $node = $this->findCategory('notebooks');
  181. $node->appendToNode($node)->save();
  182. }
  183. public function testFailsToPrependIntoItself()
  184. {
  185. $this->expectException(Exception::class);
  186. $node = $this->findCategory('notebooks');
  187. $node->prependTo($node)->save();
  188. }
  189. public function testWithoutRootWorks()
  190. {
  191. $result = Category::withoutRoot()->pluck('name');
  192. $this->assertNotEquals('store', $result);
  193. }
  194. public function testAncestorsReturnsAncestorsWithoutNodeItself()
  195. {
  196. $node = $this->findCategory('apple');
  197. $path = all($node->ancestors()->pluck('name'));
  198. $this->assertEquals(array('store', 'notebooks'), $path);
  199. }
  200. public function testGetsAncestorsByStatic()
  201. {
  202. $path = all(Category::ancestorsOf(3)->pluck('name'));
  203. $this->assertEquals(array('store', 'notebooks'), $path);
  204. }
  205. public function testGetsAncestorsDirect()
  206. {
  207. $path = all(Category::find(8)->getAncestors()->pluck('id'));
  208. $this->assertEquals(array(1, 5, 7), $path);
  209. }
  210. public function testDescendants()
  211. {
  212. $node = $this->findCategory('mobile');
  213. $descendants = all($node->descendants()->pluck('name'));
  214. $expected = array('nokia', 'samsung', 'galaxy', 'sony', 'lenovo');
  215. $this->assertEquals($expected, $descendants);
  216. $descendants = all($node->getDescendants()->pluck('name'));
  217. $this->assertEquals(count($descendants), $node->getDescendantCount());
  218. $this->assertEquals($expected, $descendants);
  219. $descendants = all(Category::descendantsAndSelf(7)->pluck('name'));
  220. $expected = [ 'samsung', 'galaxy' ];
  221. $this->assertEquals($expected, $descendants);
  222. }
  223. public function testWithDepthWorks()
  224. {
  225. $nodes = all(Category::withDepth()->limit(4)->pluck('depth'));
  226. $this->assertEquals(array(0, 1, 2, 2), $nodes);
  227. }
  228. public function testWithDepthWithCustomKeyWorks()
  229. {
  230. $node = Category::whereIsRoot()->withDepth('level')->first();
  231. $this->assertTrue(isset($node['level']));
  232. }
  233. public function testWithDepthWorksAlongWithDefaultKeys()
  234. {
  235. $node = Category::withDepth()->first();
  236. $this->assertTrue(isset($node->name));
  237. }
  238. public function testParentIdAttributeAccessorAppendsNode()
  239. {
  240. $node = new Category(array('name' => 'lg', 'parent_id' => 5));
  241. $node->save();
  242. $this->assertEquals(5, $node->parent_id);
  243. $this->assertEquals(5, $node->getParentId());
  244. $node->parent_id = null;
  245. $node->save();
  246. $node->refreshNode();
  247. $this->assertEquals(null, $node->parent_id);
  248. $this->assertTrue($node->isRoot());
  249. }
  250. public function testFailsToSaveNodeUntilNotInserted()
  251. {
  252. $this->expectException(Exception::class);
  253. $node = new Category;
  254. $node->save();
  255. }
  256. public function testNodeIsDeletedWithDescendants()
  257. {
  258. $node = $this->findCategory('mobile');
  259. $node->forceDelete();
  260. $this->assertTreeNotBroken();
  261. $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count();
  262. $this->assertEquals(0, $nodes);
  263. $root = Category::root();
  264. $this->assertEquals(8, $root->getRgt());
  265. }
  266. public function testNodeIsSoftDeleted()
  267. {
  268. $root = Category::root();
  269. $samsung = $this->findCategory('samsung');
  270. $samsung->delete();
  271. $this->assertTreeNotBroken();
  272. $this->assertNull($this->findCategory('galaxy'));
  273. sleep(1);
  274. $node = $this->findCategory('mobile');
  275. $node->delete();
  276. $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count();
  277. $this->assertEquals(0, $nodes);
  278. $originalRgt = $root->getRgt();
  279. $root->refreshNode();
  280. $this->assertEquals($originalRgt, $root->getRgt());
  281. $node = $this->findCategory('mobile', true);
  282. $node->restore();
  283. $this->assertNull($this->findCategory('samsung'));
  284. $this->assertNotNull($this->findCategory('nokia'));
  285. }
  286. public function testSoftDeletedNodeisDeletedWhenParentIsDeleted()
  287. {
  288. $this->findCategory('samsung')->delete();
  289. $this->findCategory('mobile')->forceDelete();
  290. $this->assertTreeNotBroken();
  291. $this->assertNull($this->findCategory('samsung', true));
  292. $this->assertNull($this->findCategory('sony'));
  293. }
  294. public function testFailsToSaveNodeUntilParentIsSaved()
  295. {
  296. $this->expectException(Exception::class);
  297. $node = new Category(array('title' => 'Node'));
  298. $parent = new Category(array('title' => 'Parent'));
  299. $node->appendTo($parent)->save();
  300. }
  301. public function testSiblings()
  302. {
  303. $node = $this->findCategory('samsung');
  304. $siblings = all($node->siblings()->pluck('id'));
  305. $next = all($node->nextSiblings()->pluck('id'));
  306. $prev = all($node->prevSiblings()->pluck('id'));
  307. $this->assertEquals(array(6, 9, 10), $siblings);
  308. $this->assertEquals(array(9, 10), $next);
  309. $this->assertEquals(array(6), $prev);
  310. $siblings = all($node->getSiblings()->pluck('id'));
  311. $next = all($node->getNextSiblings()->pluck('id'));
  312. $prev = all($node->getPrevSiblings()->pluck('id'));
  313. $this->assertEquals(array(6, 9, 10), $siblings);
  314. $this->assertEquals(array(9, 10), $next);
  315. $this->assertEquals(array(6), $prev);
  316. $next = $node->getNextSibling();
  317. $prev = $node->getPrevSibling();
  318. $this->assertEquals(9, $next->id);
  319. $this->assertEquals(6, $prev->id);
  320. }
  321. public function testFetchesReversed()
  322. {
  323. $node = $this->findCategory('sony');
  324. $siblings = $node->prevSiblings()->reversed()->value('id');
  325. $this->assertEquals(7, $siblings);
  326. }
  327. public function testToTreeBuildsWithDefaultOrder()
  328. {
  329. $tree = Category::whereBetween('_lft', array(8, 17))->defaultOrder()->get()->toTree();
  330. $this->assertEquals(1, count($tree));
  331. $root = $tree->first();
  332. $this->assertEquals('mobile', $root->name);
  333. $this->assertEquals(4, count($root->children));
  334. }
  335. public function testToTreeBuildsWithCustomOrder()
  336. {
  337. $tree = Category::whereBetween('_lft', array(8, 17))
  338. ->orderBy('title')
  339. ->get()
  340. ->toTree();
  341. $this->assertEquals(1, count($tree));
  342. $root = $tree->first();
  343. $this->assertEquals('mobile', $root->name);
  344. $this->assertEquals(4, count($root->children));
  345. $this->assertEquals($root, $root->children->first()->parent);
  346. }
  347. public function testToTreeWithSpecifiedRoot()
  348. {
  349. $node = $this->findCategory('mobile');
  350. $nodes = Category::whereBetween('_lft', array(8, 17))->get();
  351. $tree1 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree(5);
  352. $tree2 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree($node);
  353. $this->assertEquals(4, $tree1->count());
  354. $this->assertEquals(4, $tree2->count());
  355. }
  356. public function testToTreeBuildsWithDefaultOrderAndMultipleRootNodes()
  357. {
  358. $tree = Category::withoutRoot()->get()->toTree();
  359. $this->assertEquals(2, count($tree));
  360. }
  361. public function testToTreeBuildsWithRootItemIdProvided()
  362. {
  363. $tree = Category::whereBetween('_lft', array(8, 17))->get()->toTree(5);
  364. $this->assertEquals(4, count($tree));
  365. $root = $tree[1];
  366. $this->assertEquals('samsung', $root->name);
  367. $this->assertEquals(1, count($root->children));
  368. }
  369. public function testRetrievesNextNode()
  370. {
  371. $node = $this->findCategory('apple');
  372. $next = $node->nextNodes()->first();
  373. $this->assertEquals('lenovo', $next->name);
  374. }
  375. public function testRetrievesPrevNode()
  376. {
  377. $node = $this->findCategory('apple');
  378. $next = $node->getPrevNode();
  379. $this->assertEquals('notebooks', $next->name);
  380. }
  381. public function testMultipleAppendageWorks()
  382. {
  383. $parent = $this->findCategory('mobile');
  384. $child = new Category([ 'name' => 'test' ]);
  385. $parent->appendNode($child);
  386. $child->appendNode(new Category([ 'name' => 'sub' ]));
  387. $parent->appendNode(new Category([ 'name' => 'test2' ]));
  388. $this->assertTreeNotBroken();
  389. }
  390. public function testDefaultCategoryIsSavedAsRoot()
  391. {
  392. $node = new Category([ 'name' => 'test' ]);
  393. $node->save();
  394. $this->assertEquals(23, $node->_lft);
  395. $this->assertTreeNotBroken();
  396. $this->assertTrue($node->isRoot());
  397. }
  398. public function testExistingCategorySavedAsRoot()
  399. {
  400. $node = $this->findCategory('apple');
  401. $node->saveAsRoot();
  402. $this->assertTreeNotBroken();
  403. $this->assertTrue($node->isRoot());
  404. }
  405. public function testNodeMovesDownSeveralPositions()
  406. {
  407. $node = $this->findCategory('nokia');
  408. $this->assertTrue($node->down(2));
  409. $this->assertEquals($node->_lft, 15);
  410. }
  411. public function testNodeMovesUpSeveralPositions()
  412. {
  413. $node = $this->findCategory('sony');
  414. $this->assertTrue($node->up(2));
  415. $this->assertEquals($node->_lft, 9);
  416. }
  417. public function testCountsTreeErrors()
  418. {
  419. $errors = Category::countErrors();
  420. $this->assertEquals([ 'oddness' => 0,
  421. 'duplicates' => 0,
  422. 'wrong_parent' => 0,
  423. 'missing_parent' => 0 ], $errors);
  424. Category::where('id', '=', 5)->update([ '_lft' => 14 ]);
  425. Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]);
  426. Category::where('id', '=', 11)->update([ '_lft' => 20 ]);
  427. Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]);
  428. $errors = Category::countErrors();
  429. $this->assertEquals(1, $errors['oddness']);
  430. $this->assertEquals(2, $errors['duplicates']);
  431. $this->assertEquals(1, $errors['missing_parent']);
  432. }
  433. public function testCreatesNode()
  434. {
  435. $node = Category::create([ 'name' => 'test' ]);
  436. $this->assertEquals(23, $node->getLft());
  437. }
  438. public function testCreatesViaRelationship()
  439. {
  440. $node = $this->findCategory('apple');
  441. $child = $node->children()->create([ 'name' => 'test' ]);
  442. $this->assertTreeNotBroken();
  443. }
  444. public function testCreatesTree()
  445. {
  446. $node = Category::create(
  447. [
  448. 'name' => 'test',
  449. 'children' =>
  450. [
  451. [ 'name' => 'test2' ],
  452. [ 'name' => 'test3' ],
  453. ],
  454. ]);
  455. $this->assertTreeNotBroken();
  456. $this->assertTrue(isset($node->children));
  457. $node = $this->findCategory('test');
  458. $this->assertCount(2, $node->children);
  459. $this->assertEquals('test2', $node->children[0]->name);
  460. }
  461. public function testDescendantsOfNonExistingNode()
  462. {
  463. $node = new Category;
  464. $this->assertTrue($node->getDescendants()->isEmpty());
  465. }
  466. public function testWhereDescendantsOf()
  467. {
  468. $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
  469. Category::whereDescendantOf(124)->get();
  470. }
  471. public function testAncestorsByNode()
  472. {
  473. $category = $this->findCategory('apple');
  474. $ancestors = all(Category::whereAncestorOf($category)->pluck('id'));
  475. $this->assertEquals([ 1, 2 ], $ancestors);
  476. }
  477. public function testDescendantsByNode()
  478. {
  479. $category = $this->findCategory('notebooks');
  480. $res = all(Category::whereDescendantOf($category)->pluck('id'));
  481. $this->assertEquals([ 3, 4 ], $res);
  482. }
  483. public function testMultipleDeletionsDoNotBrakeTree()
  484. {
  485. $category = $this->findCategory('mobile');
  486. foreach ($category->children()->take(2)->get() as $child)
  487. {
  488. $child->forceDelete();
  489. }
  490. $this->assertTreeNotBroken();
  491. }
  492. public function testTreeIsFixed()
  493. {
  494. Category::where('id', '=', 5)->update([ '_lft' => 14 ]);
  495. Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]);
  496. Category::where('id', '=', 11)->update([ '_lft' => 20 ]);
  497. Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]);
  498. $fixed = Category::fixTree();
  499. $this->assertTrue($fixed > 0);
  500. $this->assertTreeNotBroken();
  501. $node = Category::find(8);
  502. $this->assertEquals(2, $node->getParentId());
  503. $node = Category::find(2);
  504. $this->assertEquals(null, $node->getParentId());
  505. }
  506. public function testSubtreeIsFixed()
  507. {
  508. Category::where('id', '=', 8)->update([ '_lft' => 11 ]);
  509. $fixed = Category::fixSubtree(Category::find(5));
  510. $this->assertEquals($fixed, 1);
  511. $this->assertTreeNotBroken();
  512. $this->assertEquals(Category::find(8)->getLft(), 12);
  513. }
  514. public function testParentIdDirtiness()
  515. {
  516. $node = $this->findCategory('apple');
  517. $node->parent_id = 5;
  518. $this->assertTrue($node->isDirty('parent_id'));
  519. $node = $this->findCategory('apple');
  520. $node->parent_id = null;
  521. $this->assertTrue($node->isDirty('parent_id'));
  522. }
  523. public function testIsDirtyMovement()
  524. {
  525. $node = $this->findCategory('apple');
  526. $otherNode = $this->findCategory('samsung');
  527. $this->assertFalse($node->isDirty());
  528. $node->afterNode($otherNode);
  529. $this->assertTrue($node->isDirty());
  530. $node = $this->findCategory('apple');
  531. $otherNode = $this->findCategory('samsung');
  532. $this->assertFalse($node->isDirty());
  533. $node->appendToNode($otherNode);
  534. $this->assertTrue($node->isDirty());
  535. }
  536. public function testRootNodesMoving()
  537. {
  538. $node = $this->findCategory('store');
  539. $node->down();
  540. $this->assertEquals(3, $node->getLft());
  541. }
  542. public function testDescendantsRelation()
  543. {
  544. $node = $this->findCategory('notebooks');
  545. $result = $node->descendants;
  546. $this->assertEquals(2, $result->count());
  547. $this->assertEquals('apple', $result->first()->name);
  548. }
  549. public function testDescendantsEagerlyLoaded()
  550. {
  551. $nodes = Category::whereIn('id', [ 2, 5 ])->get();
  552. $nodes->load('descendants');
  553. $this->assertEquals(2, $nodes->count());
  554. $this->assertTrue($nodes->first()->relationLoaded('descendants'));
  555. }
  556. public function testDescendantsRelationQuery()
  557. {
  558. $nodes = Category::has('descendants')->whereIn('id', [ 2, 3 ])->get();
  559. $this->assertEquals(1, $nodes->count());
  560. $this->assertEquals(2, $nodes->first()->getKey());
  561. $nodes = Category::has('descendants', '>', 2)->get();
  562. $this->assertEquals(2, $nodes->count());
  563. $this->assertEquals(1, $nodes[0]->getKey());
  564. $this->assertEquals(5, $nodes[1]->getKey());
  565. }
  566. public function testParentRelationQuery()
  567. {
  568. $nodes = Category::has('parent')->whereIn('id', [ 1, 2 ]);
  569. $this->assertEquals(1, $nodes->count());
  570. $this->assertEquals(2, $nodes->first()->getKey());
  571. }
  572. public function testRebuildTree()
  573. {
  574. $fixed = Category::rebuildTree([
  575. [
  576. 'id' => 1,
  577. 'children' => [
  578. [ 'id' => 10 ],
  579. [ 'id' => 3, 'name' => 'apple v2', 'children' => [ [ 'name' => 'new node' ] ] ],
  580. [ 'id' => 2 ],
  581. ]
  582. ]
  583. ]);
  584. $this->assertTrue($fixed > 0);
  585. $this->assertTreeNotBroken();
  586. $node = Category::find(3);
  587. $this->assertEquals(1, $node->getParentId());
  588. $this->assertEquals('apple v2', $node->name);
  589. $this->assertEquals(4, $node->getLft());
  590. $node = $this->findCategory('new node');
  591. $this->assertNotNull($node);
  592. $this->assertEquals(3, $node->getParentId());
  593. }
  594. public function testRebuildSubtree()
  595. {
  596. $fixed = Category::rebuildSubtree(Category::find(7), [
  597. [ 'name' => 'new node' ],
  598. [ 'id' => '8' ],
  599. ]);
  600. $this->assertTrue($fixed > 0);
  601. $this->assertTreeNotBroken();
  602. $node = $this->findCategory('new node');
  603. $this->assertNotNull($node);
  604. $this->assertEquals($node->getLft(), 12);
  605. }
  606. public function testRebuildTreeWithDeletion()
  607. {
  608. Category::rebuildTree([ [ 'name' => 'all deleted' ] ], true);
  609. $this->assertTreeNotBroken();
  610. $nodes = Category::get();
  611. $this->assertEquals(1, $nodes->count());
  612. $this->assertEquals('all deleted', $nodes->first()->name);
  613. $nodes = Category::withTrashed()->get();
  614. $this->assertTrue($nodes->count() > 1);
  615. }
  616. public function testRebuildFailsWithInvalidPK()
  617. {
  618. $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
  619. Category::rebuildTree([ [ 'id' => 24 ] ]);
  620. }
  621. public function testFlatTree()
  622. {
  623. $node = $this->findCategory('mobile');
  624. $tree = $node->descendants()->orderBy('name')->get()->toFlatTree();
  625. $this->assertCount(5, $tree);
  626. $this->assertEquals('samsung', $tree[2]->name);
  627. $this->assertEquals('galaxy', $tree[3]->name);
  628. }
  629. // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7.
  630. // What's the purpose of this method? @todo: remove/update?
  631. /*public function testSeveralNodesModelWork()
  632. {
  633. $category = new Category;
  634. $category->name = 'test';
  635. $category->saveAsRoot();
  636. $duplicate = new DuplicateCategory;
  637. $duplicate->name = 'test';
  638. $duplicate->saveAsRoot();
  639. }*/
  640. public function testWhereIsLeaf()
  641. {
  642. $categories = Category::leaves();
  643. $this->assertEquals(7, $categories->count());
  644. $this->assertEquals('apple', $categories->first()->name);
  645. $this->assertTrue($categories->first()->isLeaf());
  646. $category = Category::whereIsRoot()->first();
  647. $this->assertFalse($category->isLeaf());
  648. }
  649. public function testEagerLoadAncestors()
  650. {
  651. $queryLogCount = count(Capsule::connection()->getQueryLog());
  652. $categories = Category::with('ancestors')->orderBy('name')->get();
  653. $this->assertEquals($queryLogCount + 2, count(Capsule::connection()->getQueryLog()));
  654. $expectedShape = [
  655. 'apple (3)}' => 'store (1) > notebooks (2)',
  656. 'galaxy (8)}' => 'store (1) > mobile (5) > samsung (7)',
  657. 'lenovo (4)}' => 'store (1) > notebooks (2)',
  658. 'lenovo (10)}' => 'store (1) > mobile (5)',
  659. 'mobile (5)}' => 'store (1)',
  660. 'nokia (6)}' => 'store (1) > mobile (5)',
  661. 'notebooks (2)}' => 'store (1)',
  662. 'samsung (7)}' => 'store (1) > mobile (5)',
  663. 'sony (9)}' => 'store (1) > mobile (5)',
  664. 'store (1)}' => '',
  665. 'store_2 (11)}' => ''
  666. ];
  667. $output = [];
  668. foreach ($categories as $category) {
  669. $output["{$category->name} ({$category->id})}"] = $category->ancestors->count()
  670. ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray())
  671. : '';
  672. }
  673. $this->assertEquals($expectedShape, $output);
  674. }
  675. public function testLazyLoadAncestors()
  676. {
  677. $queryLogCount = count(Capsule::connection()->getQueryLog());
  678. $categories = Category::orderBy('name')->get();
  679. $this->assertEquals($queryLogCount + 1, count(Capsule::connection()->getQueryLog()));
  680. $expectedShape = [
  681. 'apple (3)}' => 'store (1) > notebooks (2)',
  682. 'galaxy (8)}' => 'store (1) > mobile (5) > samsung (7)',
  683. 'lenovo (4)}' => 'store (1) > notebooks (2)',
  684. 'lenovo (10)}' => 'store (1) > mobile (5)',
  685. 'mobile (5)}' => 'store (1)',
  686. 'nokia (6)}' => 'store (1) > mobile (5)',
  687. 'notebooks (2)}' => 'store (1)',
  688. 'samsung (7)}' => 'store (1) > mobile (5)',
  689. 'sony (9)}' => 'store (1) > mobile (5)',
  690. 'store (1)}' => '',
  691. 'store_2 (11)}' => ''
  692. ];
  693. $output = [];
  694. foreach ($categories as $category) {
  695. $output["{$category->name} ({$category->id})}"] = $category->ancestors->count()
  696. ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray())
  697. : '';
  698. }
  699. // assert that there is number of original query + 1 + number of rows to fulfill the relation
  700. $this->assertEquals($queryLogCount + 12, count(Capsule::connection()->getQueryLog()));
  701. $this->assertEquals($expectedShape, $output);
  702. }
  703. public function testWhereHasCountQueryForAncestors()
  704. {
  705. $categories = all(Category::has('ancestors', '>', 2)->pluck('name'));
  706. $this->assertEquals([ 'galaxy' ], $categories);
  707. $categories = all(Category::whereHas('ancestors', function ($query) {
  708. $query->where('id', 5);
  709. })->pluck('name'));
  710. $this->assertEquals([ 'nokia', 'samsung', 'galaxy', 'sony', 'lenovo' ], $categories);
  711. }
  712. public function testReplication()
  713. {
  714. $category = $this->findCategory('nokia');
  715. $category = $category->replicate();
  716. $category->save();
  717. $category->refreshNode();
  718. $this->assertNull($category->getParentId());
  719. $category = $this->findCategory('nokia');
  720. $category = $category->replicate();
  721. $category->parent_id = 1;
  722. $category->save();
  723. $category->refreshNode();
  724. $this->assertEquals(1, $category->getParentId());
  725. }
  726. }
  727. function all($items)
  728. {
  729. return is_array($items) ? $items : $items->all();
  730. }