diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..dbad17d --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,248 @@ +# Tutorial: Testing Form Submissions + +## Overview + +In this tutorial we'll show you how to test the submission +of a SilverStripe form by remote controlling a browser. +Along the way, you'll learn how to create database fixtures +and make assertions about any changed state in the database. + +In order to illustrate these concepts, we'll create a "report this page" +feature which shows at the bottom of each page, and gives its visitors +an opportunity to help discover and fix problems with its content. +The "report" consists of a form with a dropdown, its submission +is stored in a custom SilverStripe database object. + +![](https://www.monosnap.com/image/Xa94a2DBdcrZ21mKYVzTGXCHF.png) + +## Preparation + +First of all, check out a default SilverStripe project +and ensure it runs on your environment. Detailed installation instructions +can be found in the [README](../README.md) of this module. +Once you've got the SilverStripe project running, make sure you've +started Selenium. With all configuration in place, initialize Behat +for + + vendor/bin/behat --init @mysite + +This will create a location for our feature files later on, +in `mysite/tests/behat/features`. +Note that the module doesn't need its own `behat.yml` configuration +file since it reuses the one in the root folder. + +## Feature Specification + +One major goal for "testing by example" through Behat is bringing +the tests closer to the whole team, by making them part of the agile +process and ideally have the customer write some tests in his own +language (read more about this concept at +[dannorth.net](http://dannorth.net/whats-in-a-story/)). + +In this spirit, we'll start "from the outside in", and +write our features before implementing them. A first draft might look +something like the following: + + Feature: Report Abuse + + As a website user + I want to report inappropriate content + In order to maintain high quality content + + Scenario: Report abuse through preselected options + Given I go to a page + Then I should see "Report this page" + When I select "Outdated" + And I press the button + Then I should see "Thanks for your submission" + +The "syntax" conventions used here are called the +["Gherkin" language](https://github.com/cucumber/cucumber/wiki/Gherkin). +It is fairly free-form, with only few rules about indentation and +keywords such as `Feature:`, `Scenario:` or `Given`. +Each of the actual steps underneath `Scenario` needs to map +to logic which knows how to tell the browser what to do. + +Let's try to run our scenario: + + vendor/bin/behat --ansi @mysite + +We'll see all steps marked as "not implemented" (orange). +Thankfully Behat already comes with a lot of step definitions, +so our next move is to review what's already available: + + vendor/bin/behat --di @mysite + +The step definitions include form interactions, so we only +need to adjust our steps a bit to make them executable. + + Scenario: Report abuse through preselected options + Given I go to a page + Then I should see "Report this page" + When I select "Outdated" from "Reason" + And I press "Submit Report" + Then I should see "Thanks for your submission" + +This type of refactoring is quite common, since step definitions +are ideally abstracted and shared between features. In this case, +we needed to make some steps less ambiguous, for example +referencing which form field to select from. We haven't written +the code for this form field yet, but its easy enough to label +it "Reason" without knowing much about its implementation. +Run the tests again, and you should see some steps marked +as "skipped" instead of "not implemented". + +There's still a bit of ambiguity in our feature: +Each test run starts with a clean database, meaning there's no pages +to open in a browser either. Let's fix this by defining one, +and asking Behat to open it: + + Scenario: Report abuse through preselected options + Given a "page" "My Page" + Given I go to the "page" "My Page" + ... + +## SilverStripe Code + +Enough theory, we have a good idea of what our feature should do, +let's get down to coding! We won't get too much into details, +but overall we're creating a new `PageAbuseReport` class with +a `has_one` relationship to `Page`. This new object gets written +by a form generated through `Page_Controller->ReportForm()`. + + ```php + // mysite/code/Page.php + class Page extends SiteTree { + private static $has_many = array('PageAbuseReports' => 'PageAbuseReport'); + } + class Page_Controller extends ContentController { + + // ... + + private static $allowed_actions = array('ReportForm'); + + public function ReportForm() { + return new Form( + $this, + 'ReportForm', + new FieldList( + new HeaderField('ReportTitle', 'Report this page', 3), + (new DropdownField('Reason', 'Reason')) + ->setSource(array( + 'Inappropriate' => 'Inappropriate', + 'Outdated' => 'Outdated', + 'Misleading' => 'Misleading', + )) + ), + new FieldList( + new FormAction('doReport', 'Submit Report') + ) + ); + } + + public function doReport($data, $form) { + (new PageAbuseReport(array( + 'Reason' => $data['Reason'], + 'PageID' => $this->ID, + )))->write(); + $form->sessionMessage('Thanks for your submission!', 'good'); + + return $this->redirectBack(); + } + + } + ``` + + ```php + // mysite/code/PageAbuseReport.php + class PageAbuseReport extends DataObject { + private static $db = array('Reason' => 'Text'); + private static $has_one = array('Page' => 'Page'); + } + ``` + +Now we just need to render the form on every page, +by placing it at the bottom of `themes/simple/templates/Layout/Page.ss`: + + ```html +