diff --git a/composer.json b/composer.json index 9246df6c6..4f1e052a9 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,8 @@ "require": { "php": ">=5.3.2", "composer/installers": "*" + }, + "autoload": { + "classmap": ["tests/behat/features/bootstrap"] } } \ No newline at end of file diff --git a/docs/en/topics/testing/create-functional-test.md b/docs/en/topics/testing/create-functional-test.md index 14bcdd1c5..68465a351 100644 --- a/docs/en/topics/testing/create-functional-test.md +++ b/docs/en/topics/testing/create-functional-test.md @@ -63,3 +63,6 @@ We can use string processing on the body of the response to then see if it fits If you're testing for natural language responses like error messages, make sure to use [i18n](/topics/i18n) translations through the *_t()* method to avoid tests failing when i18n is enabled. + +Note that for a more highlevel testing approach, SilverStripe also supports +[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions. \ No newline at end of file diff --git a/docs/en/topics/testing/index.md b/docs/en/topics/testing/index.md index 9d6ae83d6..ec29bb2ca 100644 --- a/docs/en/topics/testing/index.md +++ b/docs/en/topics/testing/index.md @@ -146,6 +146,9 @@ For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-t **Assertion:** A predicate statement that must be true when a test runs. +**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level +alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org). + **Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the base test case. diff --git a/tests/behat/README.md b/tests/behat/README.md new file mode 100644 index 000000000..e072aec9f --- /dev/null +++ b/tests/behat/README.md @@ -0,0 +1 @@ +See https://github.com/silverstripe-labs/silverstripe-behat-extension \ No newline at end of file diff --git a/tests/behat/_manifest_exclude b/tests/behat/_manifest_exclude new file mode 100644 index 000000000..e69de29bb diff --git a/tests/behat/features/bootstrap/FeatureContext.php b/tests/behat/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..ea6f10054 --- /dev/null +++ b/tests/behat/features/bootstrap/FeatureContext.php @@ -0,0 +1,38 @@ +useContext('BasicContext', new BasicContext($parameters)); + $this->useContext('LoginContext', new LoginContext($parameters)); + $this->useContext('CmsFormsContext', new CmsFormsContext($parameters)); + $this->useContext('CmsUiContext', new CmsUiContext($parameters)); + + parent::__construct($parameters); + } +} diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php new file mode 100644 index 000000000..62974c029 --- /dev/null +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php @@ -0,0 +1,72 @@ +context = $parameters; + } + + /** + * Get Mink session from MinkContext + */ + public function getSession($name = null) + { + return $this->getMainContext()->getSession($name); + } + + /** + * @Then /^I should see an edit page form$/ + */ + public function stepIShouldSeeAnEditPageForm() + { + $page = $this->getSession()->getPage(); + + $form = $page->find('css', '#Form_EditForm'); + assertNotNull($form, 'I should see an edit page form'); + } + + /** + * @When /^I fill in the content form with "([^"]*)"$/ + */ + public function stepIFillInTheContentFormWith($content) + { + $this->getSession()->evaluateScript("tinyMCE.get('Form_EditForm_Content').setContent('$content')"); + } + + /** + * @Then /^the content form should contain "([^"]*)"$/ + */ + public function theContentFormShouldContain($content) + { + $this->getMainContext()->assertElementContains('#Form_EditForm_Content', $content); + } +} diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php new file mode 100644 index 000000000..c5f333056 --- /dev/null +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php @@ -0,0 +1,256 @@ +context = $parameters; + } + + /** + * Get Mink session from MinkContext + */ + public function getSession($name = null) + { + return $this->getMainContext()->getSession($name); + } + + /** + * @Then /^I should see the CMS$/ + */ + public function iShouldSeeTheCms() + { + $page = $this->getSession()->getPage(); + $cms_element = $page->find('css', '.cms'); + assertNotNull($cms_element, 'CMS not found'); + } + + /** + * @Then /^I should see a "([^"]*)" notice$/ + */ + public function iShouldSeeANotice($notice) + { + $this->getMainContext()->assertElementContains('.notice-wrap', $notice); + } + + /** + * @Then /^I should see a "([^"]*)" message$/ + */ + public function iShouldSeeAMessage($message) + { + $this->getMainContext()->assertElementContains('.message', $message); + } + + protected function getCmsTabsElement() + { + $this->getSession()->wait(5000, "window.jQuery('.cms-content-header-tabs').size() > 0"); + + $page = $this->getSession()->getPage(); + $cms_content_header_tabs = $page->find('css', '.cms-content-header-tabs'); + assertNotNull($cms_content_header_tabs, 'CMS tabs not found'); + + return $cms_content_header_tabs; + } + + protected function getCmsContentToolbarElement() + { + $this->getSession()->wait( + 5000, + "window.jQuery('.cms-content-toolbar').size() > 0 " + . "&& window.jQuery('.cms-content-toolbar').children().size() > 0" + ); + + $page = $this->getSession()->getPage(); + $cms_content_toolbar_element = $page->find('css', '.cms-content-toolbar'); + assertNotNull($cms_content_toolbar_element, 'CMS content toolbar not found'); + + return $cms_content_toolbar_element; + } + + protected function getCmsTreeElement() + { + $this->getSession()->wait(5000, "window.jQuery('.cms-tree').size() > 0"); + + $page = $this->getSession()->getPage(); + $cms_tree_element = $page->find('css', '.cms-tree'); + assertNotNull($cms_tree_element, 'CMS tree not found'); + + return $cms_tree_element; + } + + protected function getGridfieldTable($title) + { + $page = $this->getSession()->getPage(); + $table_elements = $page->findAll('css', '.ss-gridfield-table'); + assertNotNull($table_elements, 'Table elements not found'); + + $table_element = null; + foreach ($table_elements as $table) { + $table_title_element = $table->find('css', '.title'); + if ($table_title_element->getText() === $title) { + $table_element = $table; + break; + } + } + assertNotNull($table_element, sprintf('Table `%s` not found', $title)); + + return $table_element; + } + + /** + * @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/ + */ + public function iShouldSeeAButtonInCmsContentToolbar($text) + { + $cms_content_toolbar_element = $this->getCmsContentToolbarElement(); + + $element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'")); + assertNotNull($element, sprintf('%s button not found', $text)); + } + + /** + * @When /^I should see "([^"]*)" in CMS Tree$/ + */ + public function stepIShouldSeeInCmsTree($text) + { + $cms_tree_element = $this->getCmsTreeElement(); + + $element = $cms_tree_element->find('named', array('content', "'$text'")); + assertNotNull($element, sprintf('%s not found', $text)); + } + + /** + * @When /^I should not see "([^"]*)" in CMS Tree$/ + */ + public function stepIShouldNotSeeInCmsTree($text) + { + $cms_tree_element = $this->getCmsTreeElement(); + + $element = $cms_tree_element->find('named', array('content', "'$text'")); + assertNull($element, sprintf('%s found', $text)); + } + + /** + * @When /^I expand the "([^"]*)" CMS Panel$/ + */ + public function iExpandTheCmsPanel() + { + // TODO Make dynamic, currently hardcoded to first panel + $page = $this->getSession()->getPage(); + + $panel_toggle_element = $page->find('css', '.cms-content > .cms-panel > .cms-panel-toggle > .toggle-expand'); + assertNotNull($panel_toggle_element, 'Panel toggle not found'); + + if ($panel_toggle_element->isVisible()) { + $panel_toggle_element->click(); + } + } + + /** + * @When /^I click the "([^"]*)" CMS tab$/ + */ + public function iClickTheCmsTab($tab) + { + $cms_tabs_element = $this->getCmsTabsElement(); + + $tab_element = $cms_tabs_element->find('named', array('link_or_button', "'$tab'")); + assertNotNull($tab_element, sprintf('%s tab not found', $tab)); + + $tab_element->click(); + } + + /** + * @Then /^the "([^"]*)" table should contain "([^"]*)"$/ + */ + public function theTableShouldContain($table, $text) + { + $table_element = $this->getGridfieldTable($table); +var_dump($table_element); + $element = $table_element->find('named', array('content', "'$text'")); + assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $table)); + } + + /** + * @Then /^the "([^"]*)" table should not contain "([^"]*)"$/ + */ + public function theTableShouldNotContain($table, $text) + { + $table_element = $this->getGridfieldTable($table); + + $element = $table_element->find('named', array('content', "'$text'")); + assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $table)); + } + + /** + * @Given /^I click on "([^"]*)" in the "([^"]*)" table$/ + */ + public function iClickOnInTheTable($text, $table) + { + $table_element = $this->getGridfieldTable($table); + + $element = $table_element->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text)); + assertNotNull($element, sprintf('Element containing `%s` not found', $text)); + $element->click(); + } + + /** + * @Then /^I can see the preview panel$/ + */ + public function iCanSeeThePreviewPanel() + { + $this->getMainContext()->assertElementOnPage('.cms-preview'); + } + + /** + * @Given /^the preview contains "([^"]*)"$/ + */ + public function thePreviewContains($content) + { + $driver = $this->getSession()->getDriver(); + $driver->switchToIFrame('cms-preview-iframe'); + + $this->getMainContext()->assertPageContainsText($content); + $driver->switchToWindow(); + } + + /** + * @Given /^the preview does not contain "([^"]*)"$/ + */ + public function thePreviewDoesNotContain($content) + { + $driver = $this->getSession()->getDriver(); + $driver->switchToIFrame('cms-preview-iframe'); + + $this->getMainContext()->assertPageNotContainsText($content); + $driver->switchToWindow(); + } +} diff --git a/tests/behat/features/files/file1.jpg b/tests/behat/features/files/file1.jpg new file mode 100644 index 000000000..beb5a91b0 Binary files /dev/null and b/tests/behat/features/files/file1.jpg differ diff --git a/tests/behat/features/files/file2.jpg b/tests/behat/features/files/file2.jpg new file mode 100644 index 000000000..fc834a452 Binary files /dev/null and b/tests/behat/features/files/file2.jpg differ diff --git a/tests/behat/features/files/testfile.jpg b/tests/behat/features/files/testfile.jpg new file mode 100644 index 000000000..7aee322fe Binary files /dev/null and b/tests/behat/features/files/testfile.jpg differ diff --git a/tests/behat/features/login.feature b/tests/behat/features/login.feature new file mode 100644 index 000000000..b7a8388fd --- /dev/null +++ b/tests/behat/features/login.feature @@ -0,0 +1,20 @@ +# features/login.feature +Feature: Log in + As an site owner + I want to access to the CMS to be secure + So that only my team can make content changes + + Scenario: Bad login + Given I log in with "bad@example.com" and "badpassword" + Then I will see a bad log-in message + + Scenario: Valid login + Given I am logged in with "ADMIN" permissions + When I go to "/admin/" + Then I should see the CMS + + Scenario: /admin/ redirect for not logged in user + # disable automatic redirection so we can use the profiler + When I go to "/admin/" without redirection + Then I should be redirected to "/Security/login" + And I should see a log-in form \ No newline at end of file diff --git a/tests/behat/features/manage-files.feature b/tests/behat/features/manage-files.feature new file mode 100644 index 000000000..a0c09985d --- /dev/null +++ b/tests/behat/features/manage-files.feature @@ -0,0 +1,84 @@ +@javascript @assets +Feature: Manage files + As a cms author + I want to upload and manage files within the CMS + So that I can insert them into my content efficiently + + Background: + # Idea: We could weave the database reset into this through + # saying 'Given there are ONLY the following...'. + Given there are the following Folder records + """ + folder1: + Filename: assets/folder1 + folder1.1: + Filename: assets/folder1/folder1.1 + Parent: =>Folder.folder1 + folder2: + Filename: assets/folder2 + Name: folder2 + """ + And there are the following File records + """ + file1: + Filename: assets/folder1/file1.jpg + Name: file1.jpg + Parent: =>Folder.folder1 + file2: + Filename: assets/folder1/folder1.1/file2.jpg + Name: file2.jpg + Parent: =>Folder.folder1.1 + """ + And I am logged in with "ADMIN" permissions + # Alternative fixture shortcuts, with their titles + # as shown in admin/security rather than technical permission codes. + # Just an idea for now, could be handled by YAML fixtures as well +# And I am logged in with the following permissions +# - Access to 'Pages' section +# - Access to 'Files' section + And I go to "/admin/assets" + + @modal + Scenario: I can add a new folder + Given I press the "Add folder" button + And I type "newfolder" into the dialog + And I confirm the dialog + Then the "Files" table should contain "newfolder" + + Scenario: I can list files in a folder + Given I click on "folder1" in the "Files" table + Then the "folder1" table should contain "file1" + And the "folder1" table should not contain "file1.1" + + Scenario: I can upload a file to a folder + Given I click on "folder1" in the "Files" table + And I press the "Upload" button + And I attach the file "testfile.jpg" to "AssetUploadField" with HTML5 + And I wait for 5 seconds + And I press the "Back to folder" button + Then the "folder1" table should contain "testfile" + + Scenario: I can edit a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I fill in "renamedfile" for "Title" + And I press the "Save" button + And I press the "Back" button + Then the "folder1" table should not contain "testfile" + And the "folder1" table should contain "renamedfile" + + Scenario: I can delete a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I press the "Delete" button + Then the "folder1" table should not contain "file1" + + Scenario: I can change the folder of a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I fill in =>Folder.folder2 for "ParentID" + And I press the "Save" button + # /show/0 is to ensure that we are on top level folder + And I go to "/admin/assets/show/0" + And I click on "folder2" in the "Files" table + And the "folder2" table should contain "file1" \ No newline at end of file diff --git a/tests/behat/features/manage-users.feature b/tests/behat/features/manage-users.feature new file mode 100644 index 000000000..117e6bc14 --- /dev/null +++ b/tests/behat/features/manage-users.feature @@ -0,0 +1,87 @@ +@database-defaults +Feature: Manage users + As a site administrator + I want to create and manage user accounts on my site + So that I can control access to the CMS + + Background: + Given there are the following Permission records + """ + admin: + Code: ADMIN + security-admin: + Code: CMS_ACCESS_SecurityAdmin + """ + And there are the following Group records + """ + admingroup: + Title: Admin Group + Code: admin + Permissions: =>Permission.admin + staffgroup: + Title: Staff Group + Code: staffgroup + """ + And there are the following Member records + """ + admin: + FirstName: Admin + Email: admin@test.com + Groups: =>Group.admingroup + staffmember: + FirstName: Staff + Email: staffmember@test.com + Groups: =>Group.staffgroup + """ + And I am logged in with "ADMIN" permissions + And I go to "/admin/security" + + @javascript + Scenario: I can list all users regardless of group + When I click the "Users" CMS tab + Then I should see "admin@test.com" in the "#Root_Users" element + And I should see "staffmember@test.com" in the "#Root_Users" element + + @javascript + Scenario: I can list all users in a specific group + When I click the "Groups" CMS tab + # TODO Please check how performant this is + And I click "Admin Group" in the "#Root_Groups" element + Then I should see "admin@test.com" in the "#Root_Members" element + And I should not see "staffmember@test.com" in the "#Root_Members" element + + @javascript + Scenario: I can add a user to the system + When I click the "Users" CMS tab + And I press the "Add Member" button + And I fill in the following: + | First Name | John | + | Surname | Doe | + | Email | john.doe@test.com | + And I press the "Create" button + Then I should see a "Saved member" message + + When I go to "admin/security/" + Then I should see "john.doe@test.com" in the "#Root_Users" element + + @javascript + Scenario: I can edit an existing user and add him to an existing group + When I click the "Users" CMS tab + And I click "staffmember@test.com" in the "#Root_Users" element + And I select "Admin Group" from "Groups" + And I additionally select "Administrators" from "Groups" + And I press the "Save" button + Then I should see a "Saved Member" message + + When I go to "admin/security" + And I click the "Groups" CMS tab + And I click "Admin Group" in the "#Root_Groups" element + Then I should see "staffmember@test.com" + + @javascript + Scenario: I can delete an existing user + When I click the "Users" CMS tab + And I click "staffmember@test.com" in the "#Root_Users" element + And I press the "Delete" button + Then I should see "admin@test.com" + And I should not see "staffmember@test.com" \ No newline at end of file