FileListContext.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. <?php
  2. /**
  3. *
  4. * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.com)
  5. *
  6. * @license GNU AGPL version 3 or any later version
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. use Behat\Behat\Context\Context;
  23. use PHPUnit\Framework\Assert;
  24. class FileListContext implements Context, ActorAwareInterface {
  25. /**
  26. * @var Actor
  27. */
  28. private $actor;
  29. /**
  30. * @var array
  31. */
  32. private $fileListAncestorsByActor;
  33. /**
  34. * @var Locator
  35. */
  36. private $fileListAncestor;
  37. /**
  38. * @BeforeScenario
  39. */
  40. public function initializeFileListAncestors() {
  41. $this->fileListAncestorsByActor = [];
  42. $this->fileListAncestor = null;
  43. }
  44. /**
  45. * @param Actor $actor
  46. */
  47. public function setCurrentActor(Actor $actor) {
  48. $this->actor = $actor;
  49. if (array_key_exists($actor->getName(), $this->fileListAncestorsByActor)) {
  50. $this->fileListAncestor = $this->fileListAncestorsByActor[$actor->getName()];
  51. } else {
  52. $this->fileListAncestor = null;
  53. }
  54. }
  55. /**
  56. * Sets the file list ancestor to be used in the steps performed by the
  57. * given actor from that point on (until changed again).
  58. *
  59. * This is meant to be called from other contexts, for example, when the
  60. * Files app or the public page for a shared folder are opened.
  61. *
  62. * The FileListAncestorSetter trait can be used to reduce the boilerplate
  63. * needed to set the file list ancestor from other contexts.
  64. *
  65. * @param null|Locator $fileListAncestor the file list ancestor
  66. * @param Actor $actor the actor
  67. */
  68. public function setFileListAncestorForActor($fileListAncestor, Actor $actor) {
  69. $this->fileListAncestorsByActor[$actor->getName()] = $fileListAncestor;
  70. }
  71. /**
  72. * @return Locator
  73. */
  74. public static function mainWorkingIcon($fileListAncestor) {
  75. return Locator::forThe()->css(".mask.icon-loading")->
  76. descendantOf($fileListAncestor)->
  77. describedAs("Main working icon in file list");
  78. }
  79. /**
  80. * @return Locator
  81. */
  82. public static function breadcrumbs($fileListAncestor) {
  83. return Locator::forThe()->css(".files-controls .breadcrumb")->
  84. descendantOf($fileListAncestor)->
  85. describedAs("Breadcrumbs in file list");
  86. }
  87. /**
  88. * @return Locator
  89. */
  90. public static function createMenuButton($fileListAncestor) {
  91. return Locator::forThe()->css(".files-controls .button.new")->
  92. descendantOf($fileListAncestor)->
  93. describedAs("Create menu button in file list");
  94. }
  95. /**
  96. * @return Locator
  97. */
  98. private static function createMenuItemFor($fileListAncestor, $newType) {
  99. return Locator::forThe()->xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' newFileMenu ')]//span[normalize-space() = '$newType']/ancestor::li")->
  100. descendantOf($fileListAncestor)->
  101. describedAs("Create $newType menu item in file list");
  102. }
  103. /**
  104. * @return Locator
  105. */
  106. public static function createNewFolderMenuItem($fileListAncestor) {
  107. return self::createMenuItemFor($fileListAncestor, "New folder");
  108. }
  109. /**
  110. * @return Locator
  111. */
  112. public static function createNewFolderMenuItemNameInput($fileListAncestor) {
  113. return Locator::forThe()->css(".filenameform input[type=text]")->
  114. descendantOf(self::createNewFolderMenuItem($fileListAncestor))->
  115. describedAs("Name input in create new folder menu item in file list");
  116. }
  117. /**
  118. * @return Locator
  119. */
  120. public static function createNewFolderMenuItemConfirmButton($fileListAncestor) {
  121. return Locator::forThe()->css(".filenameform input[type=submit]")->
  122. descendantOf(self::createNewFolderMenuItem($fileListAncestor))->
  123. describedAs("Confirm button in create new folder menu item in file list");
  124. }
  125. /**
  126. * @return Locator
  127. */
  128. public static function fileListHeader($fileListAncestor) {
  129. return Locator::forThe()->css("thead")->
  130. descendantOf($fileListAncestor)->
  131. describedAs("Header in file list");
  132. }
  133. /**
  134. * @return Locator
  135. */
  136. public static function selectedFilesActionsMenuButton($fileListAncestor) {
  137. return Locator::forThe()->css(".actions-selected")->
  138. descendantOf(self::fileListHeader($fileListAncestor))->
  139. describedAs("Selected files actions menu button in file list");
  140. }
  141. /**
  142. * @return Locator
  143. */
  144. public static function selectedFilesActionsMenu() {
  145. return Locator::forThe()->css(".filesSelectMenu")->
  146. describedAs("Selected files actions menu in file list");
  147. }
  148. /**
  149. * @return Locator
  150. */
  151. private static function selectedFilesActionsMenuItemFor($itemText) {
  152. return Locator::forThe()->xpath("//a[normalize-space() = '$itemText']")->
  153. descendantOf(self::selectedFilesActionsMenu())->
  154. describedAs($itemText . " item in selected files actions menu in file list");
  155. }
  156. /**
  157. * @return Locator
  158. */
  159. public static function moveOrCopySelectedFilesMenuItem() {
  160. return self::selectedFilesActionsMenuItemFor("Move or copy");
  161. }
  162. /**
  163. * @return Locator
  164. */
  165. public static function rowForFile($fileListAncestor, $fileName) {
  166. return Locator::forThe()->xpath("//*[@class = 'files-fileList']//span[contains(concat(' ', normalize-space(@class), ' '), ' nametext ') and normalize-space() = '$fileName']/ancestor::tr")->
  167. descendantOf($fileListAncestor)->
  168. describedAs("Row for file $fileName in file list");
  169. }
  170. /**
  171. * @return Locator
  172. */
  173. public static function rowForFilePreceding($fileListAncestor, $fileName1, $fileName2) {
  174. return Locator::forThe()->xpath("//preceding-sibling::tr//span[contains(concat(' ', normalize-space(@class), ' '), ' nametext ') and normalize-space() = '$fileName1']/ancestor::tr")->
  175. descendantOf(self::rowForFile($fileListAncestor, $fileName2))->
  176. describedAs("Row for file $fileName1 preceding $fileName2 in file list");
  177. }
  178. /**
  179. * @return Locator
  180. */
  181. public static function selectionCheckboxForFile($fileListAncestor, $fileName) {
  182. // Note that the element that the user interacts with is the label, not
  183. // the checbox itself.
  184. return Locator::forThe()->css(".selection label")->
  185. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  186. describedAs("Selection checkbox for file $fileName in file list");
  187. }
  188. /**
  189. * @return Locator
  190. */
  191. public static function selectionCheckboxInputForFile($fileListAncestor, $fileName) {
  192. return Locator::forThe()->css(".selection input[type=checkbox]")->
  193. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  194. describedAs("Selection checkbox input for file $fileName in file list");
  195. }
  196. /**
  197. * @return Locator
  198. */
  199. public static function favoriteMarkForFile($fileListAncestor, $fileName) {
  200. return Locator::forThe()->css(".favorite-mark")->
  201. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  202. describedAs("Favorite mark for file $fileName in file list");
  203. }
  204. /**
  205. * @return Locator
  206. */
  207. public static function notFavoritedStateIconForFile($fileListAncestor, $fileName) {
  208. return Locator::forThe()->css(".icon-star")->
  209. descendantOf(self::favoriteMarkForFile($fileListAncestor, $fileName))->
  210. describedAs("Not favorited state icon for file $fileName in file list");
  211. }
  212. /**
  213. * @return Locator
  214. */
  215. public static function favoritedStateIconForFile($fileListAncestor, $fileName) {
  216. return Locator::forThe()->css(".icon-starred")->
  217. descendantOf(self::favoriteMarkForFile($fileListAncestor, $fileName))->
  218. describedAs("Favorited state icon for file $fileName in file list");
  219. }
  220. /**
  221. * @return Locator
  222. */
  223. public static function mainLinkForFile($fileListAncestor, $fileName) {
  224. return Locator::forThe()->css(".name")->
  225. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  226. describedAs("Main link for file $fileName in file list");
  227. }
  228. /**
  229. * @return Locator
  230. */
  231. public static function renameInputForFile($fileListAncestor, $fileName) {
  232. return Locator::forThe()->css("input.filename")->
  233. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  234. describedAs("Rename input for file $fileName in file list");
  235. }
  236. /**
  237. * @return Locator
  238. */
  239. public static function commentActionForFile($fileListAncestor, $fileName) {
  240. return Locator::forThe()->css(".action-comment")->
  241. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  242. describedAs("Comment action for file $fileName in file list");
  243. }
  244. /**
  245. * @return Locator
  246. */
  247. public static function shareActionForFile($fileListAncestor, $fileName) {
  248. return Locator::forThe()->css(".action-share")->
  249. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  250. describedAs("Share action for file $fileName in file list");
  251. }
  252. /**
  253. * @return Locator
  254. */
  255. public static function fileActionsMenuButtonForFile($fileListAncestor, $fileName) {
  256. return Locator::forThe()->css(".action-menu")->
  257. descendantOf(self::rowForFile($fileListAncestor, $fileName))->
  258. describedAs("File actions menu button for file $fileName in file list");
  259. }
  260. /**
  261. * @return Locator
  262. */
  263. public static function fileActionsMenu() {
  264. return Locator::forThe()->css(".fileActionsMenu")->
  265. describedAs("File actions menu in file list");
  266. }
  267. /**
  268. * @return Locator
  269. */
  270. private static function fileActionsMenuItemFor($itemText) {
  271. return Locator::forThe()->xpath("//a[normalize-space() = '$itemText']")->
  272. descendantOf(self::fileActionsMenu())->
  273. describedAs($itemText . " item in file actions menu in file list");
  274. }
  275. /**
  276. * @return Locator
  277. */
  278. public static function addToFavoritesMenuItem() {
  279. return self::fileActionsMenuItemFor("Add to favorites");
  280. }
  281. /**
  282. * @return Locator
  283. */
  284. public static function removeFromFavoritesMenuItem() {
  285. return self::fileActionsMenuItemFor("Remove from favorites");
  286. }
  287. /**
  288. * @return Locator
  289. */
  290. public static function detailsMenuItem() {
  291. return self::fileActionsMenuItemFor("Details");
  292. }
  293. /**
  294. * @return Locator
  295. */
  296. public static function renameMenuItem() {
  297. return self::fileActionsMenuItemFor("Rename");
  298. }
  299. /**
  300. * @return Locator
  301. */
  302. public static function moveOrCopyMenuItem() {
  303. return self::fileActionsMenuItemFor("Move or copy");
  304. }
  305. /**
  306. * @return Locator
  307. */
  308. public static function viewFileInFolderMenuItem() {
  309. return self::fileActionsMenuItemFor("View in folder");
  310. }
  311. /**
  312. * @return Locator
  313. */
  314. public static function deleteMenuItem() {
  315. return self::fileActionsMenuItemFor("Delete");
  316. }
  317. /**
  318. * @Given I create a new folder named :folderName
  319. */
  320. public function iCreateANewFolderNamed($folderName) {
  321. $this->actor->find(self::createMenuButton($this->fileListAncestor), 10)->click();
  322. $this->actor->find(self::createNewFolderMenuItem($this->fileListAncestor), 2)->click();
  323. $this->actor->find(self::createNewFolderMenuItemNameInput($this->fileListAncestor), 2)->setValue($folderName);
  324. $this->actor->find(self::createNewFolderMenuItemConfirmButton($this->fileListAncestor), 2)->click();
  325. }
  326. /**
  327. * @Given I enter in the folder named :folderName
  328. */
  329. public function iEnterInTheFolderNamed($folderName) {
  330. $this->actor->find(self::mainLinkForFile($this->fileListAncestor, $folderName), 10)->click();
  331. }
  332. /**
  333. * @Given I select :fileName
  334. */
  335. public function iSelect($fileName) {
  336. $this->iSeeThatIsNotSelected($fileName);
  337. $this->actor->find(self::selectionCheckboxForFile($this->fileListAncestor, $fileName), 10)->click();
  338. }
  339. /**
  340. * @Given I start the move or copy operation for the selected files
  341. */
  342. public function iStartTheMoveOrCopyOperationForTheSelectedFiles() {
  343. $this->actor->find(self::selectedFilesActionsMenuButton($this->fileListAncestor), 10)->click();
  344. $this->actor->find(self::moveOrCopySelectedFilesMenuItem(), 2)->click();
  345. }
  346. /**
  347. * @Given I open the details view for :fileName
  348. */
  349. public function iOpenTheDetailsViewFor($fileName) {
  350. $this->openFileActionsMenuForFile($fileName);
  351. $this->actor->find(self::detailsMenuItem(), 2)->click();
  352. }
  353. /**
  354. * @Given I rename :fileName1 to :fileName2
  355. */
  356. public function iRenameTo($fileName1, $fileName2) {
  357. $this->openFileActionsMenuForFile($fileName1);
  358. $this->actor->find(self::renameMenuItem(), 2)->click();
  359. // For reference, due to a bug in the Firefox driver of Selenium and/or
  360. // maybe in Firefox itself, as a range is selected in the rename input
  361. // (the name of the file, without its extension) when the value is set
  362. // the window must be in the foreground. Otherwise, if the window is in
  363. // the background, instead of setting the value in the whole field it
  364. // would be set only in the selected range.
  365. // This should not be a problem, though, as the default behaviour is to
  366. // bring the browser window to the foreground when switching to a
  367. // different actor.
  368. $this->actor->find(self::renameInputForFile($this->fileListAncestor, $fileName1), 10)->setValue($fileName2);
  369. }
  370. /**
  371. * @Given I start the move or copy operation for :fileName
  372. */
  373. public function iStartTheMoveOrCopyOperationFor($fileName) {
  374. $this->openFileActionsMenuForFile($fileName);
  375. $this->actor->find(self::moveOrCopyMenuItem(), 2)->click();
  376. }
  377. /**
  378. * @Given I mark :fileName as favorite
  379. */
  380. public function iMarkAsFavorite($fileName) {
  381. $this->iSeeThatIsNotMarkedAsFavorite($fileName);
  382. $this->openFileActionsMenuForFile($fileName);
  383. $this->actor->find(self::addToFavoritesMenuItem(), 2)->click();
  384. }
  385. /**
  386. * @Given I unmark :fileName as favorite
  387. */
  388. public function iUnmarkAsFavorite($fileName) {
  389. $this->iSeeThatIsMarkedAsFavorite($fileName);
  390. $this->openFileActionsMenuForFile($fileName);
  391. $this->actor->find(self::removeFromFavoritesMenuItem(), 2)->click();
  392. }
  393. /**
  394. * @When I view :fileName in folder
  395. */
  396. public function iViewInFolder($fileName) {
  397. $this->openFileActionsMenuForFile($fileName);
  398. $this->actor->find(self::viewFileInFolderMenuItem(), 2)->click();
  399. }
  400. /**
  401. * @When I delete :fileName
  402. */
  403. public function iDelete($fileName) {
  404. $this->openFileActionsMenuForFile($fileName);
  405. $this->actor->find(self::deleteMenuItem(), 2)->click();
  406. }
  407. /**
  408. * @When I open the unread comments for :fileName
  409. */
  410. public function iOpenTheUnreadCommentsFor($fileName) {
  411. $this->actor->find(self::commentActionForFile($this->fileListAncestor, $fileName), 10)->click();
  412. }
  413. /**
  414. * @Then I see that the file list is eventually loaded
  415. */
  416. public function iSeeThatTheFileListIsEventuallyLoaded() {
  417. if (!WaitFor::elementToBeEventuallyNotShown(
  418. $this->actor,
  419. self::mainWorkingIcon($this->fileListAncestor),
  420. $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
  421. Assert::fail("The main working icon for the file list is still shown after $timeout seconds");
  422. }
  423. }
  424. /**
  425. * @Then I see that the file list is currently in :path
  426. */
  427. public function iSeeThatTheFileListIsCurrentlyIn($path) {
  428. // The text of the breadcrumbs is the text of all the crumbs separated
  429. // by white spaces.
  430. Assert::assertEquals(
  431. str_replace('/', ' ', $path), $this->actor->find(self::breadcrumbs($this->fileListAncestor), 10)->getText());
  432. }
  433. /**
  434. * @Then I see that it is not possible to create new files
  435. */
  436. public function iSeeThatItIsNotPossibleToCreateNewFiles() {
  437. // Once a file list is loaded the "Create" menu button is always in the
  438. // DOM, so it is checked if it is visible or not.
  439. Assert::assertFalse($this->actor->find(self::createMenuButton($this->fileListAncestor))->isVisible());
  440. }
  441. /**
  442. * @Then I see that the file list contains a file named :fileName
  443. */
  444. public function iSeeThatTheFileListContainsAFileNamed($fileName) {
  445. Assert::assertNotNull($this->actor->find(self::rowForFile($this->fileListAncestor, $fileName), 10));
  446. }
  447. /**
  448. * @Then I see that the file list does not contain a file named :fileName
  449. */
  450. public function iSeeThatTheFileListDoesNotContainAFileNamed($fileName) {
  451. if (!WaitFor::elementToBeEventuallyNotShown(
  452. $this->actor,
  453. self::rowForFile($this->fileListAncestor, $fileName),
  454. $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
  455. Assert::fail("The file list still contains a file named $fileName after $timeout seconds");
  456. }
  457. }
  458. /**
  459. * @Then I see that :fileName1 precedes :fileName2 in the file list
  460. */
  461. public function iSeeThatPrecedesInTheFileList($fileName1, $fileName2) {
  462. Assert::assertNotNull($this->actor->find(self::rowForFilePreceding($this->fileListAncestor, $fileName1, $fileName2), 10));
  463. }
  464. /**
  465. * @Then I see that :fileName is not selected
  466. */
  467. public function iSeeThatIsNotSelected($fileName) {
  468. Assert::assertFalse($this->actor->find(self::selectionCheckboxInputForFile($this->fileListAncestor, $fileName), 10)->isChecked());
  469. }
  470. /**
  471. * @Then I see that :fileName is marked as favorite
  472. */
  473. public function iSeeThatIsMarkedAsFavorite($fileName) {
  474. Assert::assertNotNull($this->actor->find(self::favoritedStateIconForFile($this->fileListAncestor, $fileName), 10));
  475. }
  476. /**
  477. * @Then I see that :fileName is not marked as favorite
  478. */
  479. public function iSeeThatIsNotMarkedAsFavorite($fileName) {
  480. Assert::assertNotNull($this->actor->find(self::notFavoritedStateIconForFile($this->fileListAncestor, $fileName), 10));
  481. }
  482. /**
  483. * @Then I see that :fileName has unread comments
  484. */
  485. public function iSeeThatHasUnreadComments($fileName) {
  486. Assert::assertTrue($this->actor->find(self::commentActionForFile($this->fileListAncestor, $fileName), 10)->isVisible());
  487. }
  488. private function waitForRowForFileToBeFullyOpaque($fileName) {
  489. $actor = $this->actor;
  490. $fileRowXpathExpression = $this->actor->find(self::rowForFile($this->fileListAncestor, $fileName), 10)->getWrappedElement()->getXpath();
  491. $fileRowIsFullyOpaqueCallback = function () use ($actor, $fileRowXpathExpression) {
  492. $opacity = $actor->getSession()->evaluateScript("return window.getComputedStyle(document.evaluate(\"" . $fileRowXpathExpression . "\", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).opacity;");
  493. if ($opacity === "1") {
  494. return true;
  495. }
  496. return false;
  497. };
  498. if (!Utils::waitFor($fileRowIsFullyOpaqueCallback, $timeout = 2 * $this->actor->getFindTimeoutMultiplier(), $timeoutStep = 1)) {
  499. Assert::fail("The row for file $fileName in file list is not fully opaque after $timeout seconds");
  500. }
  501. }
  502. private function openFileActionsMenuForFile($fileName) {
  503. // When a row is added to the file list the opacity of the file row is
  504. // animated from transparent to fully opaque. As the file actions menu
  505. // is a descendant of the row but overflows it when the row is not fully
  506. // opaque clicks on the menu entries "fall-through" and are received
  507. // instead by the rows behind. Therefore it should be waited until the
  508. // row of the file is fully opaque before using the menu.
  509. $this->waitForRowForFileToBeFullyOpaque($fileName);
  510. $this->actor->find(self::fileActionsMenuButtonForFile($this->fileListAncestor, $fileName), 10)->click();
  511. }
  512. }