. * */ use Behat\Behat\Context\Context; use PHPUnit\Framework\Assert; class FileListContext implements Context, ActorAwareInterface { /** * @var Actor */ private $actor; /** * @var array */ private $fileListAncestorsByActor; /** * @var Locator */ private $fileListAncestor; /** * @BeforeScenario */ public function initializeFileListAncestors() { $this->fileListAncestorsByActor = []; $this->fileListAncestor = null; } /** * @param Actor $actor */ public function setCurrentActor(Actor $actor) { $this->actor = $actor; if (array_key_exists($actor->getName(), $this->fileListAncestorsByActor)) { $this->fileListAncestor = $this->fileListAncestorsByActor[$actor->getName()]; } else { $this->fileListAncestor = null; } } /** * Sets the file list ancestor to be used in the steps performed by the * given actor from that point on (until changed again). * * This is meant to be called from other contexts, for example, when the * Files app or the public page for a shared folder are opened. * * The FileListAncestorSetter trait can be used to reduce the boilerplate * needed to set the file list ancestor from other contexts. * * @param null|Locator $fileListAncestor the file list ancestor * @param Actor $actor the actor */ public function setFileListAncestorForActor($fileListAncestor, Actor $actor) { $this->fileListAncestorsByActor[$actor->getName()] = $fileListAncestor; } /** * @return Locator */ public static function mainWorkingIcon($fileListAncestor) { return Locator::forThe()->css(".mask.icon-loading")-> descendantOf($fileListAncestor)-> describedAs("Main working icon in file list"); } /** * @return Locator */ public static function breadcrumbs($fileListAncestor) { return Locator::forThe()->css(".files-controls .breadcrumb")-> descendantOf($fileListAncestor)-> describedAs("Breadcrumbs in file list"); } /** * @return Locator */ public static function createMenuButton($fileListAncestor) { return Locator::forThe()->css(".files-controls .button.new")-> descendantOf($fileListAncestor)-> describedAs("Create menu button in file list"); } /** * @return Locator */ private static function createMenuItemFor($fileListAncestor, $newType) { return Locator::forThe()->xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' newFileMenu ')]//span[normalize-space() = '$newType']/ancestor::li")-> descendantOf($fileListAncestor)-> describedAs("Create $newType menu item in file list"); } /** * @return Locator */ public static function createNewFolderMenuItem($fileListAncestor) { return self::createMenuItemFor($fileListAncestor, "New folder"); } /** * @return Locator */ public static function createNewFolderMenuItemNameInput($fileListAncestor) { return Locator::forThe()->css(".filenameform input[type=text]")-> descendantOf(self::createNewFolderMenuItem($fileListAncestor))-> describedAs("Name input in create new folder menu item in file list"); } /** * @return Locator */ public static function createNewFolderMenuItemConfirmButton($fileListAncestor) { return Locator::forThe()->css(".filenameform input[type=submit]")-> descendantOf(self::createNewFolderMenuItem($fileListAncestor))-> describedAs("Confirm button in create new folder menu item in file list"); } /** * @return Locator */ public static function fileListHeader($fileListAncestor) { return Locator::forThe()->css("thead")-> descendantOf($fileListAncestor)-> describedAs("Header in file list"); } /** * @return Locator */ public static function selectedFilesActionsMenuButton($fileListAncestor) { return Locator::forThe()->css(".actions-selected")-> descendantOf(self::fileListHeader($fileListAncestor))-> describedAs("Selected files actions menu button in file list"); } /** * @return Locator */ public static function selectedFilesActionsMenu() { return Locator::forThe()->css(".filesSelectMenu")-> describedAs("Selected files actions menu in file list"); } /** * @return Locator */ private static function selectedFilesActionsMenuItemFor($itemText) { return Locator::forThe()->xpath("//a[normalize-space() = '$itemText']")-> descendantOf(self::selectedFilesActionsMenu())-> describedAs($itemText . " item in selected files actions menu in file list"); } /** * @return Locator */ public static function moveOrCopySelectedFilesMenuItem() { return self::selectedFilesActionsMenuItemFor("Move or copy"); } /** * @return Locator */ public static function rowForFile($fileListAncestor, $fileName) { return Locator::forThe()->xpath("//*[@class = 'files-fileList']//span[contains(concat(' ', normalize-space(@class), ' '), ' nametext ') and normalize-space() = '$fileName']/ancestor::tr")-> descendantOf($fileListAncestor)-> describedAs("Row for file $fileName in file list"); } /** * @return Locator */ public static function rowForFilePreceding($fileListAncestor, $fileName1, $fileName2) { return Locator::forThe()->xpath("//preceding-sibling::tr//span[contains(concat(' ', normalize-space(@class), ' '), ' nametext ') and normalize-space() = '$fileName1']/ancestor::tr")-> descendantOf(self::rowForFile($fileListAncestor, $fileName2))-> describedAs("Row for file $fileName1 preceding $fileName2 in file list"); } /** * @return Locator */ public static function selectionCheckboxForFile($fileListAncestor, $fileName) { // Note that the element that the user interacts with is the label, not // the checbox itself. return Locator::forThe()->css(".selection label")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Selection checkbox for file $fileName in file list"); } /** * @return Locator */ public static function selectionCheckboxInputForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".selection input[type=checkbox]")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Selection checkbox input for file $fileName in file list"); } /** * @return Locator */ public static function favoriteMarkForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".favorite-mark")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Favorite mark for file $fileName in file list"); } /** * @return Locator */ public static function notFavoritedStateIconForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".icon-star")-> descendantOf(self::favoriteMarkForFile($fileListAncestor, $fileName))-> describedAs("Not favorited state icon for file $fileName in file list"); } /** * @return Locator */ public static function favoritedStateIconForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".icon-starred")-> descendantOf(self::favoriteMarkForFile($fileListAncestor, $fileName))-> describedAs("Favorited state icon for file $fileName in file list"); } /** * @return Locator */ public static function mainLinkForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".name")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Main link for file $fileName in file list"); } /** * @return Locator */ public static function renameInputForFile($fileListAncestor, $fileName) { return Locator::forThe()->css("input.filename")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Rename input for file $fileName in file list"); } /** * @return Locator */ public static function commentActionForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".action-comment")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Comment action for file $fileName in file list"); } /** * @return Locator */ public static function shareActionForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".action-share")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("Share action for file $fileName in file list"); } /** * @return Locator */ public static function fileActionsMenuButtonForFile($fileListAncestor, $fileName) { return Locator::forThe()->css(".action-menu")-> descendantOf(self::rowForFile($fileListAncestor, $fileName))-> describedAs("File actions menu button for file $fileName in file list"); } /** * @return Locator */ public static function fileActionsMenu() { return Locator::forThe()->css(".fileActionsMenu")-> describedAs("File actions menu in file list"); } /** * @return Locator */ private static function fileActionsMenuItemFor($itemText) { return Locator::forThe()->xpath("//a[normalize-space() = '$itemText']")-> descendantOf(self::fileActionsMenu())-> describedAs($itemText . " item in file actions menu in file list"); } /** * @return Locator */ public static function addToFavoritesMenuItem() { return self::fileActionsMenuItemFor("Add to favorites"); } /** * @return Locator */ public static function removeFromFavoritesMenuItem() { return self::fileActionsMenuItemFor("Remove from favorites"); } /** * @return Locator */ public static function detailsMenuItem() { return self::fileActionsMenuItemFor("Details"); } /** * @return Locator */ public static function renameMenuItem() { return self::fileActionsMenuItemFor("Rename"); } /** * @return Locator */ public static function moveOrCopyMenuItem() { return self::fileActionsMenuItemFor("Move or copy"); } /** * @return Locator */ public static function viewFileInFolderMenuItem() { return self::fileActionsMenuItemFor("View in folder"); } /** * @return Locator */ public static function deleteMenuItem() { return self::fileActionsMenuItemFor("Delete"); } /** * @Given I create a new folder named :folderName */ public function iCreateANewFolderNamed($folderName) { $this->actor->find(self::createMenuButton($this->fileListAncestor), 10)->click(); $this->actor->find(self::createNewFolderMenuItem($this->fileListAncestor), 2)->click(); $this->actor->find(self::createNewFolderMenuItemNameInput($this->fileListAncestor), 2)->setValue($folderName); $this->actor->find(self::createNewFolderMenuItemConfirmButton($this->fileListAncestor), 2)->click(); } /** * @Given I enter in the folder named :folderName */ public function iEnterInTheFolderNamed($folderName) { $this->actor->find(self::mainLinkForFile($this->fileListAncestor, $folderName), 10)->click(); } /** * @Given I select :fileName */ public function iSelect($fileName) { $this->iSeeThatIsNotSelected($fileName); $this->actor->find(self::selectionCheckboxForFile($this->fileListAncestor, $fileName), 10)->click(); } /** * @Given I start the move or copy operation for the selected files */ public function iStartTheMoveOrCopyOperationForTheSelectedFiles() { $this->actor->find(self::selectedFilesActionsMenuButton($this->fileListAncestor), 10)->click(); $this->actor->find(self::moveOrCopySelectedFilesMenuItem(), 2)->click(); } /** * @Given I open the details view for :fileName */ public function iOpenTheDetailsViewFor($fileName) { $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::detailsMenuItem(), 2)->click(); } /** * @Given I rename :fileName1 to :fileName2 */ public function iRenameTo($fileName1, $fileName2) { $this->openFileActionsMenuForFile($fileName1); $this->actor->find(self::renameMenuItem(), 2)->click(); // For reference, due to a bug in the Firefox driver of Selenium and/or // maybe in Firefox itself, as a range is selected in the rename input // (the name of the file, without its extension) when the value is set // the window must be in the foreground. Otherwise, if the window is in // the background, instead of setting the value in the whole field it // would be set only in the selected range. // This should not be a problem, though, as the default behaviour is to // bring the browser window to the foreground when switching to a // different actor. $this->actor->find(self::renameInputForFile($this->fileListAncestor, $fileName1), 10)->setValue($fileName2); } /** * @Given I start the move or copy operation for :fileName */ public function iStartTheMoveOrCopyOperationFor($fileName) { $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::moveOrCopyMenuItem(), 2)->click(); } /** * @Given I mark :fileName as favorite */ public function iMarkAsFavorite($fileName) { $this->iSeeThatIsNotMarkedAsFavorite($fileName); $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::addToFavoritesMenuItem(), 2)->click(); } /** * @Given I unmark :fileName as favorite */ public function iUnmarkAsFavorite($fileName) { $this->iSeeThatIsMarkedAsFavorite($fileName); $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::removeFromFavoritesMenuItem(), 2)->click(); } /** * @When I view :fileName in folder */ public function iViewInFolder($fileName) { $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::viewFileInFolderMenuItem(), 2)->click(); } /** * @When I delete :fileName */ public function iDelete($fileName) { $this->openFileActionsMenuForFile($fileName); $this->actor->find(self::deleteMenuItem(), 2)->click(); } /** * @When I open the unread comments for :fileName */ public function iOpenTheUnreadCommentsFor($fileName) { $this->actor->find(self::commentActionForFile($this->fileListAncestor, $fileName), 10)->click(); } /** * @Then I see that the file list is eventually loaded */ public function iSeeThatTheFileListIsEventuallyLoaded() { if (!WaitFor::elementToBeEventuallyNotShown( $this->actor, self::mainWorkingIcon($this->fileListAncestor), $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { Assert::fail("The main working icon for the file list is still shown after $timeout seconds"); } } /** * @Then I see that the file list is currently in :path */ public function iSeeThatTheFileListIsCurrentlyIn($path) { // The text of the breadcrumbs is the text of all the crumbs separated // by white spaces. Assert::assertEquals( str_replace('/', ' ', $path), $this->actor->find(self::breadcrumbs($this->fileListAncestor), 10)->getText()); } /** * @Then I see that it is not possible to create new files */ public function iSeeThatItIsNotPossibleToCreateNewFiles() { // Once a file list is loaded the "Create" menu button is always in the // DOM, so it is checked if it is visible or not. Assert::assertFalse($this->actor->find(self::createMenuButton($this->fileListAncestor))->isVisible()); } /** * @Then I see that the file list contains a file named :fileName */ public function iSeeThatTheFileListContainsAFileNamed($fileName) { Assert::assertNotNull($this->actor->find(self::rowForFile($this->fileListAncestor, $fileName), 10)); } /** * @Then I see that the file list does not contain a file named :fileName */ public function iSeeThatTheFileListDoesNotContainAFileNamed($fileName) { if (!WaitFor::elementToBeEventuallyNotShown( $this->actor, self::rowForFile($this->fileListAncestor, $fileName), $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { Assert::fail("The file list still contains a file named $fileName after $timeout seconds"); } } /** * @Then I see that :fileName1 precedes :fileName2 in the file list */ public function iSeeThatPrecedesInTheFileList($fileName1, $fileName2) { Assert::assertNotNull($this->actor->find(self::rowForFilePreceding($this->fileListAncestor, $fileName1, $fileName2), 10)); } /** * @Then I see that :fileName is not selected */ public function iSeeThatIsNotSelected($fileName) { Assert::assertFalse($this->actor->find(self::selectionCheckboxInputForFile($this->fileListAncestor, $fileName), 10)->isChecked()); } /** * @Then I see that :fileName is marked as favorite */ public function iSeeThatIsMarkedAsFavorite($fileName) { Assert::assertNotNull($this->actor->find(self::favoritedStateIconForFile($this->fileListAncestor, $fileName), 10)); } /** * @Then I see that :fileName is not marked as favorite */ public function iSeeThatIsNotMarkedAsFavorite($fileName) { Assert::assertNotNull($this->actor->find(self::notFavoritedStateIconForFile($this->fileListAncestor, $fileName), 10)); } /** * @Then I see that :fileName has unread comments */ public function iSeeThatHasUnreadComments($fileName) { Assert::assertTrue($this->actor->find(self::commentActionForFile($this->fileListAncestor, $fileName), 10)->isVisible()); } private function waitForRowForFileToBeFullyOpaque($fileName) { $actor = $this->actor; $fileRowXpathExpression = $this->actor->find(self::rowForFile($this->fileListAncestor, $fileName), 10)->getWrappedElement()->getXpath(); $fileRowIsFullyOpaqueCallback = function () use ($actor, $fileRowXpathExpression) { $opacity = $actor->getSession()->evaluateScript("return window.getComputedStyle(document.evaluate(\"" . $fileRowXpathExpression . "\", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).opacity;"); if ($opacity === "1") { return true; } return false; }; if (!Utils::waitFor($fileRowIsFullyOpaqueCallback, $timeout = 2 * $this->actor->getFindTimeoutMultiplier(), $timeoutStep = 1)) { Assert::fail("The row for file $fileName in file list is not fully opaque after $timeout seconds"); } } private function openFileActionsMenuForFile($fileName) { // When a row is added to the file list the opacity of the file row is // animated from transparent to fully opaque. As the file actions menu // is a descendant of the row but overflows it when the row is not fully // opaque clicks on the menu entries "fall-through" and are received // instead by the rows behind. Therefore it should be waited until the // row of the file is fully opaque before using the menu. $this->waitForRowForFileToBeFullyOpaque($fileName); $this->actor->find(self::fileActionsMenuButtonForFile($this->fileListAncestor, $fileName), 10)->click(); } }