Skip to content

Commit

Permalink
Update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh Bassett committed Mar 10, 2017
1 parent be1054c commit 93c8ef6
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.gem
.bundle
.yardoc
Gemfile.lock
1 change: 1 addition & 0 deletions .yardopts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--markup markdown
84 changes: 61 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
# Mache
# Mâché

[![Build Status](https://travis-ci.org/nullobject/mache.svg?branch=master)](https://travis-ci.org/nullobject/mache)

A [page object](https://martinfowler.com/bliki/PageObject.html) library for writing cleaner acceptance tests with [Capybara](https://github.com/teamcapybara/capybara).
Mâché (pronounced "mash-ay") helps you to write cleaner and more expressive
acceptance tests for your web applications using page objects.

## What is a page object?

A [page object](https://martinfowler.com/bliki/PageObject.html) is a data
structure which provides an interface to your web application for the purposes
of test automation. For example, it could represent a single HTML page, or
perhaps even a fragment of HTML on a page.

From Martin Fowler:

> A page object wraps an HTML page, or fragment, with an application-specific
> API, allowing you to manipulate page elements without digging around in the
> HTML.
[Capybara](https://github.com/teamcapybara/capybara) can only get us part of
the way there. It allows us to work with an API rather than manipulating the
HTML directly, but what it provides isn't an *application specific* API. It
gives us low-level API methods like `find`, `fill_in`, and `click_button`, but
it doesn't provide us with high-level methods to do things like "sign in to the
app" or "click the Dashboard item in the navigation bar".

This is where page objects come in. Using Mâché we can for instance define a
page object class called `SignInPage` and use it any time we want to automate
authenticating with our app. It could handle visiting the sign in page,
entering the user's credentials, and clicking the "Sign in" button.

## Getting started

Consider the following HTML snippet:
Let's dive straight in and take a look at an example. Consider the following
HTML fragment for the welcome page in our app:

```html
<html>
Expand All @@ -26,9 +53,9 @@ Consider the following HTML snippet:
</html>
```

To define a page object class to wrap this HTML snippet, we extend the
`Mache::Page` class. The only method our class needs to provide is `path`, this
tells Mache where to go when we want to visit the page.
To define a `WelcomePage` page object class to wrap this HTML page, we extend
the `Mache::Page` class. The only method our class needs to provide is `path`,
this tells Mâché where to go when we want to visit the page:

```ruby
class WelcomePage < Mache::Page
Expand All @@ -38,26 +65,32 @@ class WelcomePage < Mache::Page
end
```

This is how we visit our page object:
We can visit our welcome page using our page object:

```ruby
page = WelcomePage.visit
page.current? # true
```

Any Capybara API methods will be forwarded to the underlying node:
Mâché also handily exposes the Capybara API on our page object:

```ruby
page.find("body > main").text # "lorem ipsum"
```

We can also use the `node` attribute to get the underlying Capybara node object:

```ruby
page.node # <Capybara::Node>
```

### Elements

To make our page object more useful, we can define an element on our page
object class using the `element` macro. An element is simply a HTML element
that we expect to find on the page using a CSS selector.

Let's define a `main` element:
Let's define a `main` element to represent the main section of our HTML page:

```ruby
class WelcomePage < Mache::Page
Expand All @@ -69,7 +102,7 @@ class WelcomePage < Mache::Page
end
```

Now we can query the element on our page object:
We can query the `main` element as an attribute of our page object:

```ruby
page.has_main? # true
Expand All @@ -79,19 +112,21 @@ page.main.text # "lorem ipsum"

### Components

For elements that can be shared across an number of page object classes, it may
For elements that can be shared across any number of page object classes it may
be useful to define a reusable component by extending the `Mache::Component`
class. A component class can contain any number of elements or other
components:
class. A component can contain any number of elements (or even other
components).

Let's define a `Header` component to represent the header of our HTML page:

```ruby
class Header < Mache::Component
element :title, "h1"
end
```

Our page object class can mount our component at a given CSS selector using the
`component` macro:
We can mount the `Header` component in our page object class at a given CSS
selector using the `component` macro:

```ruby
class WelcomePage < Mache::Page
Expand All @@ -104,7 +139,7 @@ class WelcomePage < Mache::Page
end
```

Querying a component on our page object is much the same as an element:
Querying a component of our page object is much the same as with an element:

```ruby
page.has_header? # true
Expand All @@ -114,7 +149,9 @@ page.header.title.text # "Welcome"

## Example

Let's look at a more complete example for our `WelcomePage`:
Let's look at a more complete example for our `WelcomePage`. Note that the
`Header`, `NavItem`, and `Nav` components can be reused in any other page
object classes we may define later for our web application.

```ruby
class Header < Mache::Component
Expand Down Expand Up @@ -142,28 +179,29 @@ class WelcomePage < Mache::Page
end
```

We can use our page objects to write expressive tests:
We can use our page objects to write very expressive acceptance tests:

```ruby
feature "Home page" do
feature "Welcome page" do
let(:home_page) { WelcomePage.visit }

scenario "A user visits the home page" do
scenario "A user visits the welcome page" do
expect(home_page).to be_current

# header
expect(home_page).to have_header
expect(home_page.header.title).to eq("Welcome")

# nav
expect(home_page).to have_nav
expect(home_page.nav).to have_items
expect(home_page.nav.items.count).to be(3)

expect(home_page.nav.items[0]).to be_selected
expect(home_page.nav.items[0].text).to eq("foo")
expect(home_page.nav.items[1].text).to eq("bar")
expect(home_page.nav.items[2].text).to eq("baz")

expect(home_page.nav.items[0]).to be_selected

# main
expect(home_page.main.text).to eq("lorem ipsum")
end
end
Expand Down
16 changes: 16 additions & 0 deletions lib/mache/component.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
require "mache/node"

module Mache
# The Component class wraps a fragment of HTML and can be used in any number
# of {Page} classes using the `component` macro. A component can contain
# elements and other components.
#
# @example
#
# class NavItem < Mache::Component
# def selected?
# node[:class].include?("selected")
# end
# end
#
# class Nav < Mache::Component
# components :items, NavItem, "a"
# end
#
class Component < Node
end
end
10 changes: 6 additions & 4 deletions lib/mache/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ def elements(name, selector)
define_helper_methods(name, selector)
end

def component(name, type, selector)
def component(name, klass, selector)
define_method(name.to_s) do
type.new(@node.find(selector))
klass.new(node: @node.find(selector))
end
define_helper_methods(name, selector)
end

def components(name, type, selector)
def components(name, klass, selector)
define_method(name.to_s) do
@node.all(selector, minimum: 1).map { |element| type.new(element) }
@node.all(selector, minimum: 1).map do |element|
klass.new(node: element)
end
end
define_helper_methods(name, selector)
end
Expand Down
19 changes: 15 additions & 4 deletions lib/mache/node.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
require "mache/dsl"

module Mache
# An abstract class that wraps a capybara node object and exposes the mache
# DSL. It also delegates any capybara methods to the underlying node object.
# The Node class represents a wrapped HTML page, or fragment. It exposes all
# methods from the Mache {DSL}, and forwards any Capybara API methods to the
# {#node} object.
#
# @abstract
class Node
include DSL

# The underlying Capybara node object wrapped by this node.
#
# @return [Capybara::Node] the node object
attr_reader :node

def initialize(node)
@node = node
# Returns a new instance of Node.
#
# @param node [Capybara::Node] the Capybara node object to wrap
def initialize(node:)
@node ||= node
end

# Forwards any Capybara API calls to the node object.
def method_missing(name, *args, &block)
if @node.respond_to?(name)
@node.send(name, *args, &block)
Expand All @@ -20,6 +30,7 @@ def method_missing(name, *args, &block)
end
end

# @!visibility private
def respond_to_missing?(name, include_private = false)
@node.respond_to?(name) || super
end
Expand Down
39 changes: 36 additions & 3 deletions lib/mache/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,60 @@
require "mache/node"

module Mache
# A page provides a DSL for querying a wrapped capybara node object node. A
# page can also has a path which can be visited.
# The Page class wraps an HTML page with an application-specific API. You can
# extend it to define your own API for manipulating the pages of your web
# application.
#
# @example
#
# class WelcomePage < Mache::page
# element :main, "#main"
# component :nav, Nav, "#nav"
# end
#
# page = WelcomePage.new(path: "/welcome")
# page.visit
# page.current # true
# page.main.text # lorem ipsum
#
class Page < Node
# The path where the page is located, without any domain information.
#
# @return [String] the path string
# @example
# "/welcome"
# "/users/sign_in"
attr_reader :path

# Returns a new page object.
#
# @param node [Capybara::Node] the Capybara node to attach to
# @param path [String] the path where the page is located
def initialize(node: Capybara.current_session, path: nil)
@node ||= node
@path ||= path
end

# Visits the page at its {#path}.
#
# @return [Page] the page object
def visit
@node.visit(path)
self
end

# Tests whether the page is current.
#
# @return [Boolean] `true` if the page is current, `false` otherwise
def current?
@node.current_path == path
end

# Creates a new page object and calls {#visit} on it.
#
# @return [Page] the page object
def self.visit
new.tap(&:visit)
new.visit
end
end
end
4 changes: 2 additions & 2 deletions mache.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
spec.version = Mache::VERSION
spec.authors = ["Joshua Bassett"]
spec.email = ["[email protected]"]
spec.summary = "A page object library for writing cleaner acceptance tests with Capybara."
spec.description = "Mache provides a DSL for writing page object clases to use in your acceptance tests."
spec.summary = "A library for writing cleaner and more expressive acceptance tests using page objects."
spec.description = "Mâché provides helps you to write cleaner and more expressive acceptance tests for your web applications using page objects."
spec.homepage = "https://github.com/nullobject/mache"
spec.license = "MIT"
spec.files = `git ls-files -z`.split("\x0").reject do |f|
Expand Down

0 comments on commit 93c8ef6

Please sign in to comment.