diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2659611
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+composer.lock
diff --git a/README.md b/README.md
index 4125f53..dc05439 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,31 @@
-# TDD Dublin demo module
+# TDD Example Drupal 8 Blog Module
-A demo module to accompany my [TDD Test Driven Drupal][0] talk at DrupalCamp
+A demo module to accompany my [TDD - Test Driven Drupal][0] talk, originally for DrupalCamp
Dublin 2017.
+In order to see my workflow of writing comments first, converting them into
+failing tests, and then writing the implementation code to make them pass, you
+can see the [list of previous commits][1] and see each step taken, as well as
+[the tags][2] that identify the commits when each failing test is added and
+then subsequently passes.
+
## Acceptance Criteria
This module will be used to demonstrate how to take a test-driven approach to
develop a module to the following acceptance criteria:
- As a site visitor
-- I want to see a list of all published pages at `/pages`
-- Ordered alphabetically by title
+- I want to see a list of all published blog posts at `/blog`
+- Ordered by post date, with the newest posts first
+
+## Installation
+
+Within your Drupal 8 site:
+
+```bash
+cd modules
+git clone git@github.com:opdavies/drupal-module-tdd-blog.git tdd_blog
+```
## Running the Tests
@@ -18,23 +33,23 @@ These tests are functional tests based on the `BrowserTestBase` class so need
to be executed with PHPUnit (which is required in core's `composer.json` file).
The path to your `vendor` directory may be different depending on your setup.
-Because of autoloading, you will need to be inside Drupal's `core` subdirectory
-when running the tests for them to execute successfully.
+Because of autoloading, you will either need to be inside Drupal's `core` subdirectory
+, or add `-c core` to the PHPUnit command when running the tests for them to execute successfully.
This also assumes that the module is within a `modules/custom` directory and
-named `tdd_dublin` as per the repository name.
+named `tdd_blog` as per the repository name.
```
-cd core
-
-../vendor/bin/phpunit ../modules/custom/tdd_dublin
+vendor/bin/phpunit -c core modules/custom/tdd_blog
```
You can use PHPUnit's `--filter` option to specify a single test method to run,
rather than all of the tests within the module. For example:
```
-../vendor/bin/phpunit ../modules/custom/tdd_dublin --filter=testOnlyPublishedPagesAreShown
+vendor/bin/phpunit -c core modules/custom/tdd_blog --filter=testOnlyPublishedPagesAreShown
```
[0]: https://www.oliverdavies.uk/talks/tdd-test-driven-drupal
+[1]: https://github.com/opdavies/drupal-module-tdd-blog/commits/HEAD
+[2]: https://github.com/opdavies/drupal-module-tdd-blog/tags
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..aa7e974
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,4 @@
+{
+ "name": "drupal/tdd_blog",
+ "type": "drupal-custom-module"
+}
diff --git a/config/install/node.type.article.yml b/config/install/node.type.article.yml
new file mode 100644
index 0000000..1fd439c
--- /dev/null
+++ b/config/install/node.type.article.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies: { }
+name: Article
+type: article
+description: 'Use articles for time-sensitive content like news, press releases or blog posts.'
+help: ''
+new_revision: true
+preview_mode: 1
+display_submitted: true
diff --git a/config/install/node.type.page.yml b/config/install/node.type.page.yml
deleted file mode 100644
index a6ddd46..0000000
--- a/config/install/node.type.page.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-langcode: en
-status: true
-dependencies: { }
-_core:
- default_config_hash: KuyA4NHPXcmKAjRtwa0vQc2ZcyrUJy6IlS2TAyMNRbc
-name: 'Basic page'
-type: page
-description: 'Use basic pages for your static content, such as an ''About us'' page.'
-help: ''
-new_revision: true
-preview_mode: 1
-display_submitted: false
diff --git a/config/install/views.view.pages.yml b/config/install/views.view.blog.yml
similarity index 73%
rename from config/install/views.view.pages.yml
rename to config/install/views.view.blog.yml
index bf293cd..783846f 100644
--- a/config/install/views.view.pages.yml
+++ b/config/install/views.view.blog.yml
@@ -1,11 +1,13 @@
langcode: en
status: true
dependencies:
+ config:
+ - node.type.article
module:
- - node
- - user
-id: pages
-label: pages
+ - node
+ - user
+id: blog
+label: Blog
module: views
description: ''
tag: ''
@@ -136,22 +138,62 @@ display:
expose:
operator: ''
group: 1
+ type:
+ id: type
+ table: node_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value:
+ article: article
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ reduce: false
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: node
+ entity_field: type
+ plugin_id: bundle
sorts:
created:
id: created
table: node_field_data
field: created
- order: DESC
- entity_type: node
- entity_field: created
- plugin_id: date
relationship: none
group_type: group
admin_label: ''
+ order: ASC
exposed: false
expose:
label: ''
granularity: second
+ entity_type: node
+ entity_field: created
+ plugin_id: date
header: { }
footer: { }
empty: { }
@@ -161,11 +203,11 @@ display:
cache_metadata:
max-age: -1
contexts:
- - 'languages:language_content'
- - 'languages:language_interface'
- - url.query_args
- - 'user.node_grants:view'
- - user.permissions
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
tags: { }
page_1:
display_plugin: page
@@ -174,13 +216,13 @@ display:
position: 1
display_options:
display_extenders: { }
- path: pages
+ path: blog
cache_metadata:
max-age: -1
contexts:
- - 'languages:language_content'
- - 'languages:language_interface'
- - url.query_args
- - 'user.node_grants:view'
- - user.permissions
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
tags: { }
diff --git a/tdd_dublin.info.yml b/tdd_blog.info.yml
similarity index 75%
rename from tdd_dublin.info.yml
rename to tdd_blog.info.yml
index 448351b..1cdb6c0 100644
--- a/tdd_dublin.info.yml
+++ b/tdd_blog.info.yml
@@ -1,6 +1,7 @@
-name: 'TDD Dublin'
+name: 'TDD Blog'
description: 'A demo module for DrupalCamp Dublin to show test driven module development.'
core: 8.x
+core_version_requirement: ^8 || ^9
type: module
dependencies:
diff --git a/tests/src/Functional/PageListTest.php b/tests/src/Functional/PageListTest.php
index 9c1d9e6..6205de1 100644
--- a/tests/src/Functional/PageListTest.php
+++ b/tests/src/Functional/PageListTest.php
@@ -1,24 +1,20 @@
drupalGet('pages');
- $this->assertSession()->statusCodeEquals(200);
+ protected $defaultTheme = 'stark';
+
+ public function testBlogPageExists() {
+ $this->drupalGet('blog');
+
+ $this->assertSession()->statusCodeEquals(Response::HTTP_OK);
}
}
diff --git a/tests/src/Kernel/PageListTest.php b/tests/src/Kernel/PageListTest.php
new file mode 100644
index 0000000..2e5cb48
--- /dev/null
+++ b/tests/src/Kernel/PageListTest.php
@@ -0,0 +1,99 @@
+installEntitySchema('node');
+ $this->installEntitySchema('user');
+
+ $this->installConfig(['filter', 'tdd_blog']);
+ }
+
+ /**
+ * Ensure that only the correct nodes are returned.
+ *
+ * Ensure that only published pages are returned by the view. Unpublished
+ * pages or content of different types should not be shown.
+ */
+ public function testOnlyPublishedArticlesAreShown() {
+ // This is a published article, so it should be visible.
+ $this->createNode(['type' => 'page', 'status' => TRUE]);
+
+ // This is a page, so it should not be visible.
+ $this->createNode(['type' => 'article']);
+
+ // This article is not published, so it should not be visible.
+ $this->createNode(['type' => 'article', 'status' => FALSE]);
+
+ // Rather than testing the rendered HTML, we are going to load the view
+ // results programmatically and run assertions against the data it returns.
+ // This makes it easier to test certain scenarios, and ensures that the
+ // test is future-proofed and won't fail at a later date due to a change in
+ // the presentation code.
+ $nids = $this->getViewResults();
+
+ // Only node 1 matches the criteria of being a published page, so only that
+ // node ID should be being returned from the view. assertEquals() can be
+ // used to compare the expected result to what is being returned.
+ $this->assertEquals([2], $nids);
+ }
+
+ /**
+ * Ensure that the results are ordered by title.
+ */
+ public function testArticlesAreOrderedByDate() {
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 day'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 month'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+3 days'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 hour'))->getTimestamp()]);
+
+ // Get the result data from the view.
+ $nids = $this->getViewResults();
+
+ // Compare the expected order based on the titles defined above to the
+ // ordered results from the view.
+ $this->assertEquals([4, 1, 3, 2], $nids);
+ }
+
+ /**
+ * Load the view and get the results.
+ *
+ * @param string $view
+ * (optional) The name of the view. Defaults to 'blog'.
+ *
+ * @return array
+ * An array of returned entity IDs.
+ */
+ private function getViewResults($view = 'blog') {
+ return array_map(function (ResultRow $result) {
+ return $result->_entity->id();
+ }, views_get_view_result($view));
+ }
+
+}