# Welcome Puppetry

Puppetry is an open-source desktop application that gives non-developers the ability to create, manage, and integrate automated tests for Web.

* [Releases](https://github.com/dsheiko/puppetry/releases)
* [Issues](https://github.com/dsheiko/puppetry/issues)
* [Slack channel](https://puppetry-app.slack.com)
* [Facebook page](https://www.facebook.com/puppetry.testing)

End-to-end testing of a web application is mostly about locating a target (page or DOM element), performing a command on it (navigate to URL, modify DOM, etc.) and asserting that the target satisfies the provided conditions. Puppetry provides a user-friendly interface to define targets and manage test cases bundled in test suits. During test run (or export) Puppetry suites are turned into [Jest](https://jestjs.io) test files powered by [Puppeteer API](https://pptr.dev/).&#x20;

Puppetry deals with projects, where [**project**](https://docs.puppetry.app/project) is basically a folder with project configuration, reusable snippets and suite files.  [**Suite**](https://docs.puppetry.app/suite) contains [**targets**](https://docs.puppetry.app/target) and [**groups**](https://docs.puppetry.app/group) of [**tests cases**](https://docs.puppetry.app/test-case), where test case is a sequence of [**test steps**](https://docs.puppetry.app/test-case) (**commands**, **assertions** and **references**). Besides, project includes environment-dependent [**template variables**](https://docs.puppetry.app/template) and [**git configuration**](https://docs.puppetry.app/version-control).


# Getting Started

When you launch Puppetry the very first time you are expected to see the **welcome page**:

![Puppetry Welcome page](/files/-Lu7PNVoV23XDszCRlBa)

At this stage we cannot do much, only to **open** an existing project or **create** one:

![File menu](/files/-Lu7PTSfSQ3L-iqzKMmc)

Let's create our first project intended to test responsive design on the landing page or our [imaginary ACME forum application](https://docs.puppetry.app/examples).

![New project modal window](/files/-LhLSIYIX6I6xwLOB61S)

So we enter project name, browse for location and provide a name for the first suite. After pressing submit we end up on the suite page:

![Newly created suite](/files/-Lu7Pfsm80G5MIMDY-jl)

Let's add a few targets:

![ACME forum test targets](/files/-Lu7PmOroeD4N1Qy85Nh)

Here we identify main menu container, which is visible on desktop, but behind the burger menu on mobile. We define teaser target (the first matching the selector), which also visible only on desktop. Eventually we specify mobile menu container that on the contrary shows up on mobile, but hidden on desktop.

Now we switch to the **Test Cases** tab:

![No test cases so far](/files/-Lu7QrkVz-_IyOuo-QYF)

Let's add the first group "Landing page":

![The first test group added](/files/-Lu7Q0jHNZrbAToi2n3h)

Under the group record we get a new form to enter test case name. So add one:

![The first test case added](/files/-Lu7Qg759sdAbBjx1zjI)

Let's add our first command into the test case. So we click on **Add a command/assertion** button:

![Add a command/assertion button](/files/-LhLWo6WE1L6wSPLnEvj)

That brings us to **Edit Command/Assertion** modal window:

![Edit Command/Assertion modal window](/files/-Lu7RsY6mA1Z_NR1TcxZ)

Our goal is to ensure desktop-like screen size. So we select **page** target and **setViewport** method. We set screen width **1440** and height **900**:

![Adding command to set viewport](/files/-Lu7R8dshbp_-lCqTkTa)

Many of test steps have additional options collapsed under **Advanced Options** link. E.g:

![Advanced options of page.setViewport command](/files/-Lu7Rl86qbQK6utNj0oG)

Here we don't need to change them. We click on **Save** button and get back to test case editing, where we click on **Add a command/assertion** button again to set navigation URL:

![Adding command to visit forum landing page](/files/-Lu7Rz9ak2VrSSfFWANw)

Now we have to assert that desktop menu and teasers are visible in this test case, but mobile menu not. So we click **Add a command/assertion** button and select a target identifier corresponding to mobile menu container:

![Selecting a target](/files/-Lu7SZvra94BVn7XKery)

Next we need `assertVisible` method, so we focus on method input and start typing **"assert"**:

![Searching for a method](/files/-Lu7SU_xVyWUgkUNeCvz)

The list of options adjust according to search keyword, so we can easily find the required method.

![Asserting MENU\_DESKTOP\_CONTAINER target is available and visible](/files/-LuDGagP9kldDwzU_fwq)

We take **"available on the page and observable"** option in the select "The target is". This means the element available in page DOM and can be visually located. We save the changes. Similarly we create assertions for desktop menu and teaser, but take option **"available, but NOT observable"**. Finally we add command `page.screenshot` and our first test case is ready:

![Test case of RWD on landing page for 1440x900 display](/files/-Lu7TWj9U5Qa5Eu6Qc_g)

I would suggest now to run the tests and check if everything is ok. So we press **F6** and get the following modal window:

![Run Tests modal window](/files/-Lu7TyhYX6I_w10iRXNC)

Here we just click on **Run** button.&#x20;

As we run it Puppetry opens panel **Test report** with test results:

![Test results](/files/-Lu7V757Al7V3ZN41pLo)

It says everything is fine. If we click on the screenshot thumbnail it gets open in the viewer.

![Screenshot in the viewer](/files/-Lu7WB93_s2z-LfdlzDZ)

Alternatively we can click on **Download** link, which appears when we hover the thumbnail.

![Download screenshot](/files/-Lu7Uz5-g6T5_CnaCKFl)

Then the screenshot will be open with default viewer in your hosting OS.

Let's now go further and create a new test case named **on iPhone 8 mobile menu displayed**. We start it with the command to emulate iPhone 8 device:

![Adding command to emulate iPhone 8](/files/-Lu7XMFivL8LFh4DXn8m)

Next we need a command to visit the landing page and assertions from the previous case. Let's just copy/paste them. We **click** on the every command **with Shift pressed** to select records.

![Selecting records for a bulk action](/files/-Lu7YDoMiXWT8S9JzYwU)

Then right-click on the selection and pick the **Copy** option in the context-menu.

![Copying the selection](/files/-Lu7YSL5JZNhuCHt1MIm)

Now we focus the destination - `page.emulate` command of the second test case and right-click again:

![Pasting the selection](/files/-Lu7YoXjmfd9O8K_RSwj)

We have just copied, bu the are still addressing the desktop view and need to be adjusted for iPhone device. But let's see what happens if we run the tests with failing assertions. So we press **F6** and observer the test report:

![Test report with failing assertion](/files/-Lu7ZjM-f4Tuy9DjheH4)

So you see it warns us about the failure and provides detailed information why the assertion failed.

If we switch to suite tab (rwd.json) we see that failed assertion record is highlighted:

![Failing assertion gets highlighted](/files/-Lu7_8uCljcuoi_y6Q0p)

When working with test cases (especially debugging) sometimes you will want to temporarily disable some test steps and run only the ones you currently focused on. That can be done from the context menu (**right-click**). The disabled test steps will be also highlighted:

![Disabled test steps are highlighted](/files/-Lu7_KqdtxxdILMn8kmp)


# Project

Project is basically a folder with project configuration (including template variables and environments) (`.puppetryrc`), git configuration (`.puppetrygit`), reusable snippets file (`.snippets.json`) and suite files (`*.json`) . Project also keeps information about all latelly opened panels, expandable rows.&#x20;

You can use hotkey **Ctrl+S** (⌘S) to save project (and open suite).

## Create New Project

To create a new project you can press **Ctrl+Shift-N** (⌘⇧N)  or click on **File / New Project** item in the main menu:

![New Project in the main menu](/files/-LheK0gHD2nclL8qsRQA)

It will open the following modal window:

![New Project modal window](/files/-LhLSIYIX6I6xwLOB61S)

You need to provide project and suite names in the corresponding fields and use **Browse...** button to select project location. Then you press **Create** button.&#x20;

## Open Project

To open project you can press **Ctrl+Shift-O** (⌘⇧O)  or click on **File / Open Project** item in the main menu:

![Open Project in the main menu](/files/-LheJyg9GemOuSWruoL_)

It will open the following modal window:

![](/files/-LheKFSLXv0bVXwZELa6)

You need to use **Browse...** button to select project location. Then you press **Open** button.&#x20;

## Edit Project

When you have a project open, you can still change the project name. Click on the edit icon next to the project name in the application header:

![Edit project icon](/files/-LheKR1Y7WAB78IJB0tl)

It opens the following modal window:

![Edit Project modal window](/files/-LheKTqTiW34dRA9zBHu)

So you can provide a new name and press **Save** button.

{% hint style="info" %}
This modal also has a read-only field App data directory, showing where the Electron project is located (application storage, run-time tests export and so on)
{% endhint %}

## Save Project As..

You can clone the project to a new location. To do it click on **File / Save Project As...** item in the main menu:

![Save Project As.. in the main menu](/files/-LheLOWpHrJ4v6bxSxlr)

And select a new location in the following modal window:

![Save Project As... modal window](/files/-LheLjnCIMXNx8vQRU_x)

## Project Page

If you close all the application tabs you can see the project page:

![Project page](/files/-Lu3dW5d5meumJFh_KAb)

{% hint style="info" %}
If you forgot the physical location of your Puppetry project, use Project page to find it out
{% endhint %}

## Project Explorer

You can find the project explorer window below the main menu as soon as you have opened the very first project. The explorer helps you to switch between recently opened projects and manage the list of active project suites.

![Project explorer](/files/-Lu7-qPoCJK3ng4qdjF5)

Right in the header there two buttons. The first one to create a new project.

![Create new project](/files/-Lu7-yvM-NwoSjlWl6py)

The second button is to open an existing project.

![Open project](/files/-Lu702-RZpY85tzsYNtk)

In order to **focus a project** or suite you click on it once. To **open a project** or a suite you double-click on it.‌

Right-click on a suite opens the context menu where you can choose an action: **open** or **delete** (suite file):

![Suite context menu‌](/files/-LhKxLwvQt9gGJvKCHYB)

Right click on project displays the context menu where you choose between **open project** and **remove project from the list**. The last one doesn't physically remove project folder, but simply won't show it anymore in project explorer unless you open via main menu again.

![Project context menu](/files/-LhKy6yHSpeiMRcb6anw)


# Suite

Suite is literally a JSON file keeping suite-related targets, groups, test-cases and test-steps.

## Create Suite

To create a new project you can press **Ctrl+N** (⌘N)  or click on **File / New Suite** item in the main menu:

![New Suite in the main menu](/files/-LhewMjZirrIeSlPPt-m)

It opens the following modal window:

![New Suite modal window](/files/-LhewXBRLkt9kg3PGf_z)

You need to specify suite name and press **Create** button. It will create a json file with name built from provided one and open it.

Optionally you can give a custom name to the file. Just click to open **Specify filename** panel:

![Custom filename for suite](/files/-Lhex38yAFv8tsx0EjoB)

## Record Suite

After you created anew suite you can either populate it manually or record it. To go with the last option, click on **Record** button in the footer of **Groups** panel:

![Record suite button](/files/-LhkDYgvTI6O0zjJuPnZ)

It opens a new window:

![Suite Recorder window: Initial state](/files/-LuDYJlc2GJ-c_psLvQL)

Here you can

1\) Select viewport&#x20;

![Select viewport combobox](/files/-LhkE09lugZsKU0aILMZ)

2\) Set a color for element highlighting on test pages

![Color selector](/files/-LhkEAur67dVM_6vyn2J)

3\) Enter URL or test page and press **ENTER**. It will bring you to the specified page:

![Suite Recorder window: URL entered](/files/-LuDYO1Q85qyC-wyZcaW)

Now you can interact with the page (click on elements, type in inputs and so on). Any affected element will be automatically registered into targets. But you can **right-click** (or **Ctrl-Shift-click) an element** to register it into targets explicitly.&#x20;

![After Ctrl-Shift-click you will be prompted for a target name](/files/-LhkGiebnxtLV_mVt7I-)

You can also find recorder console below the address bar.

![Recorder console](/files/-LuDYb1p-1YamO-KmuD_)

It prints all the user actions intercepted by the recorder. If you hover the console it expands showing you more of the log.

![Recorder log](/files/-LuDYfQ7GDp_6MK4JF7Z)

Left to the **Create Suite** button you can see an icon:

![DevTools button](/files/-LuDYjsGMLmh0cruuWXO)

Click it when you need **Chrome DevTools**.

4\) When you are done, click on **Create Suite** button in the header. That will bring you back to the **Groups** panel, where you will see a newly created **Recorded group:**

![Recorded group collapsed](/files/-LuDYrR_8dJmO_6jMmjs)

Let's expand the generated group and the test case:

![Recorded group expanded](/files/-LuDYvJebgHFiRNTNLGF)

As you can see our interactions on the page recorded into a valid test case. Now let's switch to **Targets** panel:

![Recorded targets](/files/-LuDYyjpJgNJBOHICY7P)

As you can see automatically generated targets named after their selectors. Besides, we have an explicitly specified target (`AVATAR`)&#x20;

## Open Suite

To open suite click on **File / Open Suite** item in the main menu:

![Open suite in the main menu](/files/-LhexwQNM3mqosDT9O6_)

It opens the following modal window:

![Open Suite modal window](/files/-LheyX8DE7v9S8bAwjFG)

Here we select a suite from the list of suites currently available in the project

You can also use [Project Explorer](https://docs.puppetry.app/projects#project-explorer)

## Save Suite

To create a new project you can press **Ctrl+S** (⌘S)  or click on **File / Save Suite** item in the main menu:

![Save Suite in the main menu](/files/-LheytfIrcYRjh91LtuX)

## Save Suite As..

You can clone a suite. To do it click on **File / Save Suite** **As...** item in the main menu:

![](/files/-LhexthAeaVU30dOfCoe)

It opens the following modal window:

![Save Suite modal window](/files/-Lhez3GAPwbLlXG3tTCX)

Here you just need to provide a name for suite clone.

## Suite Options

In suite tabs you can find one called Suite o**ptions**:

![Suite options tab](/files/-LuDZ29SekGSqlihBU5L)

Click on it to get the **Suite Options** modal window:

![Suite Options modal window](/files/-LuDZ5ibORaHkMheMdnG)

Here you change the suite name and set up test run timeout (maximal allowed time for the entire test run)


# Group

During test run or export Puppetry suites are turned into [Jest](https://jestjs.io) test file. Groups are converted into [describe blocks](https://blog.teamtreehouse.com/an-introduction-to-rspec). So we use them to define the context for the included test cases.

> Group defines the page context for all its test cases, meaning the first test case in a group has a fresh new context. We run commands and assertions within that context, so it gets affected. Yet the test cases in the next group receive a new clean context.

![Test group](/files/-Lu8cNpkb3bEm72Rha8O)


# Target


# Simple Target

Targets are basically identifiers associated with locators (CSS selector or XPath) that we can refer in the tests

## Adding Target with CSS Selector

![Adding Target with CSS Selector](/files/-LuI8VAJpKSIlL5cbXrI)

{% hint style="info" %}
When you change name of an existing target, the new name will be reflected in all the test cases referring to it
{% endhint %}

## Adding Target with XPath

![Adding Target with XPath](/files/-LuI8jA3uVTcMGEDANPx)

## Editing Targets as CSV

You can edit targets as CSV. After clicking on **Edit as CSV** button you get a modal window where existing targets presented as lines of target/locator pairs separated with comma.

![ Editing Targets as CSV](/files/-LuI9m5utyTinb53k6LW)


# iFrame Target

Imagine we have an iframe (`#iframeFoo`) and need to query a sub-element within its scope. So we create a target for the iframe container:

![Defining iframe container target](/files/-LuI-t0DS86-J2hVVRNe)

Then  we create a target for the local element

![Defining local target](/files/-LuI-vfHZGcgQavR600e)

The first one is a simple target like we are used to have in Puppetry, but the second is chained one. While building it we expand Options, set **Parent target** to `IFRAME_FOO` and **Parent type** to `inline frame`.

![Chained target](/files/-LuI-zwgiE420RssQtvh)

As soon as the local element target defined (`IFRAME_FOO_LOCAL`) we can address it as any simple target.


# ShadowDOM Target

Imagine we have a web component `<foo-component id="fooShadowRoot">` and need to query a sub-element within its Shadow DOM. So we create a target for the foo-component:

![Defining shadow root target](/files/-Ltyu6zV9plxkktlJqvU)

Then  we create a target for the hosted element

![Defining hosted element target](/files/-LtyuBDltkgSOvUR6SEb)

The first one is a simple target like we are used to have in Puppetry, but the second is chained one. While building it we expand Options, set **Parent target** to `SHADOW_FOO` and **Parent type** to `shadow host`.

![Chained target](/files/-Ltyuth3vvu4X9qKb4xh)

As soon as the hosted element target defined (`SHADOWDOM_FOO_LOCAL`) we can address it as any simple target:

![Addressing a chained target](/files/-LtyuylKl8VVEZqJ2aEF)


# Chained Target

We are used to nested querying in JavaScript e.g. `document.querySelector("#foo").querySelector("#bar")`. Similarly you can chain targets in Puppetry.

Let's say we have a target `LEVEL_1`:

![Defining parent target](/files/-LuI0iRRsMTlJP78TeLI)

If we want to have a child target we use **Options** link

![Defining local target](/files/-LuI0kJLmPhNdsxMvNx5)

We set **Parent target** to `LEVEL_1` and **Parent type** to `generic element`.

![Chained target](/files/-LuI0mFGw7OPv7TyG41k)

As soon as the `LEVEL_2` target defined we can address it as any simple target.


# Shared Target

In **Project** menu you can find **Shared Targets**:

![Shared targets in Project menu](/files/-LuIAG8BuSyz_Y3hO8MP)

It's pretty much the same as suite targets, but these targets as soon as defined get available in all the suites of the project.

![Shared targets](/files/-LuIAMQp2hO0VCABgwyx)

> If any of suite targets has the name as a shared one, the priority will be given to the local one. E.g. you have FOO: #foo1 target in the suite and FOO: #foo2 shared target. When you run the suite for the target FOO it will use selector #foo1&#x20;


# Test Case

Test case is a specification of commands, assertions and references  to ensure that a targeted component of the test application acts as intended.

![Test case](/files/-Lu8cNpkb3bEm72Rha8O)


# Test Step / Action

Test steps make the test case. They can be commands, assertions and references. There are different collections of commands and assertions for page and for a target (DOM element).

![Test steps](/files/-LuCJgjeXM1lsmV7NDB4)

## Action Comment

If you want provide an action with a comment while editing it you click on **"comment"** link.

![Comment link](/files/-LuD0lDtbGAc1nhY09Og)

The comment form expands and you can type in your comment.&#x20;

![Comment form](/files/-LuD172exZ5bV4QWPV7W)

As you save the changes the comment appears in the test case table.

![Comment in the test case table](/files/-LuD19mp1JamHchlF_ME)

## "Wait for the target" Option

While editing any target action (except `target.assertVisible`) you can use "wait for the target" checkbox to specify if the system needs to wait for target to appear in the page DOM before performing the action

![Wait for target option](/files/-LuD1QZwd2SS1WCh3TIp)

As you save the changes with the option set ON the test case table you can see updated action description.

![Action description updates](/files/-LuD1TIxy1ZTGUkegiTJ)


# Page Commands

## page.emulate

Emulates given device metrics and user agent

![page.emulate](/files/-Lu8fFnw4GUu5cTsWWGu)

## page.setViewport

Defines browser viewport, where the viewport is the user's visible area of a web page

![page.setViewport ](/files/-Lu8fPgzEpu6CSDgo9wn)

![page.setViewport predefined resolution list](/files/-Lu8f_DZsmrk8QfgewFZ)

![page.setViewport advanced options](/files/-Lu8fjBZbmRRPoGh_6ZH)

## page.goto

Navigates to a given URL and waits until the page loaded

![page.goto](/files/-Lu8fs7Ybf29GhI2WqjO)

## page.screenshot

Makes a screenshot of the page

![page.screenshot](/files/-Lu8g8Jj8tOCq-ObBPcr)

![We can select targets to be highlighted on the screenshot](/files/-Lu8gE98NizazsQ9v6er)

![page.screenshot advanced options](/files/-Lu8gQC_UOCmAwMAiv1v)

## page.click

Emulates mouse click according to given options

![page.click](/files/-LuCC6wwEsIahnyZ45EW)

## page.moveMouse

Moves mouse to given position

![page.moveMouse](/files/-LuCCOoBPNhP_QkevffY)

## page.tap

Emulates tap according to given options

![page.tap](/files/-LuCCWfQ0swFDH8-lxZl)

## page.press

Emulates pressing on a key, optionally with modifiers such as ⇧, ⌥, alt, control, ⌘

![page.press](/files/-LuCCc-EY2Rmwlv2_ujA)

![page.press advanced options](/files/-LuCChdqq_YineGW4_h6)

## page.scroll

Scrolls the document in the window by the given amount

![page.scroll](/files/-LuCCnztEj6w04nNveJF)

## page.reload

Refreshes the page

![page.reload](/files/-LuCCuCcbtHBpjlIGvoc)

## page.setUserAgent

Sets custom user agent

![page.setUserAgent](/files/-LuCDAYIjOaMpcP5BxLF)

{% hint style="info" %}
While running test on < 3.0.1 this method requires adding "--no-sandbox --disable-setuid-sandbox" Chromium arguments in Browser options
{% endhint %}

## page.setCookie

Sets cookies on the page

![page.setCookie](/files/-LuCDGxoAtV7rYvTzCdv)

![page.setCookie advanced options](/files/-LuCDioSXax-hG7-WnnY)

## page.mockRequest

Intercepts to a given URL and replaces it according to provided data

{% hint style="info" %}
As soon as a matching request intercepted the session gets detached, meaning Puppetry stop listening for mocking. You have to set `page.mockRequest` before every request that you want to mock.
{% endhint %}

![page.mockRequest](/files/-LvtyJHoKFtqBJVjnPy5)

## page.waitFor

Waits for a given time before proceeding to the next command

![page.waitFor](/files/-LuCDd1lTcqk9fkmc6pe)

## page.waitForSelector

Waits for an element matching a provided [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)

![page.waitForSelector](/files/-LuCDs1fv7-zegrBi3DS)

## page.waitForNavigation

Waits until a given event before proceeding to the next command

![page.waitForNavigation](/files/-LuCE1J9Ym4DZzx86lL2)

## page.waitForResponse

Waits for HTTP(S) response

![page.waitForResponse](/files/-LuCER0JxgMiT3lbpaE3)

## page.waitForRequest

Waits for HTTP(S) request

![page.waitForRequest](/files/-LuCEYonVGMtm6B0U-8h)

## page.authenticate

Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).

![page.authenticate](/files/-M8Kor_IRzIPL3erZ5IG)

## page.evaluate

Evaluates JavaScript code in the page context

![page.evaluate](/files/-LuCEfY6r8qhpV_y2HC_)

## page.runjs

Runs custom JavaScript code in the test suite with use of [Puppeteer API](https://pptr.dev) and [Puppetry API](https://docs.puppetry.app/command-api). You can access [dynamic environment variables](https://docs.puppetry.app/template) via `ENV` map (e.g. `ENV[VAR_NAME]`)

![page.runjs](/files/-LuCEmdN5f8jOPzemzvG)

## page.debug

Stops execution of JavaScript, and calls (if available) the [debugging function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger). During test scenarios may happen many things - DOM changes, pages load. It makes hard to figure out why some test assertions fail in some particular moment. You can use this tool to set a breakpoint. Test flow will stop as soon as it reaches this point. So you will be able to examine the page state with DevTools.

![page.debug](/files/-LuCEraz5HGj7Bm8OpG6)

## page.closeDialog

Listen to dialog events and dismiss or accept dialogs (alert, beforeunload, confirm or prompt) as they are called

> The step must be defined before the expected dialog event

![page.closeDialog](/files/-LuCFFm_t_PoM_rdoQD1)

## page.assignVar

Assigns template variable dynamically

![page.assignVar](/files/-LuCFPpPXb2sl8iy1ePK)

## page.assignVarRemotely

Polls `URL` with a given intervals until a response satisfying `parserFn` function received or a specified timeout exceeded. It can be used, for example, to retrieve a value from an [email sent by the application under test](https://docs.puppetry.app/testing-emails)

![page.assignVarRemotely](/files/-LuCFUKgHJAQBs1BcZvQ)


# Page Assertions

## page.assertTitle

Asserts that the page title satisfies the given constraint

![page.assertTitle](/files/-LuCFtNdmvUDiYHFk0K_)

## page.assertUrl

Asserts that the page URL satisfies the given constraint

![page.assertUrl](/files/-LuCG4D2Kkh4HXQg3cB3)

## page.assertContent

Asserts that the page content (HTML) satisfies the given constraint

![page.assertContent](/files/-LuCGJVPz09_bWaxV_0w)

## page.assertNodeCount

Asserts that number of elements matching a specified selector satisfies the given constraint

![page.assertNodeCount](/files/-LuCGPvcuiOAwZA-KGaA)

## page.assertScroll

Asserts that [window scroll](https://developer.mozilla.org/en-US/docs/Web/API/Window/scroll) offset satisfies the given constraint

![page.assertScroll](/files/-LuCGWA_6Tx69qhsPqSJ)

## page.assertVar

Asserts that variable associated with a given name satisfies the given constraint

![page.assertVar](/files/-LuCGbs_2K67E-98Xg2p)

## page.assertScreenshot

Asserts that screenshot of the page matches already approved one

![page.assertScreenshot](/files/-LuCGt6z3UxgnFatvuLB)

![page.assertScreenshot advanced options](/files/-LuCGvrK-P5zqr83LyOj)

## page.assertRest

Makes HTTP/S request and asserts that the response satisfies the given constraint.

{% hint style="info" %}
The method automatically assigns the response text data to template variable `PUPPETRY_LAST_RESPONSE_TEXT`
{% endhint %}

![ page.assertRest](/files/-LvtymRgPCHMhNKYpowt)

## page.assertResponse

Asserts that the HTTP/S response satisfies the given constraint

![page.assertResponse](/files/-LvtyxdlScBTGR6_N61I)

## page.assertConsoleMessage

Asserts there were (no) console messages sent to console to satisfy the given constraint

![page.assertConsoleMessage](/files/-LuCU7aLM7uB1v8mc--f)

## page.assertDialog

Asserts there was called a dialog (alert, beforeunload, confirm or prompt).

![page.assertDialog](/files/-LuCUBG7j6IBYHuLlS2n)

## page.assertPerformanceAssetWeight

Asserts that total weight of assets (JavaScript, CSS, images, media, fonts, XHR) on the page satisfies the given budget.&#x20;

This assertion makes Puppetry to intercept HTTP requests. It compares the given limits to the encoded length of the requests. Note that the request length is considered `0` when request loaded from cache. So it makes sense to place this step next to the very first `page.goto` in the test suite.

![page.assertPerformanceAssetWeight](/files/-LuCHsENqomQr7BK4e0N)

## page.assertPerformanceAssetCount

Asserts that total number of requested assets (JavaScript, CSS, images, media, fonts, XHR) on the page satisfies the given budget.

![page.assertPerformanceAssetCount](/files/-LuCI9wLypFFWlj1OgZC)

## page.assertPerformanceTiming

Asserts a given [resource timing](https://w3c.github.io/perf-timing-primer/#resource-timing) under a provided budget

![page.assertPerformanceTiming](/files/-LuCIOaTiazTrGcMbedz)

## page.assertGaTracking

Asserts a given action was sent to Google Analytics with [analytics.js](https://developers.google.com/analytics/devguides/collection/analyticsjs) or [gtag.js](https://developers.google.com/analytics/devguides/collection/gtagjs). We also assert that sent payloads are valid according to the Google API

![page.assertGaTracking](/files/-LuCI_wv4zJat1WOhk8R)


# Target Commands

## target.type

Focuses the element, and then sends keyboard events for each character in the text

![target.type](/files/-LuCqULfkW6T4ZMiMq5B)

## target.select

Sets value on select element

![target.select](/files/-LuCqbQ_-THr617_wMfj)

## target.focus

Focuses the element

![ target.focus](/files/-LuCqgXi4qrtTsmtioSW)

## target.click

Emulates mouse click on the element

![target.click](/files/-LuCqlifvlvwSpFcTaNi)

## target.reset

Resets the input or form element

![target.reset](/files/-LuCqpm_wHAgF5sCzLST)

## target.upload

Sets the value of a file input. Note that the target element must be INPUT of FILE type. It is also recommended to emulate click on the target (or clickable element calling the file browser) before going with this command

![target.upload](/files/-LuCqw0Ig7Aa9oajt8V7)

{% hint style="info" %}
Since ver. 3.2.6 file path supports template expressions. So you can use e.g. environment variables to build the path.
{% endhint %}

Alternatively you can specify with in advanced options file size and name and it will be generated and attached to the file input element.

![target.upload advanced options](/files/-LvBO_5_1vGW1IRz6SfZ)

## target.tap

Taps the element

![target.tap](/files/-LuD2L_2vYmVCRfPb9VP)

## target.scroll

Sets the number of pixels that an element's content is scrolled horizontally/vertically.

![target.scroll](/files/-LuD2TE4SvvTOch1uxuY)

## target.scrollIntoView

Scrolls element into view.

![target.scrollIntoView](/files/-LuD2jcQRivUwkzDWBvk)

## target.hover

Scrolls element into view if needed, and then hovers over the center of the element

![target.hover](/files/-LuD2pgaVIJOkKTc5jZp)

## target.checkBox

Toggles checkbox/radio state

![target.checkBox](/files/-LuD2unq2BuZH9gFKe9L)

## target.toggleClass

Toggles the specified class value (adds or removes)

![target.toggleClass](/files/-LuD3-bgUJdxCmvVglLc)

## target.setAttribute

Sets the value of an [attribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) on a target

![target.setAttribute](/files/-LuD36TT02r758jt5Xo7)

## target.screenshot

Takes a screenshot of the target element.

![target.screenshot](/files/-LuD3HgWR9nSPH5pLm0S)

![Selecting targets to highlight on the screenshot](/files/-LuD3O5Ile6qjzZjHlnP)

## target.waitForTarget

Waits for an element matching a provided target to appear in page.

![target.waitForTarget](/files/-LuD3h4PLgZoLMFv9zr_)


# Target Assertions

## target.assertAttribute

Asserts that the specified [attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes) of a target satisfies the given constraint

![target.assertAttribute](/files/-LuD3sQVc6RXV85HJhNh)

![target.assertAttribute available assertions](/files/-LuD3wMETXFXht26G83Q)

## target.assertProperty

Asserts that the specified [property](https://developer.mozilla.org/en-US/docs/Web/API/Element) of a target satisfies the given constraint

![target.assertProperty](/files/-LuD43jNRCaqFdVJx4w_)

![target.assertProperty available assertions](/files/-LuD472Yi8jJ5ooFQ_Gs)

## target.assertVisible

Asserts that the element is currently available and visible. Available settings:

* **available on the page and observable** - the target is available in the DOM and visible to the user (displayed, visible, opaque, within the viewport)
* **available on the page** - the target is available in the DOM. It enables the following options&#x20;
  * display: any/none/NOT none
  * visibility: any/hidden/NOT hidden
  * opacity: any/0/NOT 0
  * offset: any/within the viewport/out of the viewport
* **available, but NOT observable** - the target is available in the DOM, not not visible to the user (display: none or visibility: hidden or opacity: 0 or out of the viewport)
* **NOT available on the page** - the target is not available in the DOM&#x20;

![target.assertVisible](/files/-LuD7-91GRMvpK0IJIeX)

## target.assertHtml

Asserts that the HTML content of the focused element satisfies the given constraint

![target.assertHtml](/files/-LuD781zQtOpU6gs__5u)

## target.assertText

Asserts that the rendered text content of the focused element satisfies the given constraint. Unlike `target.assertHtml` this method takes pure text content without HTML, which approximates the text the user would get if they highlighted the contents of the element with the cursor and then copied it to the clipboard.

![target.assertText](/files/-M8KpFCH9C_DpsWH_9Og)

## target.assertBoundingBox

Asserts that the [bounding box](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) (size and position) of a target satisfies the given constraint

![target.assertBoundingBox](/files/-LuD7DljaGvDsAsaoRh6)

## target.assertPosition

Asserts that target's position relative to other given target

![target.assertPosition](/files/-LuD7KXg52439ANOiN4-)

## target.assertStyle

Asserts that the [computed style](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) of a target matches the given value

![target.assertStyle](/files/-LuD7QD30tSrOJK-8ZdD)

## target.assertMatchesSelector

Asserts that a target matches a given selector or pseudo-selector

![target.assertMatchesSelector](/files/-LuD7UkR2GPLUJtgAs1J)

## target.assertNodeCount

Asserts that number of child elements matching a specified selector satisfies the given constraint.

![target.assertNodeCount](/files/-LuD7_KIQ2kHKGeYj_3c)

## target.assertTextCount

Asserts that number of child elements containing a specified text satisfies the given constraint.

![target.assertTextCount](/files/-LuD7dKNJUa4v8j4QXP2)

## target.assertContainsClass

Asserts that the specified class value exists in the [element's class attribute](https://developer.mozilla.org/en-US/docs/Web/API/Element).

![target.assertContainsClass](/files/-LuD7hFz_RfDzQ2I3XLL)

## target.assertScroll

Asserts that [scroll](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop) offset on the target satisfies the given constraint

![target.assertScroll](/files/-LuD7mts66yQtoU1sCYX)

## target.assertScreenshot

Asserts that screenshot of the target matches already approved one

![target.assertScreenshot](/files/-M0Wn1TP-bn88EXA-mNF)


# Managing Records

Puppetry takes advantage of data-tables to manage targets, groups, test cases, test steps, variables, snippets. All that test data are basically records of the data-tables and have the same UI:

## Adding Records

To add a new record you need to fill in the empty field(s) at the end of the table and press **Add** button next to it:

![Adding records](/files/-LhtRwmdGgTxr7jKq0tH)

## Inserting Records

Alternatively you can add an record to a particular position. Just hover the target record and right-click for the context menu. Select **Insert** item.

![Inserting records](/files/-LhtS4-m-JNT6uCUtOK-)

## Editing Records

To edit an record, click on **Edit** link next to the target record, provide new value and click **Save** button.

![Editing records](/files/-LhtS8_DnsLorKLrTutv)

## Cloning Records

If you need a sequence of similar records, you don't need to create each one from the scratch. Instead you clone one and adjust parameters of the clones:

![Cloning records](/files/-LhtSCtCqDGxgn7DFtqS)

## Removing Records

To remove an record, click on **Remove** link next to the target record. Choose **OK** for confirmation.

![Removing records](/files/-LhtSG5vJDV5xKYIHX6S)

> You can select multiple rows to the action with Shift-Click

![Bulk removing](/files/-LuDWMYmFKBVaQDj-R0M)

## Disabling/Enabling Records

If you don't want to remove an record, but temporarily exclude it for test run, hover the target record and right-click for the context menu. Choose **Disable** item:

![Disabling records](/files/-LhtSItLUcowyqulzXip)

> You can select multiple rows to the action with Shift-Click

![Bulk disabling](/files/-LuDVydZ8Vv1zdil9fQZ)

## Drag & Drop

You can use drag & drop  to move records:

![Moving records](/files/-LhtSNcF8Ke2IWagtQ_c)

## Copy/Paste

You can copy/paste any of records:

![Copy.paste records](/files/-LhtSS-Hah7MnnPmOglz)

> You can select multiple rows to the action with Shift-Click

![Bulk copy/paste](/files/-LuDWD9NSzYZUFEzTxrU)

Besides, you can copy an record in one suite/project and paste it in another. In fact, you can even copy an record, save it in a text file and paste it later when you need it:

![Cross-application copy/paste](/files/-LhtSVRYq2XyRnntUbTn)


# Snippets

Snippets are reusable test cases in scope of project. So you can create a snippet and refer to it in your test suites. For example, to login a user we need to perform multiple actions: visit login form, fill it out and submit. Instead of adding these test steps to every test case for logged in user we can simply create once a snippet and then refer to it from many test cases. Besides, to every reference we can assign  a set of template variables that can be addressed in the body of the snippet. In other words, we can execute the same snippet, but giving it a different input every time.

## Using Snippets by Example

In order to illustrate the concept we are going to create a simple test case for an [imaginary ACME forum app](https://docs.puppetry.app/examples). Let's say we have a test case "user registers and activates the account" where Puppetry visits forum register page, fills out the registration form and submits it. So we have a bran new user in the system and can login to run test flows for an authorized user. However to get the initial state in the beginning of every test group we need to repeat and repeat the login flow (visit login page, fill out the form, submit). Why not to make a snippet and reuse it?

### Creating Login Snippet

Next we press **Project / Snippets** in the main menu

![Snippets in the Project menu](/files/-LuHn4XmVpMs6f95CX7K)

Snippets are quite similar to suites. They have local targets and test cases.

We start by defining targets for the [login form](https://docs.puppetry.app/examples)&#x20;

![Snippet's targets](/files/-LuHnGl-Pp0tiQgDhSeS)

Now we just into **Snippets** tab and create the test case:

![Snippet's body](/files/-LuHnM4CUXmY7YYoYXiX)

As we are done with the snippet we can navigate back to the suite:

![](/files/-LhKVU37KbBdLz2SP7SG)

We create an test case "user gets logged in" and click under it to the **Add a reference** button

![](/files/-LhKVVvC2SI2kU6Pa939)

In the following modal window we select our recently created snippet:

![Adding a reference into test case body](/files/-LuHnUvOhLyjZEEXcvts)

After extending the test case with other test steps it looks like that:

![Test case body after adding a reference](/files/-LuHnaa1vpq9zV2NYUzd)

During the test run first all the test steps of the snippet are executed, so user gets logged in. We wait until the page is ready (login form doesn't contain .categories selector, but landing page does). Now we can assert that page header changed (has #user-header-name).&#x20;

## Running Snippets with Diverse Input

In the example above we used `TEST_EMAIL` template variable defined during registration flow. But imagine that we have a number of already registered accounts  (let's say one is inactive, one is active, one is privileged) and we want sequencely log in with each one and assert that the application responds as intended. What we can do  is reusing **"user logs in"** snippet, but with diverse emails. Click on the **Edit** action next to the reference test step:

![](/files/-LhKW2lYViomIr_L-PRF)

You get a modal window that we know from the example below. Click to **Local Template Variables** to expand the template editing interface and add `TEST_EMAIL` variable:

![Providing reference with input data ](/files/-LuHnkBF6CK7sY3WTvTQ)

Now you clone the reference and edit it again. Remove TEST\_EMAIL and add it again with a new value. As you run the tests the references will execute the snippet with the values we assigned.


# Running tests

When we are ready with our test suite we can run the tests. Click on **Run..** item in the main menu (or press **F6**).

![Run tests menu item](/files/-Lh_4MS0_BVJ9fXnADzm)

You get **Run Tests** modal window

![](/files/-LuDraoEl3qke3IkM3Qg)

Learn more about **"interactive mode"** option in [Interactive mode](https://docs.puppetry.app/v/3.0.0/running-tests/interactive-mode)

![](/files/-LuDwc67kw2XN2115WxP)

Learn more about **"update comparison images"** option in [CSS Regression Testing](https://docs.puppetry.app/v/3.0.0/testing-techniques/css-regression-testing)

![](/files/-LuDwVqpK4qT_p-dOSEy)

### Selecting Staging Environment

The modal window allows you to select a [target environment](https://docs.puppetry.app/template):

![Choosing staging environment](/files/-LuDtAiJ9peEj-JZs8Hy)

###

### Browser Options

We can use **Browser options** tab to adjust the runner

![Browser options tab](/files/-M0WkKBHdFuPtWbQpT6S)

On this panel we can select a browser to run the tests:<br>

![Picking up a browser](/files/-M0WfU0JBXVKfdmH6LzW)

Available options are:

[**Headless Chromium**](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) allows running [Chromium](https://en.wikipedia.org/wiki/Chromium_\(web_browser\)) in a headless/server environment. In this mode the test will run considerably faster.

[**Chromium**](https://en.wikipedia.org/wiki/Chromium_\(web_browser\)) is an open-source web browser, which is used as basis for Google Chrome browser. Puppetry downloads and uses a specific version of Chromium so its API is guaranteed to work out of the box.

[Google Chrome](https://www.google.com/chrome/) is a cross-platform web browser developed by Google.

[**Mozilla Firefox**](https://www.mozilla.org/en-US/firefox/) is a free and open-source web browser developed by the Mozilla Foundation.

**Connect to Chrome** - connecting to a running instance of Chrome

Browser-specific options:

* **DevTools** - enables Chrome DevTools in the browser
* **incognito window** - runs the tests in [private session](https://support.google.com/chrome/answer/7440301)
* **maximized** - maximizes the browser window
* **fullscreen** - switches the browser in fullscreen mode
* **ignore HTTPs errors** - tolerates HTTPs errors like invalid certificate

Besides you can manually provide any [Chromium command line options](https://peter.sh/experiments/chromium-command-line-switches/) in the textbox below.

You can also specify location of Chrome extension. Learn more [here](https://docs.puppetry.app/v/3.0.0/testing-techniques/testing-chrome-extensions)

### Connecting to Chrome

There are some cases when we need to connect to a ruining instance of chrome instead of starting a new one. For example to bypass reCaptcha we can solve it manually in Chrome and then run the tests on it.

In order to connect we need to start Chrome in command-line with remote-debugging-port parameter.

{% tabs %}
{% tab title="Windows" %}

```
start chrome.exe –remote-debugging-port=9222 --user-data-dir=remote-profile
```

{% endtab %}

{% tab title="macOS" %}

```
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --no-first-run --no-default-browser-check --user-data-dir=$(mktemp -d -t 'chrome-remote_data_dir')
```

{% endtab %}

{% tab title="Linix" %}

```bash
google-chrome --remote-debugging-port=9222
```

{% endtab %}
{% endtabs %}

Next we navigate in the started browser to `http://127.0.0.1:9222/json/version`. On the page we can see a JSON object.&#x20;

![Obtaining webSocketDebuggerUrl](/files/-M0Wj7nc3h0DTS8ONH0R)

We shell copy the value of `webSocketDebuggerUrl` property. Open **Run Tests** modal window (F6), switch to **Browser options** tab and paste the saved value into **WS Endpoint** input.<br>

![Connect to chrome option](/files/-M0WkAIRZPuca9Q70hiE)

Now we can press **Run**.

### Running tests

As we press Run button the tests are sent to Jest and we have to wait for [Test Report](https://docs.puppetry.app/v/3.0.0/test-reports)

![Tests are running](/files/-LuDrr1-Fk6HOv3AWxlh)

&#x20;


# Interactive Mode

When "**interactive mode**" option set on in the "**Run Tests**" modal window Puppetry runs the tests in the browser. It renders a panel with the suite tree on top of the pages. The tests pause on the first meaningful test step waiting until you click on the **Next** button in the panel. As you click the next step performs and the cursor in the panel moves respectively. This way you can navigate the test cases and check on-the-fly what actually happens with every test step. In order to make it even more obvious Puppetry highlights the targets involved in the action.&#x20;

> You can open DevTools in browser by pressing ⌘⌥I (Mac) or F12/Control+Shift+I (Window/Linux)

![Puppetry interactive mode](/files/-LuHVdpvZ1yDIc9QE9s7)


# Troubleshooting

On Linux you may run into "Failed to launch chrome" issue

![](/files/-LirOwb-tCrJE9xxksL8)

In order to protect the host environment from untrusted web content, Chrome uses [multiple layers of sandboxing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux_sandboxing.md). For this to work properly, the host should be configured first. If there's no good sandbox for Chrome to use we get this problem. You can find how to configure sandbox [here](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox). But actually if you trust the content of the application under test you can simply run with `--no-sandbox` option.

![](/files/-LirQ8PmcSoEEoOV6JBT)


# Test Report

As you run tests (**F6**) Puppetry generates and performs Jest/Puppeteer project. Puppetry receives the report from Jest and display the report.

![Test Report in Puppetry](/files/-LuDbcBVE_q0oQxEXpmA)

When the test cases of the tested bundle include page/target.screenshot method the report adds thumbnails of the generated screenshots. If you click on a thumbnail the screenshots gets shown in the viewer.

![Screenshot viewer](/files/-LuDdliR0UPPynZehFJm)

Note the icons in the top right corner.  You can use them to switch viewer in fullscreen mode and to close the viewer.<br>

Alternatively you can open a screenshot with default image viewer in your hosting OS. Just hiver the thumbnail and click on **Download** link.

![Download screenshot link](/files/-LuDdelSoFJGyh7B8Obh)

When the test cases of the bundle include performance assertions the report has button "Download performance report", which opens with external text viewer the network activity report.

![Download performance report button](/files/-LuDbgRlU65Fr5a1KwQT)

If any test cases of the bundle have failing assertions the report explains the problem:

![Test report with failing assertion](/files/-Lu7ZjM-f4Tuy9DjheH4)

Clicking on Error details link expands the full report from Jest.

![Error details link](/files/-LuDbpzhQ3dq8rPj75IE)


# Export


# Exporting as Jest Project (CI-friendly)

Project suites can be exported as [Jest test project](https://jestjs.io/) and be ran in command line by, for example, Continuous Integration server.&#x20;

Press **Ctrl+Shift-E** (⌘⇧E) or click on **File/Export Project as...** menu item:

![Export Project as...](/files/-LuIFVt4ThCtvMvhwGpC)

**Export Project** modal window shows up.&#x20;

![Export project modal](/files/-LuIGM_-KS-FcLyMeW9j)

It is quite  similar to [Run Tests ](https://docs.puppetry.app/v/3.0.0/running-tests)window. Similarly we can choose a target environment (see also [Template variables](https://docs.puppetry.app/v/3.0.0/template)) and set up **Browser options**. What it's new we have to specify the destination folder for the exported project.

After exporting we can jump to the given export directory and install npm dependencies:

```bash
npm install
```

then we run the tests:

```bash
npm test
```

We are expected to get output like that:

![](/files/-M0XPaWnYSraNeli9VH7)

## View Test Reports with Allure

If you have [Allure](http://allure.qatools.ru) installed on your system you can also run&#x20;

```bash
npm run report
```

It will load the generated test report in Allure browser

![Allure test report](/files/-LuIBo-boyhWGyvUt8ek)

## Configuring Puppeteer

You can change the configuration set while exporting in `puppeteer.config.json`

* `incognito` - when true the test will run in incognito window
* `puppeteer.launch` - options to pass to [puppeteer.launch](https://pptr.dev/#?product=Puppeteer\&version=v2.1.0\&show=api-puppeteerlaunchoptions)
* `puppeteer.connect` - options to pass to [puppeteer.connect](https://pptr.dev/#?product=Puppeteer\&version=v2.1.0\&show=api-puppeteerconnectoptions)

#### Example:

```
{
  "incognito": true,
  "puppeteer.connect": {
    "browserWSEndpoint": null,
    "ignoreHTTPSErrors": true
  },
  "puppeteer.launch": {
    "product": "chrome",
    "headless": false,
    "devtools": false,
    "ignoreHTTPSErrors": true,
    "args": [
      "--start-maximized",
      "--ignore-certificate-errors"
    ],
    "executablePath": "/usr/bin/google-chrome"
  }
}
```


# Exporting as Test Specification

You can export test suite(s) in a human-readable report, e.g. to share it with colleges, who have no experience with Puppetry.

Press **Ctrl+Shift-E** (⌘⇧E) or click on **File/Export Project as...** menu item:

![Export Project as...](/files/-LuIFVt4ThCtvMvhwGpC)

**Export Project** modal window shows up.&#x20;

![Export project modal](/files/-LuIGM_-KS-FcLyMeW9j)

Select format "**test specification**"

![Export as test specification](/files/-LuIGaLny0U0KfwU6lXr)

Now you just need to select a destination folder for the report and click **Export** button. You will have a text report like this:

```
Demo Project
environment: test

template variables:
   SANDBOX_BASEURL = https://puppetry.app/demo/

TodoMVC
   filename: todomvc.json
   timeout: 50000
   
   targets:
      FIRSTNAME_INPUT = #fname
      LASTNAME_INPUT = #lname
      MESSAGE_INPUT = #comment
      CONSENT_CHECKBOX = #consent
      OPTION_RADIO_1 = #option1
      ATTACHMENT_FILE = #attachment
      FORM = #form
      SUBMIT_BTN = #submit
      HEADER =  .todoapp .header
      NEWTODO_INPUT =  .todoapp .header input.new-todo
      MAIN = .todoapp > div > .main
      ....

   1. describe: Functionality

      1.1. test: No todos
         1.1.1. Set browser viewport as "1920x1080", "x1"
         1.1.2. Visit "http://todomvc.com/examples/react/#/"
         1.1.3. Wait until target "HEADER" appears on the page with timeout "30000ms"
            ▷ Give React.js time to build the app
         1.1.4. Assert that target "HEADER" is "available" and "visible"
         1.1.5. Assert that target "MAIN" is "NOT available"
         1.1.6. Assert that target "FOOTER" is "NOT available"
```

## Report Screenshots

You can extend the report with the automatically generated screenshots per every test step. Just check on the "run tests and generate step screenshots" option in the "**Export project**" moda window:

![Tick it on to get generated step screenshots](/files/-LuIGe-B3vzMhWlxImEL)

When exporting is done, you are going to find in the given destination folder screenshots named after the test steps indices:&#x20;

![Test step screenshots in file explorer](/files/-LuIGhsQ16USgLScXZGF)

\
If you open a screenshot corresponding a target command or assertion you will see the acting targert highlighted:

![Action target is highlighted](/files/-LuIGm_y1p7OJFem154z)


# Settings

You can find **Settings** panel in the main menu.

![Settings](/files/-LuIamoZdcfBB1vpTZcA)

## Autosave

Autosave is enabled by default, but if you want to save changes manually you can tick off  "**save changes automatically**" option

## Test Case Style

You can also set look\&feel for test case body by choosing between two styles **Gherkin** and **Declarative**:

![Gherkin  style](/files/-LuIbgM_YJ7rB2bZ-i2N)

![Declarative style](/files/-LuIbkLx_LIcxJNkSpuF)


# Template Expressions

Many of command/assertion parameters accept templates. So you can use template variables and expressions. For example, `page.goto("https://github.com/{{ USER }}/puppetry/")` or `TARGET.type("name{{ counter() }}")`

## Template variables

By using **Settings/Template Variables** panel we can define a set of variables per environment. For an instance, we can declare a separate target app URL for every environment (test.acme.com, stage.acme.com, [www.acme.com](http://www.acme.com)). Before running/exporting test project we specify the desired environment and the corresponding template tags will be replaced with the value (URL) given for that environment.

### Managing Variables

To edit template variables click on **Project/Template variables** in the main menu:

![Template variables in the Project menu](/files/-LuIcC3Jv-ERr6Qi8nGZ)

Under the table Template variables you will find a manageable table with variables (by default it's empty):

![Template variables panel](/files/-LuIdHUkW1TsYFmUsRaH)

You can use this UI the same way to do with targets, suits, and test cases. The difference is that the displayed variables belong to the selected environment:

![Selecting destination environment](/files/-LhKRoSTVQCnEoU-Pv-y)

As you add a new variable let's say to **test** environment it appears in all available environments. But when you update it's value, the value stays unique per environment.

Also note the **edit** action link next to the environment selector. When you click it you get a modal window where you can manage the list of environments:

![Editing environments](/files/-LuIdWJ0i9h6hzKkP17K)

After we assigned the variables and running the tests (or exporting them for CI) in the modal window we can choose what environment we target:

![Destinating test run for a given environment](/files/-LuIe8OA-vp_8rMzfdJT)

### Template Syntax

#### Syntax

```
{{ VARIABLE_NAME }}
```

The tag makes Puppetry injecting the variable value assigned for the selected environment into the container string

#### Examples

```
{{ BASE_URL }}/api/{{ API_VERSION }}/method
```

{% hint style="info" %}
Template variables can be also assigned dynamically. E.g. you can use `page.assignVar` to define a variable by using expressions or `page.assignVarRemotely` to define a variable by using REST API ([learn more](https://docs.puppetry.app/testing-emails)).
{% endhint %}

## Template Expressions

Template expressions are basically functions that we may want to use for building template string.

Test step parameters supporting templates provided with a helper widget, which you can you to autofill the field:

![Template helper widget](/files/-LhKU2k4JJRqBMrO4ovS)

### Environment Variables

#### Syntax

```
{{ env( NAME ) }}
```

Function returns system [environment variable](https://en.wikipedia.org/wiki/Environment_variable) by a given name

#### Examples

```
{{ env( "SECRET" ) }}
```

### Counter

#### Syntax

```
{{ counter() }}
```

Function increments every time it called, but is unique per command/assertion&#x20;

#### Examples

```
joe.{{ counter() }}@acme.com
```

This template resolves in email addresses <joe.1@acme.com>, <joe.2@acme.com> and so on

### Uniqid

#### Syntax

```
{{ uniqid() }}
```

Function generates a unique id like `8fzk2g5fpkb`

#### Examples

```
{{ uniqid() }}
```

### Iterate

#### Syntax

```
{{ iterate( LIST ) }}
```

Function iterates through a given list. It starts from the beginning as it reaches the end

#### Examples

```
{{ iterate(["sansas@got.com", "yarag@got.com", "gendryb@got.com"]) }}
```

This template resolves in <sansas@got.com>, <yarag@got.com>, <gendryb@got.com>, <sansas@got.com>, ...

### Random

#### Syntax

```
{{ random( LIST ) }}
```

Function picks a random value out of a given list.

#### Examples

```
{{ random(["sansas@got.com", "yarag@got.com", "gendryb@got.com"]) }}
```

### HtmlOf

#### Syntax

```
{{ htmlOf( TARGET ) }}
```

Function extracts content (innerHTML) of a given TARGET.

#### Examples

```
{{ htmlOf("FOO") }}
```

### AttributeOf

#### Syntax

```
{{ attributeOf( TARGET, ATTRIBUTE_NAME ) }}
```

Function retrieve attribute value of a given TARGET.

#### Examples

```
{{ attributeOf("FOO", "href") }}
```

### PropertyOf

#### Syntax

```
{{ propertyOf( TARGET, PROPERTY_NAME ) }}
```

Function retrieve property value of a given TARGET.

#### Examples

```
{{ propertyOf("FOO", "checked") }}
```

### Faker

#### Syntax

```
{{ fake( FAKER_METHOD, LOCALE ) }}
```

Function generates dummy data, by using [Faker.js methods](https://github.com/marak/Faker.js)

#### Examples

```
 {{ faker("address.streetSuffix", "en_GB") }}
```

### Eval

#### Syntax

```
{{ eval( "JavaScript code" ) }}
```

Function injects the given JavaScript into the suite code forming the string.

#### Examples

```
{{ eval("process.cwd()") }}
```

resolves into

```
`${ process.cwd() }`
```

NPM modules "fs", "path" and "os" as well as Command API (<https://docs.puppetry.app/command-api>) are available for evaluation.&#x20;

Quotation marks in the JavaScript code have to be escaped.


# Testing Techniques


# Testing Dynamic Content

\
Usually the pages under test have dynamic content. For example we are testing news page and assert that first news title is “Puppetry 3 is out”. But as soon as anybody adds a new story our tests are to fail. What we can do is to use `page.assignVar` method to extract actual content (e.g. news title) from the page and make assertions on it. Consider the following example:

News page has search filtering by tags. If one click on a tag the news listing gets filtered for the stories associated with the tag.  So we create a variable `VAR_ARTICLE1_TITLE`, which takes-in HTML of `ARTICLE1_TITLE` target that corresponds to the element representing title of the first story.

![Extracting actual news title from the page](/files/-Ltdm1H2OIss83CcD2Qt)

We send click on a tag, waiting until the request for listing update complete and assert that the first story title (`ARTICLE1_TITLE`) changed, meaning the news list updated.

![Testing dynamic content](/files/-Ltdm5TCleu9TEmiAZrf)

\
Similarly you can make your test case waiting until a particular image fully loaded. So you extract image URL from `src` attribute and wait for response including that URL.

![Waiting for image to load](/files/-LtdpsrxUGkv74peN_jt)

> If the endpoint has redirections, the response will have URL different from one set with src attribute and therefore the approach won’t work.&#x20;


# Exhaustive Testing

> Exhaustive testing is a testing or quality assurance approach in which all possible combinations of scenarios and use/test cases are used for testing.
>
> In testing software, it is all the possible combinations of every type of input as well as every permutation and variation of how the input is carried out to ensure that everything works as expected.&#x20;
>
> [Techopedia](https://www.techopedia.com/definition/19234/exhaustive-testing)&#x20;

Demo project distributed with Puppetry contains sandbox-exhaustive.json suite, which is a show-case of exhaustive testing. It points at the form on <https://puppetry.app/demo/> page. Our goal is to seed the form with various data and check if it responds as intended on submission.  Here we want to test if special (non-latin and emoji) character in First Name field are tolerated. In real-life application form data are sent to the server. The server stores them in the DB. We need to ensure the server sanitizes the payload and DB is set properly (e.g. MySQL default collation is utf8mb4\_general\_ci and doesn’t support extended 4-bytes characters such as Emoji).

On this example we use a snippet "Fill out the form". It types in the First Name field a value of `FIRST_NAME` template variable, which will be passed to the snippet from test case. The snippet also seeds other form fields, but our focus here is on First Name field.

![A snippet to fill out the form](/files/-LtyHvMhequUEIPgtuDl)

In this demo when user submits the form it show an Alert dialog with a message containing either OK or FAIL. So in the test case we start with `page.closeDialog` method to dismiss dialogs as they pop up. Then we call the snippet for every combination of input values and assert the dialog message is OK

![A test case of exhaustive testing](/files/-LtyMRztNY5lXvW5_zuo)

When adding snippet references we set `FIRST_NAME` variable a value (latin, non-latin and emoji)

![Passing data into snippet](/files/-LtyMVQp5-JW0jAOBOG-)

As we run the test it seeds the First Name field with test data and checks the form feedback after every submit. So we penetrate the application under test to ensure both client and server side tolerate any kind of input.


# Performance Testing

If you don’t want users leaving the app you have to think of performance. In fact nowadays if you site is not fast enough you get even “honored” with [the badge of shame](https://www.theverge.com/2019/11/11/20959865/google-chrome-slow-sites-badge-system-chrome-dev-summit-2019) from Google. Thus [performance budget](https://addyosmani.com/blog/performance-budgets/) is one of the aspects we address with end-to-end tests and include in CI/CD pipeline to ensure new releases do comply our requirements.&#x20;

With Puppetry we can assert that:&#x20;

* Page loading times comply given restrictions&#x20;
* Total weight of assets (JavaScript, CSS, images, media, fonts, XHR) requested on the page satisfies given budget.&#x20;
* Total number of requested of assets satisfies given budget&#x20;

## Milestone timings&#x20;

With `page.assertPerformanceTiming` method we can establish budget for loading times. Namely:

* **Page loading**: the whole process of navigation and page load
* **Redirection**: the time taken by document request redirections
* **Network latency**: the time taken to fetch app cache, lookup domain, establish TCP connection, send request, receive response
* **Page processing**: the time taken for page load once the page is received from the server

![Asserting page loading times](/files/-Lu2OjX-wJy7cnHleqi4)

In case of assertion failure we get a report like this:

![Page loading budget report](/files/-Lu2OeylKoxBFHyAl4IF)

## Quantity-based metrics

With `page.assertPerformanceAssetWeight` method we can assert the total weight of assets (JavaScript, CSS, images, media, fonts, XHR) requested on the page

![Asserting total weight of requested assets ](/files/-Lu2L1z9apr2OOpIW1X4)

In case of assertion failure we get a report like this:

![Assertion failed](/files/-Lu2PJcfJB3lCovdo2OM)

By clicking on **Download performance report**

![Download performance report button](/files/-Lu2PNYSjZQ1T-YtkTc0)

we can get the network activity details

![Performance report as a text file](/files/-Lu2PQvFQ7BXnWLSyPDq)

With `page.assertPerformanceAssetCount` method we can assert the total number of assets (JavaScript, CSS, images, media, fonts, XHR) requested on the page

![Asserting total number of requested assets ](/files/-Lu2L5UAR59D_NC6yeuf)


# Visual Regression Testing

CSS like any other source code needs refactoring. We work to improve code readability and reusability, to make CSS faster to execute. We try alternative solutions (like switching from flexbox to grid). As we are done with the code changes we need to ensure the app’s “look & feel” didn’t alter. It can turn out quite a challenge if do it manually – the app may consist of dozens of pages that need to be tested for every defined viewport breakpoint. Here you can take advantage of Puppetry. All what you need to do is to create tests visiting app pages for given breakpoint viewports and call `page.assertScreenshot` or `TARGET.assertScreenshot` method.&#x20;

![CSS regression test case](/files/-LtzD8VFzQ6YpFZ7v-MG)

Just before starting refactoring we run the tests with "update comparison images" flag on.

![Update comparison images checkbox is on](/files/-LtzDYmYVRxgM2cANqfY)

Since we have no assertions the report is "green". However during the run Puppetry creates screenshots of every requested page (or target) and keeps them as original for further comparison.&#x20;

![Test report](/files/-LtzDHko8QXC_hiAhANV)

Now we can do the refactoring. In order to see how it really works we are going to consider a case were refactoring breaks the existing styles.&#x20;

Well, let's say we has the following styles applied to the heading in banner section:

```css
body {
  color: white;
}
.banner h1 {
  font-size: 48px;
}
```

During refactoring we decided that general rule for typography would be:

```css
h1 {
  color: black;
}
```

Thus we didn't think that it would also apply for banner heading, which inherits currently color from body.

So we run the test and this time with  "update comparison images" set off.

![Update comparison images checkbox is off](/files/-LtzDMil3lw1m5o2We7L)

The test report informs about a failure. It also provides the expected (original) screenshot and the actual and tries to build a diff image.&#x20;

![CSS regression test report](/files/-Lu26CBPT7lZbWBHhFTu)

![Diff image](/files/-Lu26FuLOxO3vUX2n2be)


# Testing Shadow DOM

> Shadow DOM is a new DOM feature that helps you build components. You can think of shadow DOM as a scoped subtree inside your element.\
> [polymer-project.org](https://polymer-library.polymer-project.org/2.0/docs/devguide/shadow-dom)

Shadow DOM is a technology that unlocks such features as isolated DOM and scoped CSS. It's usually used with custom elements to isolate their DOM from the global one. Puppetry targets are relative to a DOM scope (global or local). In order to query elements inside custom elements we have go with chained targets. So we are to create a target for the host element (the custom element root) and targets for the hosted sub-elements that point to the host target.&#x20;

It's much easier to explain with an example. Imagine we have a custom element foo-component, built out of a template:

```markup
<foo-component id="fooShadowRoot"></foo-component>
<template id="fooTemplate">
  <div id="shadowLocalTarget">
    Local Target in Shadow DOM
  </div>  
</template>
```

We can register the component with following code:

```javascript
document.addEventListener( "DOMContentLoaded", () => {

    function registerCustomElement( elName, templateId ) {
      globalThis.customElements.define( elName, class extends HTMLElement {
      constructor() {
          super();
          this.attachShadow({ mode: "open" });
          this.shadowRoot.appendChild( document.getElementById( templateId ).content );
        }
      });
    }

    registerCustomElement( "foo-component", "fooTemplate" );
});
```

Now if we inspect the page DOM with DevTools we can find the following sub-tree:

![](/files/-LtysXayBOAcNZK1HSNU)

Let's say our goal is to access a local target (#shadowLocalTarget), hosted by foo-component element (fooShadowRoot).&#x20;

> With DevTools you can inspect the target element and get with Copy/JS Path code like the that:\
> document.querySelector("#fooShadowRoot").shadowRoot.querySelector("#shadowLocalTarget")\
> If we translate it to Puppetry it actually gives us two targets:\
> \- shadow root:  #fooShadowRoot \
> \- hosted element: #shadowLocalTarget, with the first target for the parent

What we have to do first is to create in Puppetry a target for foo-component (shadow root):

![Defining shadow root target](/files/-Ltyu6zV9plxkktlJqvU)

Then  we create a target for the hosted element

![Defining hosted element target](/files/-LtyuBDltkgSOvUR6SEb)

The first one is a simple target like we are used to have in Puppetry, but the second is chained one. While building it we expand Options, set **Parent target** to `SHADOW_FOO` and **Parent type** to `shadow host`.

![Chained target](/files/-Ltyuth3vvu4X9qKb4xh)

As soon as the hosted element target defined (`SHADOWDOM_FOO_LOCAL`) we can address it as any simple target:

![Addressing a chained target](/files/-LtyuylKl8VVEZqJ2aEF)


# Testing Google Analytics tracking code

Either you get the requirements from management to add Google Analytics code to track Ecommerce events or you have it, but need to migrate from analytics.js library ([GAT](https://developers.google.com/analytics/devguides/collection/analyticsjs/)) to global site tag ([GST](https://developers.google.com/analytics/devguides/collection/gtagjs)) Puppetry may help you to ensure all the events are being sent within the user flows according to the requirements. Including such tests in CI/CD pipeline makes sure that with any upcoming code changes the defined requirements are still met.&#x20;

Besides Puppetry validates event payloads in accordance with Google specification. So asserting for Google Analytics tracking events will check if the API is used correctly.

By using `page.assertGaTracking` in Puppetry we can test the following Google Analytics methods:

* Common
  * Page View
  * Event
  * Social Interaction
  * App / Screen
  * User Timings
  * Exceptions
* Ecommerce plugin
  * Adding Item
  * Adding Transaction
* Enhanced Ecommerce plugin
  * Product Impression
  * Product Click
  * Product Details View
  * Addition to Cart
  * Removal from Cart
  * Checkout Process (Payment)
  * Checkout Process Option
  * Transaction
  * Refund
  * Internal Promotions

Let's see it in practice. Imagine we have a web shop that sells T-shirts. When user clicks on **Add to cart** link we expect a corresponding event to be sent to Google Analytics.

![Demo store](/files/-Lu33nhckjMvxN7jZiT0)

So the test case to trigger the described event can be following:

![Test case example for adding products to the shopping cart](/files/-Lu33yWNJKXb9wd2uJX8)

The assertion for [the expected GST API call](https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#measure_additions_to_and_removals_from_shopping_carts) may look like that:

![](/files/-Lu341hoUH1ZYnHNF_iO)

When user follows the link he or she gets **Shopping Cart** popup where it's possible to click **Checkout** link to continue with the checkout process.

![Starting the checkout process](/files/-Lu34Kd4LcvOvNhUEX9C)

Next user gets **Checkout** popup to select the payment method and clicks **Proceed** link to continue.&#x20;

![Checkout process step](/files/-Lu34Ru-Nj1GeTsGWLea)

This user behavior we turn into the test case:

![Testing checkout progress](/files/-Lu34Uw-bS_Mp9EqKs1y)

Here we assert for the [Checkout / begin\_checkout event](https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#1_measure_checkout_steps):&#x20;

![Asserting checkout progress event](/files/-Lu34baGq02uOaC3m7Ge)

We also assert for the [Checkout option event](https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options):

![Asserting checkout step event](/files/-Lu34eQb9f8cKY_2r6dj)

> Asserting for Checkout / checkout\_progress omitted to simplify the example.

Eventually user confirms the purchase :

![Purchase popup](/files/-Lu34hx1LP_PxjVjoloD)

The case we also cover with tests:

![Testing purchase event](/files/-Lu34kBf5dah0lJCRBYl)

Here we assert for [purchase event](https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#measure_purchases):

![Asserting purchase](/files/-Lu34n_zFziENR1QtnUD)

As you see the entire funnel is covered with the tests. If any Google Analytics API calls are missing or do not comply the requirements we are going to be warned in the test report:&#x20;

![Assertion for Google Analytics event failed](/files/-Lu3YXRABDB0sAGgoQEm)


# Testing Chrome Extensions

An average Chrome Extension is HTML5 application, therefore we can test it with Puppetry. The only difference here is in way we obtain the page context. The following walkthrough explains how it can be done:

1\) Open Chrome browser.

2\) Navigate to `chrome://extensions`

3\) Turn on the **Developer mode** switcher.

![Loading the Chrome extension](/files/-LtyfDspPJ00oc68b9-8)

4\) Load the unpacked version of your extension. You can create a demo one with [extensionizr.com](https://extensionizr.com/) service.

![Saving Chrome extension ID](/files/-LtyfHvtmHBGhELBTGN8)

5\) Write down the extension ID (e.g. `eclajhagmjdnniapeipnfejgmplgehme`) from appeared extension card.&#x20;

6\) Launch Puppetry and create an empty test case

7\) Check the extension `manifest.json` file for `default_popup` value (e.g. `src/page_action/page_action.html`)

8\) Add command `page.goto` with URL like **chrome-extension://EXTENSION\_ID/EXTENSION\_RELATIVE\_URL** (e.g. `chrome-extension://eclajhagmjdnniapeipnfejgmplgehme/src/page_action/page_action.html`)

![Navigating to the Chrome extension main page](/files/-LtygTdg4Oo8N6qBpw9c)

9\) Add command `page.screenshot` to take a screenshot when the extension HTML is loaded

10\) Open **Run Tests** dialog (press **F6** or **Run** in the main menu)

11\) Switch to **Browser options** and set the Chrome extension folder location (with **Browse...** button)

![Feeding Chrome extension sources to Puppetry](/files/-Ltyh1TJe1EsPh9jYEpg)

12\) Click on **Run** button

13\) Observe the report

![Chrome extension test results](/files/-LtyhBLeAwDyfQtpZn_v)

\
So we have got the screenshot with Chrome extension page view. Thus we can develop a real test case with commands and assertions pointed at the extension page context.


# Testing REST API

Nowadays it’s hard to find a web-application, which doesn’t use REST API. That’s an essential part of application functionality that we have to test. Usually REST API calls are covered with integration tests. However, if by some reason, it isn’t done you can still test it with Puppetry.

Let’s put it into practice. Imagine we have a marketplace application, which relies on [Magento API](https://devdocs.magento.com/swagger/index.html).

To start, we check if product information can be retrieved. So we use `page.assertRest` as follows:

![Making GET request and asserting the response](/files/-Lvtv4zUq90kERtqX34a)

Here we assert that call respond with status code 200 (success) and the returned JSON matches the given [JSONPath](https://www.npmjs.com/package/jsonpath).

Now let’s do something more sophisticated. We are going to obtain authentication token.

The application calls `integration/admin/token` to obtain authentication token. So we set `page.assertRest` parameters like that:

![Making POST request and asserting the response](/files/-LvtvG2N3zl6HDEM4VFk)

We expect the response of status 200 with a non-empty access token.

Note that `page.assertRest` automatically assigns the response text to `PUPPETRY_LAST_RESPONSE_TEXT` template variable. So we can use it further in the test case.


# Mocking HTTP/S Requests

When we need to ensure a decent user experience we do not only test the designed flow, but also exceptional cases. We have to check that the application responds to problems gracefully. Let’s say we have a feedback form. The typical scenario would be: user submits the filled out form and gets notified when it’s done. But what if a problem happens on the server side? The application is supposed to warn the user. How do we test such cases? We cannot reproduce the issue until everything is fine with the server side. However, we can use `page.mockRequest` to intercept submission request sent to the server and replace it with our own.

On [demo page](https://puppetry.app/demo) you can find a demo form. When user submits it the page emulates sending data:

```javascript
fetch( … )
  .then(function( rsp ){
    rsp.status !== 200 && showAlert();
  })
  .catch( showAlert );

```

If the response from the server has got status code different from `200` (OK) the form displays alert box:&#x20;

![Form error message](/files/-LvAbIx0bwoo2nZSnJ7A)

In the example server always responds with status 200, unless it's down. If we want to simulate this particular case we need to replace the response from the server with our own. Overridden response will have status 500.

The test case for the exceptional behavior may look like that:

![Testing exception behavior](/files/-LvtxcjtIRxkDHfqo-Te)

Here we make Puppetry listening to the next request containing “response.json” in the URL and replace it with errored one "500 Internal Server Error":

![Mocking HTTP/S request](/files/-LvtxZdaIKeZaq-Te7ai)

Then the test fills out the form and submits it. At the end we assert that user gets warned about the problem (`ALERT_BOX` is visible).

As you see, we can test exceptional behaviour. We can entirely emulate the server by mocking requests when it's not available. We can seed the client with predefined data when testing pages with dynamic content. And we can do much more.


# Testing Transactional Emails

Transactional email is a sort of email that require an action from the receiver side like in signup confirmation emails, password resets, purchase notifications and others. In order to automate testing of user flows involving transactional emails we need to be able to request the inbox contents from the testing tool. That can be achieved by using an email server with REST API or a specialized service such as [Sendgrid](https://sendgrid.com), [Mailgun](https://www.mailgun.com), [Email Yak](http://www.emailyak.com/), [Postmark](http://www.postmarkapp.com). These services are normally quite expensive, so alternatively we can develop a bridge one, which uses IMAP or, let’s say, Google API to access a public email server. Just to give it a try we can even go with Restmail.net, which is free and requires no registration or set up.

Puppetry provides `page.assignVarRemotely` command, which polls a given email server REST API until the desired email received (ot timeout). It retrieves the parsed the parsed value (e.g. activation link) and assigns it to a dynamic template variable accessible in all following test steps. The command can be configured to parse the received content.

## Testing Signup Flow&#x20;

To get a better grip on the idea we are going to create a simplified test project for[ imaginary ACME forum](https://docs.puppetry.app/examples) built with [NodeBB](https://nodebb.org/). So the test scenario would be to type in required fields on the registration form, submit the form and use the  link sent by email to activate the account.

### Template Variables

Let's open Puppetry, create a new project and navigate to **Settings**. There we define our template variables:

![Signup flow variables](/files/-LhtfPKB-6ZBLvS8949r)

What we need are `BASE_URL` (<http://localhost:4567/> NodeBB default one for development environment), which depends on testing environment and `TEST_PASSWORD` (fixture password used for registration) that once defined we can reuse across the project.&#x20;

### Test Targets

Now we can define the test targets for the flow:

![Signup flow targets](/files/-LhFGUUw7VfzW1AYDcdT)

The registration form ([see screenshot](https://docs.puppetry.app/examples)) has email, username, password, password confirmation inputs and submit button. We introduce these elements into Puppetry as `REG_EMAIL_INPUT`, `REG_USERNAME_INPUT`, `REG_PASSWORD_INPUT`, `REG_CON_PASSWORD_INPUT`, `REG_SUBMIT_BTN`. After submitting the form the application brings us to the GDPR form ([see screenshot](https://docs.puppetry.app/examples)), where we need to tick on consent checkboxes and click on Register button.  Those elements we refer as `REG_AGREE_EMAIL_CHECKBOX`, `REG_AGREE_DATA_CHECKBOX`, `REG_REGISTER_BTN`.

### Test Case

![Signup test case, part 1](/files/-LiILIXkJiqd6Dv66BbD)

First we need to ensure that we do no register an existing user. So we need to use a unique email address. That we achieve by dynamically  assigning a template variable `TEST_EMAIL` that will accept during test run iterating values like <test1@acme.com>, <test2@acme.com> and so on.

Next we use page.goto to navigate to the signup page.

We type in email address generated in `TEST_EMAIL`.

We type in username, dynamically built with faker expression.

We type in password and password confirmation by using `TEST_PASSWORD`  template variable, which we defined above.&#x20;

Finally we click on Submit button.

![Signup test case, part 2](/files/-LiINM6rGWy49-Hlz_M8)

That is supposed to bring us to the next page with the GDPR form.

So we wait until the page is ready

We make a screenshot.

We tick on the consent checkboxes.

We click on Register button.&#x20;

We wait until the sent email arrives to the inbox and assign the parsed activation link to template variable `ACTIVATION_LINK`.

The we follow the link and make a new screenshot.&#x20;

### Remote Service

Now we can proceed with any of the following examples

{% content-ref url="/pages/-LiIIGQpJ4ZCNOyobqYs" %}
[Example with Restmail.net](/testing-techniques/testing-emails/example-with-restmail.net)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIINWgz9-8ov38y1a9" %}
[Example with Mailinator](/testing-techniques/testing-emails/example-with-...)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIIRGKI2RbQ5uVlR0W" %}
[Example with IMAP bridge](/testing-techniques/testing-emails/example-with-imap-bridge)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIIX4abjMa2OBN441x" %}
[Example with Google API](/testing-techniques/testing-emails/example-with-google-api)
{% endcontent-ref %}


# Example with Restmail.net

[Restmail.net](http://restmail.net/) is probably the simplest toll to start with. It's free, it's requires no registration, it allows to create dynamically inboxes, it exposes a REST API to read received emails. However all the sent messages are public.&#x20;

Let's take the previous test suite and modify it a bit. For Restmail.net we need user alias instead of email to check the inbox. So we can generate it like that.

![User alias for Restmail.net](/files/-LiDgHAJHk-1Av_S9oCV)

As you see we also changed `TEST_EMAIL` template variable generation. Now it build based on earlier created `TEST_USER_ALIAS` one.

Next we find `page.assignVarRemotely` command and update the URL for of of Restmail.net:

```
http://restmail.net/mail/{{ TEST_USER_ALIAS }}
```

What we need now is a custom parser function in `page.assignVarRemotely` :

```javascript
( json, payload ) => {
  const parseActivationLink = ( text ) => {
          const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
                res = text.match( re );
          return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
        },
        sentAt =  payload.sentAt,
        unseen = json.find( msg => new Date( msg.receivedAt ) > new Date( sentAt ) );

  if ( !unseen ) {
    return null;
  }

  return parseActivationLink( unseen.html );
}
```

The function has second parameter `payload` with `sentAt` property that contains sending event timestamp. Here we check if there are any messages received  later then the application under test sent the activation email. If any found we parse it for the activation link.

Our `page.assignVarRemotely` command configuration shall look now like that:

![page.assignVarRemotely configuration ](/files/-LiIP-V5ybN7WKbDU9N_)

Now we can save the changes and run the tests. On the generated screenshots we can see that a new account was created and activated.


# Example with Mailinator

Similar to [Restmail.net](http://restmail.net/) we need user name alias to use with the REST API. so we modify the first steps in the case case as follows:

![Assigning TEST\_USER\_ALIAS and TEST\_EMAIL](/files/-LiIyYpg-JJw8k2sPpTA)

Following [Mailinator API](https://manybrain.github.io/m8rdocs/#message-api) we need to set URL for `page.assignVarRemotely`:

```
https://api.mailinator.com/api/inbox?token={{ env( "EID_SECRET" ) }}&to={{ TEST_USER_ALIAS }}
```

![page.assignVarRemotely configuration for Mailinator (part 1)](/files/-LiIzsEM_hNQomumkFPz)

However [Mailinator](https://www.mailinator.com/) unlike [Restmail.net](http://restmail.net/)  doesn't allow to receive last message content with a single request. So we need to take advantage of optional `request function` command parameter:

So we alternate the default fetch logic of `page.assignVarRemotely` as follows:

```javascript
async ( url ) => {
   const inbox = await ( await fetch( url ) ).json(),
         msgHead = inbox.messages
           .reverse()
           .find( msg => msg.subject.includes( "Account Validation" ) ),
         fulMsg = await ( await fetch( `https://api.mailinator.com/api/message?token=YourAPIToken&id=${ msgHead.id }` ) ).json();
  return fulMsg.data.parts.map( p => p.body ).join();
}
```

Thus the command will read the listing and find a message with subject containing `"Account Validation"` string. Then it retrieves and returns the raw body of the matching mail. So the parse function may look like:

```javascript
( text ) => {
    const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
              res = text.match( re );
    return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
}
```

![page.assignVarRemotely configuration for Mailinator (part 2)](/files/-LiJ-iBkOEomJK7TUkzr)


# Example with IMAP bridge

Nowadays there are many specialized services providing a REST API to access your inbox remotely.  But it comes with an expense. If you want to obtain REST API  for free, you can simply use a bridge server connected to your email provider (e.g. Gmail) by IMAP or SMTP. Puppetry is distributed with an [example of a bridge to Gmail](https://github.com/dsheiko/puppetry/tree/master/examples/gmail-imap-bridge).&#x20;

### Setting up Gmail Bridge Demo

To setup and run the bridge demo we perform the following steps:

* download and extract content of the folder to an arbitrary location (e.g.   `/var/www/gmail-bridge` )
* install dependencies `npm install`
* define required [environment variables](https://en.wikipedia.org/wiki/Environment_variable):

```bash
EID_EMAIL=joe@gmail.com
EID_PASSWORD=gmail password
EID_SECRET=secret
EID_NODE_SERVER_PORT=3500
EID_NODE_SERVER_HOST=127.0.0.1
```

{% hint style="info" %}
To make it really work with Gmail, please, [enable 2-step verification](https://www.google.com/landing/2step/) and generate an [App password](https://security.google.com/settings/security/apppasswords). This password you shall use for `EID_PASSWORD`
{% endhint %}

* adjust the body of `parseActivationLink` function to parse the desired value (e.g. activation link) from email body
* start the server `npm start`

### Testing Signup Flow&#x20;

Gmail allows to have multiple user name aliases on the same account by using pattern: `username+VariableSuffix@gmail.com`. We can leverage this to emulate email server. So we change the `page.assignVar` for the following value:

```
joe+test{{ counter() }}@gmail.com
```

It is supposed to resolve in the email addresses like: <joe+test1@gmail.com>, <joe+test2@gmail.com> and so on.

The command `page.assignVarRemotely` takes in the following configuration:

![page.assignVarRemotely configuration](/files/-LhFM2kyaQ9EBckBdDJW)

Basically we  configure `page.assignVarRemotely` to request the bridge for `TEST_EMAIL` with intervals of 1 seconds and the timeout of 600 seconds. The bridge is expected to parse email body if any available for the activation link. In case of NodeBB it uses the following function:

```javascript
function parseActivationLink( text ) {
  const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
        res = text.match( re );
  return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
}
```

The parser function of command configuration simply proxies the value from a result such as`{ value: "ACTIVATION_LINK" }`. Here you can put the parsing logic if your bridge/API doesn't do the parsing. Any falsy return of this function causes the command to proceed polling.&#x20;

Note also that we use environment variable `EID_SECRET` to pass the api key to the bridge service.

As we run tests we get from the bridge an output like this one:

![Gmail Bridge output](/files/-LhFREC-bzGIEkt9P1G8)


# Example with Google API

In the previous example we used IMAP client to connect to Gmail inbox. That can be also done by using Gmail API.  You can find on GitHub a [quick start example](https://github.com/gsuitedevs/node-samples/tree/master/gmail/quickstart) by Google, which implements OAuth authorization and retrieves list of labels from user's mailbox.

The example app is surprisingly easy to set up. You just need to follow [the steps](https://developers.google.com/gmail/api/quickstart/nodejs). &#x20;

As you run the app first time it asks you to follow a printed URL to Google Services where you will be prompted to register the app. At the end you receive a code, which hand back to the application.

But we do not need labels, but last received email addressed to a given user name alias. Well we can modify a bit  [index.js](https://github.com/gsuitedevs/node-samples/blob/master/gmail/quickstart/index.js). First let's get rid of `fs.readFile('credentials.json', (err, content) => ...` block. We rather get OAuth client with an asynchronous function instead of coping with callback hell:

```javascript
/**
 * @returns {Promise} 
 */
function getOAuth2Client() {
  const credentials = fs.readFileSync( "credentials.json", "utf8" );
  return new Promise(( resolve ) => {
    authorize(JSON.parse( credentials ), ( auth ) => {
      resolve( auth );
    });
  });
}
```

Now implement a function that accepts [a filter query](https://support.google.com/mail/answer/7190?hl=en)  and returns either raw body of the first matching the query message or nothing if no matches found:

```javascript
/**
 * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
 * @param {string} q - Google searchbox query
 * @returns {Promise} 
 */
async function getUnreadEmail( auth, q ) {
  const gmail = google.gmail({ version: 'v1', auth }),
        userId = "me";

  try {
    const listRsp = await gmail.users.messages.list({
      userId,
      q
    });

    if ( !listRsp.data.messages ) {
      return;
    }

    const [ msg ] = listRsp.data.messages,

          msgRsp  = await gmail.users.messages.get({
            userId,
            id: msg.id,
            format: "raw"
          }),

          rawBody = Buffer.from( msgRsp.data.raw || "", "base64" )
            .toString( "utf8" );

    return rawBody;
  } catch ( e ) {
    console.error( e );
  }
}
```

Well, now we can use these functions from the bridge code like that:

```javascript
const oAuth2Client = await getOAuth2Client();
const rawBody = await getUnreadEmail( oAuth2Client, "to:joe+test1 is:unread" );
// Parse rawBody for the activation link
```


# Testing Forms with Captcha

The whole point of Captcha (e.g. [Recaptcha](https://www.google.com/recaptcha/intro/v3.html)) to ensure the forms filled out by a human. So we cannot simply bypass it with automating tests. However we can still test the forms if we persuade the application under test that we are a trusted source. For example, the application uses a token-based authentication. From Puppetry side by using `page.runjs` command we can login under a test account and provide to the application the received token as certificate. The application checks if the token valid and disable Captcha for the session (obviously not on the production environment). Let's say we feed to `page.runjs` the following function:

```javascript
const rsp = await fetch( "https://rest-api-sandbox.local/api/v1/login", {
    method: 'POST',         
    body: JSON.stringify({
      email: process.env.QA_EMAIL
      password: process.env.QA_PASSWORD
    })
});
ENV.SESSION_TOKEN = rsp.json().token;
```

{% hint style="info" %}
As you can see define a dynamic template variable  as `ENV.<VARIABLE NAME>`. The same way template variables can be accessed in the code .&#x20;
{% endhint %}

Next we can use `page.setCookie` command to pass the retrieved token to the app server:

![](/files/-LhG7dAUYg2IS86LxIEP)

The application server validates the token if any available in TRUSTED\_CLIENT\_TOKEN cookie and disables the captcha for the session.


# Version Control

Version control is a crucial part of development. Puppetry has an integrated [GIT client](https://git-scm.com/). Let's go together though a tutorial to examine how we can benefit of it.

## Working with Version History

First we take a look at how we can keep version history and navigate through it.

So we open a demo project and find **Project/Git** in the main menu.

![Git configuration](/files/-LuJ2F-dRSN_Js9U6xmo)

Here we provide test author name and email. This information will be used to sign project version.&#x20;

After saving the changes we go in the main menu and click on **File / GIT / Initialize**

![File / Git menu](/files/-LuJ2_hFrCSzc4u5zZhp)

We've just created a local GIT repository for our project. There will be kept snapshots of all the committed versions. Let's see how it works.

We open a suite and jump to **Targets**. We add anew target `TEST_SELECTOR` with value `.ver1`

![](/files/-LhtuembW7Gb_40Jc9m8)

Next in the main menu we click on **File / GIT / Commit**

It opens **New Commit** modal window:

![Committing ver 1 ](/files/-LhtvIFbH52v6sYav9qS)

We name the commit as **ver. 1** and click **Commit** button. Thus we get the first committed version.

Now we change the value of the target:

![](/files/-LhtvrrlMxxGiP6zBqPO)

Then we commit the change (**File / GIT / Commit**).

![Committing ver 2](/files/-Lhtw5a_XXXcGjpBr4ey)

Well, we have at least two versions of the project, so it makes some history.  Let's see what we really have. Click on  **File / GIT / Checkout**

It opens the following modal window:

![Project local history](/files/-LhtwtzUYGzdLVijxnxB)

Here we can see our previously committed changes.  Now we just to the **ver. 1** by clicking on **Checkout** link.

As we see `TEST_SELECTOR` has value `.ver1` of the previously saved version. As it says we are on a detached version. It's not the working one, so we can simply examine the contents and copy assets to paste into the head of history.

As we done we click on **Checkout master** button:&#x20;

![Checkout master button](/files/-Lhtxx78wpbwE282w2IG)

We've back to the working (last) version:

![](/files/-LhtybKD86BT0g81tD1D)

## Team Collaboration

Version control is especially good for collaboration. Let's imagine we have QA engineers Samwell Tarly and  Davos Seaworth. Sam is just committed a new version of the project and want to share it with Davos. For that guys need a remote repository. Let's take [Bitbucket](https://bitbucket.org) as service provider. So we register and create a new project:

![](/files/-LdyKf7-heeT5RF-CKK-)

That will bring us to the project page, which currently filled in with instruction to get started:

![Project page at Bitbucket](/files/-LhuT-iHOEg9MMEBaNO-)

Now we go back to Puppetry, open **Settings** application menu and jump to **GIT** tab. Next we scroll down to **Git Remote Repository** section:

![Configuring remote repository](/files/-LuJ3CCE1P4wQZOk46W7)

Here we need to provide the remote repository URL (from Bitbucket) and user credentials.  Now we can synchronize our local repository to the remote one. Click **File / GIT / Sync**&#x20;

It opens the following modal window:

![Sync with remote modal window](/files/-LuJ37HoEMu9m1ZUM5Km)

As we confirm the synchronization we can refresh the page on Bitbucket:

![Project artefacts on remote repository](/files/-LhuUqqScJ1f7SaeEf4n)

We can see that our project artifacts now are available there.&#x20;

Now Davos opens Puppetry on his machine and configure GIt:

![Davos' GIT configuration](/files/-LuJ3Hiv8LRN5HMq-B-c)

He opens **File / GIT / Clone** in the main menu and gets the following modal window:

![Clone Project modal window](/files/-LuJ3MXEy1Id-_v_nSKV)

As he selects a destination and clicks on Clone, the version of Sam's project gets delivered to Davos' Puppetry.&#x20;

Well, let's really collaborate. Davos changes the target again:

![](/files/-LhuVspeEbN0Qs7IKLvJ)

and commits the changes. Then he synchronizes with the remote repository (**File / Git / Sync with remote** ). Sam also synchronizes and sees the changes from Davos arrived:

![](/files/-LhuWODH266CgMUK--xS)


# Tips and Tricks


# Embrace the Power of CSS

When working with targets or methods such as `*. assertNodeCount` besides generic CSS selectors we can use [Pseudo classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) and [Attribute selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors). Let’s take a look at some real-world example to discover what advantages it can bring us&#x20;

### Forms&#x20;

Imagine we have a form

```markup
<form>
  <input
      type="email"
      required
      name="email"
      placeholder="Enter email" />
      
  <input
      type="text"
      required
      name="zip"
      pattern="^\d{5}$"
      placeholder="Enter ZIP code" />    
      
   <button type="submit">Submit</button>   
</form>
```

With the test case we seed the form with  data and submit the form. Then we want to assert that there are no inputs in invalid state ([see also HTML5 Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)) So we use `FORM.assertNodeCount` method to look for `input:invalid`&#x20;

![Asserting form validity](/files/-LtisY-620IDcRxAASrd)

If any of test values do not pass the validation (empty value, invalid email address, invalid ZIP) the assertion will fail.&#x20;

What is more, we can assert that a checkbox control is checked with `target.assertMatchesSelector` method:

![Asserting checkbox is checked](/files/-LtisvbqOCNKwVHigK2M)

### Lists&#x20;

Imagine we have a blog app. We are testing article details page and want to ensure the social sharing links are not broken. Let’s say a simplified layout for the block is following:&#x20;

```markup
<ul id="social">
  <li><a href="https://www.facebook.com/...">Share via Facebook</a></li>
  <li><a href="https://twitter.com/...">Share via Twitter</a></li>  
</ul>
```

So we create targets `SHARE_FACEBOOK` and `SHARE_TWITTER`&#x20;

![Taking advantage of attribute selectors](/files/-LtitVdm-tMkUtT789lt)

The trick here is that `href` attribute of share links in reality  is a long string and also dynamic (depends on the page), but we go with **attribute selector** and target any links whose `href` contains a keyword (e.g. facebook.com). Thus we distinguish facebook and twitter links. Now we can assert availability and visibility:

![Asserting availability and visibility ](/files/-Ltiu3lZ6_1tOLYH4oyU)

We can assert that the links are provided with text description:&#x20;

![Asserting that element is not empty](/files/-LtiuE7Z-MjDm1ZUSyPO)

The selector `:not(:empty)` will be matched only if the link has text or child nodes.

### In-page Navigation&#x20;

Imagine we have a page with internal links. When user follows such a link the page changes its hash and scrolls to the anchored content or changes UI state. In our case we have a tabbed UI component, which opens the tab with desired content when page hash is `#chapter1`. If we want to assert the internal link is followed we use `page.assertNodeCount`

![Using :target pseudo-class](/files/-LtiuhQFZrkNwVWG35Ej)


# Command API

API available via page.runjs command

## NPM modules

Modules "fs", "path" and "os" exposed with the corresponding variables.

```javascript
const title = ( await bs.page.title() ) + os.version();
```

{% hint style="info" %}
Available since ver. 3.2.6
{% endhint %}

## Puppeteer methods

* **bs** - object representing browsing session
  * **browser** - instance of [Browser](https://pptr.dev/#?product=Puppeteer\&version=v1.15.0\&show=api-class-browser), which is being created in test `setup` with configuration provided in Puppetry app
  * **page** - instance of [Page](https://pptr.dev/#?product=Puppeteer\&version=v1.15.0\&show=api-class-page) (alias of `bs.browser.page`)
  * **performance**
    * **resources** - array with network activity logs
  * **getGaTracking** - log of Google Analytics beacons
  * **getTarget** - get target by name
  * **getTargetOrFalse** - the same as previous, but when target is not available in the DOM return false instead of throwing exception
  * **target(**`element: ElementHandle`**)** - helpers&#x20;
    * `getProp(prop: string): Promise<string>` - obtain element property value
    * `getAttr(attr: string): Promise<string>` -  obtain element attribute value
    * `isVisible(): Promise<boolean>` - find out f element visible
    * `select(value: string): Promise<void>` - set a value on select element

Examples:

```javascript
// Navigating to "https://dsheiko.github.io/react-html5-form/"
  await bs.page.goto( "https://dsheiko.github.io/react-html5-form/", {
    timeout: 30000,
    waitUntil: "load"
  });
  
  const title = await bs.page.title();
```

### Working with targets

Assuming we have targets `SUBMIT_BTN`, `EMAIL_INPUT` defined with Puppetry app

```javascript
// Emulating mouse click
await ( await bs.getTarget( "SUBMIT_BTN" ) ).click();   
// Emulating user input
await ( await bs.getTarget( "EMAIL_INPUT" ) ).type( "json@snow.got" );
// Obtaining "name" attribute's value of EMAIL_INPUT 
const name = await bs.target( await bs.getTarget( "EMAIL_INPUT" ) ).getAttr( "name" );       
```

## Jest tools

Please find available API in [official documentation of Jest framework](https://jestjs.io/docs/en/getting-started). Most demanded tool for the context is expect. In the scope of the test it is extended with the following methods:

* **expect( result )**
  * `toBeOk( expectedValue, assertionSource)` - assert the received value is truthy
  * `toBeEqual( expectedValue, assertionSource)` -  assert the received value equals expected one
  * `toIncludeSubstring(expectedSubstring, assertionSource)` - assert the received string contains expected one
  * `toPassCondition(operator, valueToCompare, assertionSource)` - assert the received value satisfies a given Puppetry condition
  * `toMatchBoundingBoxSnapshot(snapshot, assertionSource)` - assert the received bounding box matches a given snapshot
  * `toMatchPosition(position, target, counterpart, assertionSource)` - assert the received position object matches a given one

Examples:

```javascript
// Asserting that page HTML satisfies the given constraint
result = await bs.page.content();       
expect( result ).toIncludeSubstring( "demo", "page.assertContent" );

// Asserting that window scroll offset satisfies the given constraint
result = await ( await bs.page.evaluate( () => window.scrollX ) );       
expect( result ).toPassCondition( "lt", 10000, "page.assertScroll" );

// Asserting that EMAIL_INPUT element is visible
result = await bs.target( await EMAIL_INPUT() ).isVisible();       
expect( result ).toBeOk( "EMAIL_INPUT.assertVisible" );

// Asserting that the bounding box of the element satisfies the given constraint
result = await ( await SUBMIT_BTN() ).boundingBox();    
expect( result ).toMatchBoundingBoxSnapshot( {
       "xOperator": "gt",
       "xValue": 0,
       "yOperator": "gt",
       "yValue": 0,
       "wOperator": "gt",
       "wValue": 0,
       "hOperator": "gt",
       "hValue": 0
     }, "SUBMIT_BTN.assertBoundingBox" );

 // Asserting that the bounding box of the element satisfies the given constraint
result = {
        target: await ( await DAY_SELECT() ).boundingBox(),
        counterpart: await ( await MONTH_SELECT() ).boundingBox()
      };       
expect( result ).toMatchPosition( "left", "DAY_SELECT", "MONTH_SELECT", "DAY_SELECT.assertPosition" );
```

## Helpers

Helpers are functions available in specification (suite) scope

* **util**&#x20;
  * **png** - build screenshot option&#x20;
  * **pollForValue** - poll for value (used by `page.assignVarRemotely`)
  * **exp** - template expressions functions
  * **bytesToString** - translates size into human-readable string
  * **generateTmpUploadFile** - creates a file in temporary directory by given name and size
* **fetch** - [Node.js-implementation](https://www.npmjs.com/package/node-fetch) of window\.fetch
* **localStorage** - [Node.js-implementation](https://www.npmjs.com/package/node-localstorage) of localStorage

`png( id, parentId, screenshotTitle, options ): Object` - build screenshot options

Example:

```javascript
await bs.page.screenshot( util.png( "id", "id", "Just for test", {
  "fullPage":true
}));
```

This command saves screenshot in `./screenshots/All--available--methods/Just-for-test.png` given that test title is *All available methods*

## Node.js

In the test scope one can access [Node.js modules](https://nodejs.org/en/).


# Test Application

## ACME Forum&#x20;

ACME forum is an imaginary application built with [NodeBB](https://nodebb.org/) and used as an example test application in this documentation.

### RWD

User navigates to landing page `http://localhost:4567/` on **desktop** &#x20;

![ACME forum landing page on desktop](/files/-LhL9i3pnszz52EUiCil)

Observe: main menu and teasers are visible

User navigates the landing page on **mobile (iPhone 8)**

![ACME forum landing page on mobile](/files/-LhL9kloLNT5-PUjemTW)

Observe: main menu and teasers are hidden

### Signup flow

User navigates to `http://localhost:4567/register` and fill out the form

![ACME forum registration form](/files/-LhFGJQ9ofDiF8a1KqIQ)

User  ticks on consent checkboxes on the following form

![](/files/-LhFGPeIiWDQTsiA1Nel)

User click on **Submit** button.

Observe: the user is registered

{% hint style="info" %}
When **Require Email Confirmation** switch in admin/settings/user panel is set ON the flow extends with account activation flow ([learn more](https://docs.puppetry.app/testing-emails))
{% endhint %}

### Login flow

User navigates to `http://localhost:4567/login` and fill out the form

![ACME forum login form](/files/-LhJpPOGuvXLIbnCimBE)

and submit click **Login** button.

Observe: ACME header changed, now it contains user's picture.

![](/files/-LhK-vNrxO-1Ce8aFCcc)


# Welcome Puppetry

Puppetry is an open-source desktop application that gives non-developers the ability to create, manage, and integrate automated tests for Web.

* [Releases](https://github.com/dsheiko/puppetry/releases)
* [Issues](https://github.com/dsheiko/puppetry/issues)
* [Slack channel](https://puppetry-app.slack.com)

End-to-end testing of a web application is mostly about locating a target (page or DOM element), performing a command on it (navigate to URL, modify DOM, etc.) and asserting that the target satisfies the provided conditions. Puppetry provides a user-friendly interface to define targets and manage test cases bundled in test suits. During test run (or export) Puppetry suites are turned into [Jest](https://jestjs.io) test files powered by [Puppeteer API](https://pptr.dev/).&#x20;

Puppetry deals with projects, where [**project**](https://docs.puppetry.app/project) is basically a folder with project configuration, reusable snippets and suite files.  [**Suite**](https://docs.puppetry.app/suite) contains [**targets**](https://docs.puppetry.app/target) and [**groups**](https://docs.puppetry.app/group) of [**tests cases**](https://docs.puppetry.app/test-case), where test case is a sequence of [**test steps**](https://docs.puppetry.app/test-case) (**commands**, **assertions** and **references**). Besides, project includes environment-dependent [**template variables**](https://docs.puppetry.app/template) and [**git configuration**](https://docs.puppetry.app/version-control).


# Getting Started

When you launch Puppetry the very first time you are expected to see the welcome page:

![Puppetry welcome page](/files/-LhLRcLvns3uBICacvQ_)

At this stage we cannot do much - only to open an existing project or create one:

![File menu options](/files/-LhLRkNqJxTAoBr6oiD3)

Let's create our first project intended to test responsive design on the landing page or our [imaginary ACME forum application](https://docs.puppetry.app/examples).

![New project modal window](/files/-LhLSIYIX6I6xwLOB61S)

So we enter project name, browse for location and provide a name for the first suite. After pressing submit we end up on the suite page:

![Newly created suite](/files/-LhtIwDBGihw2qETiJVv)

Let's add a few targets:

![ACME forum test targets](/files/-LhtIz5e300-kQM1MrX0)

Here we identify main menu container, which is visible on desktop, but behind the burger menu on mobile. We define teaser target (the first matching the selector), which also visible only on desktop. Eventually we specify mobile menu container that on the contrary shows up on mobile, but hidden on desktop.

Now we switch to the **Test Cases** tab:

![Initially no groups in the suite](/files/-LhtJ1rpwwy4DmKd2bLx)

Let's add the first group "Landing page":

![Expanding the first group](/files/-Lhtc5acajFjV3hs3oGg)

Now we have to expand the newly added group, so we click **expand** button:

![Expand button](/files/-LhLVc6ZgPkr6wECB-1b)

Under the group record we get a new form to enter test case name:

![Empty test case expanded](/files/-LhtcG31HcPLTNK6OJ8L)

As a new test case record appears we again click **expand** button next to it:

![](/files/-LhtcJfs_JPqxgqVv0VI)

Let's add our first command into the test case. So we click on **Add a command/assertion** button:

![Add a command/assertion button](/files/-LhLWo6WE1L6wSPLnEvj)

That brings us to **Edit Command/Assertion** modal window:

![Edit Command/Assertion modal window](/files/-LhLXHTZTTlVFXGl61H8)

Our goal is to ensure desktop-like screen size. So we select **page** target and **setViewport** method. We set screen width **1440** and height **900**:

![](/files/-LhLXtTY9Lonhs0huJqj)

Many of test steps have additional options collapsed under **Advanced Options** link. E.g:

![Advanced options of page.setViewport command](/files/-LhLYMoPwuSFKtG7QbgC)

Here we don't need to change them. We click on **Save** button and get back to test case editing, where we click on **Add a command/assertion** button again to set navigation URL:

![page.goto command](/files/-LhLZ4BxSdefOxHu1iv-)

Now we have to assert that desktop menu and teasers are visible in this test case, but mobile menu not. So we click **Add a command/assertion** button and select a target identifier corresponding to mobile menu container:

![Selecting a target](/files/-LhLZlfejF735wUtkcxq)

Next we need `assertVisible` method, so we focus on method input and start typing **"assert"**:

![Searching for a method](/files/-LhL_72jnCipuK8eBBew)

The list of options adjust according to search keyword, so we can easily find the required method.

![TARGET.assertVisible method](/files/-LhL_VBMDlxtOLBsE1au)

We tick off **value** checkbox and click **Save** button. Similarly we create assertions for desktop menu and teaser, but set **value** to on. Finally we add command `page.screenshot` and our first test case is ready:

![](/files/-LhtcMr8qNKXXRvi-yck)

I would suggest now to run the tests and check if everything is ok. So we press **F6** and get the following modal window:

![Test Reports modal window](/files/-LhLavDqTBSCQabPH03Z)

Here we just click on **Run** button. Yet the available options are:

* **On/Off** switch makes Puppetry to run the test either in headless mode or in default browser (additional options will open as son as you set it **On**)
* select a [target environment](https://docs.puppetry.app/template)
* select which suites we include in the run

As we run it Puppetry opens panel **Test report** with test results:

![Test results](/files/-LhLfHJTpDJVhmQ0b05o)

It says everything is fine. If we click on Open directory with generated screenshots link we can see generated folder named after the test case where we can find the screenshot:

![Generated screenshot](/files/-LhLfspmHAqA5yqYGgzv)

Now we create a new test case named **on iPhone 8 mobile menu displayed**. First we add a command to emulate iPhone 8 device:

![page.emulate command](/files/-LhLglDX5mtsNBiSefA3)

Next we need a command to visit the landing page and assertions from the previous case. Let's just copy/paste them. So we focus on `page.goto` command of the first test case and right-click on it:

![Copying page.goto command](/files/-LhLhDyBa121M8iVuvbk)

Now we focus the destination - `page.emulate` command of the second test case and right-click again:

![Insert copied command](/files/-LhLhf1H_U7qh6VbQRzB)

We need to repeat it for the assertions and `page.screenshot` command. Then we edit assertions for opposite values and give a new title for `page.screenshot`. After that our first suite is complete.

But what if any of assertion was wrong? Let's deliberately change assertion value for mobile menu container in the second case. So now it's expected to be not visible. We run tests (**F6**) and observer the failed test step reported:

![Failed test](/files/-LhLipgs8_ZqP6weJrv8)

If we switch to suite tab (rwd.json) we see that failed assertion record is highlighted:

![Failed assertion is highlighted](/files/-LhLjAYzoTTx6r1mRR1M)

When working with test cases (especially debugging) sometimes you will want to temporarily disable some test steps and run only the ones you currently focused on. That can be done from the context menu (right-click). The disabled test steps will be also highlighted:

![Disabled test steps are highlighted](/files/-LhLk3wy9igbDDZfy-Lr)

&#x20;


# Project

Project is basically a folder with project configuration (including template variables and environments) (`.puppetryrc`), git configuration (`.puppetrygit`), reusable snippets file (`.snippets.json`) and suite files (`*.json`) . Project also keeps information about all latelly opened panels, expandable rows.&#x20;

You can use hotkey **Ctrl+S** (⌘S) to save project (and open suite).

## Create New Project

To create a new project you can press **Ctrl+Shift-N** (⌘⇧N)  or click on **File / New Project** item in the main menu:

![New Project in the main menu](/files/-LheK0gHD2nclL8qsRQA)

It will open the following modal window:

![New Project modal window](/files/-LhLSIYIX6I6xwLOB61S)

You need to provide project and suite names in the corresponding fields and use **Browse...** button to select project location. Then you press **Create** button.&#x20;

## Open Project

To open project you can press **Ctrl+Shift-O** (⌘⇧O)  or click on **File / Open Project** item in the main menu:

![Open Project in the main menu](/files/-LheJyg9GemOuSWruoL_)

It will open the following modal window:

![](/files/-LheKFSLXv0bVXwZELa6)

You need to use **Browse...** button to select project location. Then you press **Open** button.&#x20;

## Edit Project

When you have a project open, you can still change the project name. Click on the edit icon next to the project name in the application header:

![Edit project icon](/files/-LheKR1Y7WAB78IJB0tl)

It opens the following modal window:

![Edit Project modal window](/files/-LheKTqTiW34dRA9zBHu)

So you can provide a new name and press **Save** button.

{% hint style="info" %}
This modal also has a read-only field App data directory, showing where the Electron project is located (application storage, run-time tests export and so on)
{% endhint %}

## Save Project As..

You can clone the project to a new location. To do it click on **File / Save Project As...** item in the main menu:

![Save Project As.. in the main menu](/files/-LheLOWpHrJ4v6bxSxlr)

And select a new location in the following modal window:

![Save Project As... modal window](/files/-LheLjnCIMXNx8vQRU_x)

## Project Page

If you close all the application tabs you can see the project page:

![Project page](/files/-LheM2gt0Q5NM0yqGiQ9)

{% hint style="info" %}
If you forgot the physical location of your Puppetry project, use Project page to find it out
{% endhint %}

## Project Explorer

You can find the project explorer window below the main menu as soon as you have opened the very first project. The explorer helps you to switch between recently opened projects and manage the list of active project suites.

![Project explorer](/files/-LhKwwnorspjY25SY2mt)

In order to **focus a project** or suite you click on it once. To **open a project** or a suite you double-click on it.‌

Right-click on a suite opens the context menu where you can choose an action: **open** or **delete** (suite file):

![Suite context menu‌](/files/-LhKxLwvQt9gGJvKCHYB)

Right click on project displays the context menu where you choose between **open project** and **remove project from the list**. The last one doesn't physically remove project folder, but simply won't show it anymore in project explorer unless you open via main menu again.

![Project context menu](/files/-LhKy6yHSpeiMRcb6anw)


# Suite

Suite is literally a JSON file keeping suite-related targets, groups, test-cases and test-steps.

## Create Suite

To create a new project you can press **Ctrl+N** (⌘N)  or click on **File / New Suite** item in the main menu:

![New Suite in the main menu](/files/-LhewMjZirrIeSlPPt-m)

It opens the following modal window:

![New Suite modal window](/files/-LhewXBRLkt9kg3PGf_z)

You need to specify suite name and press **Create** button. It will create a json file with name built from provided one and open it.

Optionally you can give a custom name to the file. Just click to open **Specify filename** panel:

![Custom filename for suite](/files/-Lhex38yAFv8tsx0EjoB)

## Record Suite

After you created anew suite you can either populate it manually or record it. To go with the last option, click on **Record** button in the footer of **Groups** panel:

![Record suite button](/files/-LhkDYgvTI6O0zjJuPnZ)

It opens a new window:

![Suite Recorder window](/files/-LihoDvT6LGi1Bzq9iFz)

Here you can

1\) Select viewport&#x20;

![Select viewport combobox](/files/-LhkE09lugZsKU0aILMZ)

2\) Set a color for element highlighting on test pages

![Color selector](/files/-LhkEAur67dVM_6vyn2J)

3\) Enter URL or test page and press **ENTER**. It will bring you to the specified page:

![ACME forum login page in the recorder](/files/-LhkET8WdS7zbUbSHlrE)

Now you can interact with the page (click on elements, type in inputs and so on). Any affected element will be automatically registered into targets. But you can **right-click** (or **Ctrl-Shift-click) an element** to register it into targets explicitly.&#x20;

![After Ctrl-Shift-click you will be prompted for a target name](/files/-LhkGiebnxtLV_mVt7I-)

4\) When you are done, click on **Create Suite** button in the header. That will bring you back to the **Groups** panel, where you will see a newly created **Recorded group:**

![Recorded group collapsed](/files/-LhtXqI0GGaz48Zi-QDC)

Let's expand the generated group and the test case:

![Recorded group expanded](/files/-LhtXwsNx3d6rRJu7wlb)

As you can see our interactions on the page recorded into a valid test case. Now let's switch to **Targets** panel:

![Recorded targets](/files/-LhtY-rgBBhHf_d5uXr3)

As you can see automatically generated targets named after their selectors. Besides, we have an explicitly specified target (`AVATAR`)&#x20;

### Recording Suite Showcase

![Recording suite in practice](/files/-LhtY5NpG6cFgPrpR4XV)

## Open Suite

To open suite click on **File / Open Suite** item in the main menu:

![Open suite in the main menu](/files/-LhexwQNM3mqosDT9O6_)

It opens the following modal window:

![Open Suite modal window](/files/-LheyX8DE7v9S8bAwjFG)

Here we select a suite from the list of suites currently available in the project

You can also use [Project Explorer](https://docs.puppetry.app/projects#project-explorer)

## Save Suite

To create a new project you can press **Ctrl+S** (⌘S)  or click on **File / Save Suite** item in the main menu:

![Save Suite in the main menu](/files/-LheytfIrcYRjh91LtuX)

## Save Suite As..

You can clone a suite. To do it click on **File / Save Suite** **As...** item in the main menu:

![](/files/-LhexthAeaVU30dOfCoe)

It opens the following modal window:

![Save Suite modal window](/files/-Lhez3GAPwbLlXG3tTCX)

Here you just need to provide a name for suite clone.

## Suite Options

In suite tabs you can find one called **Options**:

![Options suite tab](/files/-LhezGb9vDubMQopi0tY)

Click on it to get the **Suite Options** modal window:

![Suite Options modal window](/files/-LhezcmhTwXrQiv9F3b1)

Here you change the suite name and set up test run timeout (maximal allowed time for the entire test run)


# Group

During test run or export Puppetry suites are turned into [Jest](https://jestjs.io) test file. Groups are converted into [describe blocks](https://blog.teamtreehouse.com/an-introduction-to-rspec). So we use them to define the context for the included test cases.

![Group and test cases](/files/-LhtdNncO5ypNeDZfyHr)


# Target

Targets are basically identifiers associated with locators (CSS selector or XPath) that we can refer in the tests

## Adding Target with CSS Selector

![Target with CSS selector](/files/-LhtZ_WxJTMas-5pOQi3)

{% hint style="info" %}
When you change name of an existing target, the new name will be reflected in all the test cases referring to it
{% endhint %}

## Adding Target with XPath

![Target with XPath](/files/-LhtZdqpJJ8Xu8CeMOPq)

## Editing Targets as CSV

You can edit targets as CSV. After clicking on **Edit as CSV** button you get a modal window where existing targets presented as lines of target/locator pairs separated with comma.

![Editing Targets as CSV](/files/-Lht_XghfbsQghZQirPo)


# Test Case

Test case is a specification of commands, assertions and references  to ensure that a targeted component of the test application acts as intended.

![Group and test cases](/files/-LhtdWH-xjLQdX1EYzFh)


# Test Step

Test steps make the test case. They can be commands, assertions and references. There are different collections of commands and assertions for page and for a target (DOM element).

![Test steps in the body of a test case](/files/-LhfCcq2gMVwL9OAb1qG)


# Page Commands

## page.emulate

Emulates given device metrics and user agent

![](/files/-LhUyMBtfJ0IfdmCp_4I)

## page.setViewport

Defines browser viewport, where the viewport is the user's visible area of a web page

![](/files/-LhUyOY2S1qqLTgqyhXw)

## page.goto

Navigates to a given URL and waits until the page loaded

![](/files/-LhUyR23MNONHrVErv2j)

## page.moveMouse

Moves mouse to given position

![](/files/-LhUyTWiooE47Q80O5vH)

## page.click

Emulates mouse click according to given options

![](/files/-LhUyVrzTKDMJfAeWt7l)

## page.tap

Emulates tap according to given options

![](/files/-LhUyZxS3TiF7KvXjgUo)

## page.press

Emulates pressing on a key, optionally with modifiers such as ⇧, ⌥, alt, control, ⌘

![](/files/-LhUybm0h_BJKNrMHcHi)

## page.scroll

Scrolls the document in the window by the given amount

![](/files/-LhVFQdCoSeHpP9RX_ar)

## page.reload

Refreshes the page

![](/files/-LhVGqprGsB1DV3cw4AF)

## page.setCookie

Sets cookies on the page

![](/files/-LhZm5RauduHMsexhnC-)

![](/files/-LhZm7FLwTvQBI4oqAhk)

## page.waitFor

Waits for a given time before proceeding to the next command

![](/files/-LhVGIp96ME32Yb-3Hlx)

## page.waitForSelector

Waits for an element matching a provided [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)

![](/files/-LhVGMzXrOcsx8990krh)

## page.waitForNavigator

Waits until a given event before proceeding to the next command

![](/files/-LhVGQ5Q52lBJ-fyMNDJ)

## page.evaluate

Evaluates JavaScript code in the page context

![](/files/-LhVGTC6jvVOzWXR0ZEz)

## page.runjs

Runs custom JavaScript code in the test suite with use of [Puppeteer API](https://pptr.dev) and [Puppetry API](https://docs.puppetry.app/command-api). You can access [dynamic environment variables](https://docs.puppetry.app/template) via `ENV` map (e.g. `ENV[VAR_NAME]`)

![](/files/-LhVGV_ZO9UHydwZPnFb)

## page.debug

Stops execution of JavaScript, and calls (if available) the [debugging function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger). During test scenarios may happen many things - DOM changes, pages load. It makes hard to figure out why some test assertions fail in some particular moment. You can use this tool to set a breakpoint. Test flow will stop as soon as it reaches this point. So you will be able to examine the page state with DevTools.

![](/files/-LhVGYBFl8xEhObFsGYA)

## page.assignVar

Assigns template variable dynamically

![](/files/-LhVGa5vNcopG4IZ3QuK)

## page.assignVarRemotely

Polls `URL` with a given intervals until a response satisfying `parserFn` function received or a specified timeout exceeded. It can be used, for example, to retrieve a value from an [email sent by the application under test](https://docs.puppetry.app/testing-emails)

![](/files/-LhVGkSPTzT11H032LVg)

![](/files/-LhVGdAm_OoJXcqNQhyJ)


# Page Assertions

## page.assertTitle

Asserts that the page title satisfies the given constraint

![](/files/-LhVJTdAq6LPSUt_xVMq)

## page.assertUrl

Asserts that the page URL satisfies the given constraint

![](/files/-LhVJWQ6YuX180ncqbmt)

## page.assertHTML

Asserts that the page content (HTML) satisfies the given constraint

![](/files/-LhVJZYd3kD2a2LJBt_1)

## page.assertNodeCount

Asserts that number of elements matching a specified selector satisfies the given constraint

![](/files/-LhVJamoN-jA53_rzANx)

## page.assertScroll

Asserts that [window scroll](https://developer.mozilla.org/en-US/docs/Web/API/Window/scroll) offset satisfies the given constraint

![](/files/-LhVJdPKFj1xWPhZuJZK)

## page.assertVar

Asserts that variable associated with a given name satisfies the given constraint

![](/files/-LhVJgFndGiBquWRuDTf)


# Target Commands

## target.type

Focuses the element, and then sends keyboard events for each character in the text

![](/files/-LhVKb_cTMZecAmJQk7O)

## target.select

Sets value on select element

![](/files/-LhVKemidM0zF-zWjUax)

## target.focus

Focuses the element

![](/files/-LhVKhPUluU0fzRSdoel)

## target.click

Emulates mouse click on the element

![](/files/-LhZhYVOcYb0WIdnn8K4)

## target.reset

Resets the input or form element

![](/files/-LhZh_vJgkVoqnkkLXTk)

## target.upload

Sets the value of a file input. Note that the target element must be INPUT of FILE type. It is also recommended to emulate click on the target (or clickable element calling the file browser) before going with this command

![](/files/-LhZhcYE7FFOmyMYwKKN)

## target.tap

Taps the element

![](/files/-LhZhf0Av-UMZkBJyUvX)

## target.scroll

Sets the number of pixels that an element's content is scrolled horizontally/vertically.

![](/files/-LhZhgsMospYE7AUNrdG)

## target.hover

Scrolls element into view if needed, and then hovers over the center of the element

![](/files/-LhZhjGXL_ghOK9rzq-j)

## target.checkBox

Toggles checkbox/radio state

![](/files/-LhZhlV-nLPkw_75P885)

## target.toggleClass

Toggles the specified class value (adds or removes)

![](/files/-LhZhps1m3bXm_h3-riL)

## target.setAttribute

Sets the value of an [attribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) on a target

![](/files/-LhZhtVeIo0bKfbCGRDi)

## target.screenshot

Takes a screenshot of the target element.

![](/files/-Ljpzg0dm7ma2P44C1KA)


# Target Assertions

## target.assertAttribute

Asserts that the specified [attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes) of a target satisfies the given constraint

![](/files/-LhZhyLGXpIsxBFSGjMc)

## target.assertProperty

Asserts that the specified [property](https://developer.mozilla.org/en-US/docs/Web/API/Element) of a target satisfies the given constraint

![](/files/-LhZi-i0BR44ImAPonxI)

## target.assertVisible

Asserts that the element is currently visible in the current viewport ([intersecting](https://pptr.dev/#?product=Puppeteer\&version=v1.15.0\&show=api-elementhandleisintersectingviewport), styles `display` is not `none`, `visible` is not `hidden`, `opacity` is not `0`)

![](/files/-LhZi1xO1JvslB09UbNl)

## target.assertHtml

Asserts that the HTML content of the focused element satisfies the given constraint

![](/files/-LhZtdvwZ_xBklQjTx3u)

## target.assertBoundingBox

Asserts that the [bounding box](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) (size and position) of a target satisfies the given constraint

![](/files/-LhZiCSzQEc9x2kgk3kJ)

## target.assertPosition

Asserts that target's position relative to other given target

![](/files/-LhZiG0O5DwM8cRcp-eM)

## target.assertStyle

Asserts that the [computed style](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) of a target matches the given value

![](/files/-LhZiKbc3gj6fjF-Ynw8)

## target.assertMatchesSelector

Asserts that a target matches a given selector or pseudo-selector

![](/files/-LhZiNp44w_A8uvTQSIq)

## target.assertNodeCount

Asserts that number of child elements matching a specified selector satisfies the given constraint.

![](/files/-LjpyfhTPeiI6krgboXz)

## target.assertTextCount

Asserts that number of child elements containing a specified text satisfies the given constraint.

![](/files/-LjpywrnQsTUpFSWPMCR)

## target.assertContainsClass

Asserts that the specified class value exists in the [element's class attribute](https://developer.mozilla.org/en-US/docs/Web/API/Element).

![](/files/-LhZiQR2Pl08Yui8r2Fs)

## target.assertScroll

Asserts that [scroll](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop) offset on the target satisfies the given constraint

![](/files/-LhZiTgawNn4IzCNn5CS)


# Managing Assets

Puppetry assets (targets, groups, test cases, test steps, variables, snippets) have the same management interface based on data-tables.

## Adding Assets

To add a new asset you need to fill in the empty field(s) at the end of the table and press **Add** button next to it:

![Adding assets](/files/-LhtRwmdGgTxr7jKq0tH)

## Inserting Assets

Alternatively you can add an asset to a particular position. Just hover the target record and right-click for the context menu. Select **Insert** item.

![Inserting assets](/files/-LhtS4-m-JNT6uCUtOK-)

## Editing Assets

To edit an asset, click on **Edit** link next to the target record, provide new value and click **Save** button.

![Editing assets](/files/-LhtS8_DnsLorKLrTutv)

## Cloning Assets

If you need a sequence of similar assets, you don't need to create each one from the scratch. Instead you clone one and adjust parameters of the clones:

![Cloning assets](/files/-LhtSCtCqDGxgn7DFtqS)

## Removing Assets

To remove an asset, click on **Remove** link next to the target record. Choose **OK** for confirmation.

![Removing assets](/files/-LhtSG5vJDV5xKYIHX6S)

## Disabling Assets

If you don't want to remove an asset, but temporarily exclude it for test run, hover the target record and right-click for the context menu. Choose **Disable** item:

![Disabling assets](/files/-LhtSItLUcowyqulzXip)

## Drag & Drop

You can use drag & drop  to move assets:

![Moving assets](/files/-LhtSNcF8Ke2IWagtQ_c)

## Copy/Paste

You can copy/paste any of assets:

![Copy.paste assets](/files/-LhtSS-Hah7MnnPmOglz)

Besides, you can copy an asset in one suite/project and paste it in another. In fact, you can even copy an asset, save it in a text file and paste it later when you need it:

![Cross-application copy/paste](/files/-LhtSVRYq2XyRnntUbTn)


# Snippets

Snippets are reusable test cases in scope of project. So you can create a snippet and refer to it in your test suites. For example, to login a user we need to perform multiple actions: visit login form, fill it out and submit. Instead of adding these test steps to every test case for logged in user we can simply create once a snippet and then refer to it from many test cases. Besides, to every reference we can assign  a set of template variables that can be addressed in the body of the snippet. In other words, we can execute the same snippet, but giving it a different input every time.

## Using Snippets by Example

In order to illustrate the concept we are going to create a simple test case for an [imaginary ACME forum app](https://docs.puppetry.app/examples). The test plan would be to register a user, sign in with the newly created account and check the user is really logged in.

### Creating Registration Test Case

Let's start with user registration flow. First we need to define our target elements to fill out the registration form and the following GDPR one:

![Targets addressed in registration flow](/files/-LhteUTFL_yWl4Pyi3uz)

Now we can create a test case:

![User registration test case](/files/-LhGUcsP7hjgq2C9Xab6)

Here the description of test steps:

* we assign a dynamic template variable `TEST_EMAIL` by using template expression [counter()](https://docs.puppetry.app/template), meaning with every test run will get a new unique email address like **<joe+test1@gmail.com>**, **<joe+test2@gmail.com>** and so on
* we navigate to register page (given that we defined a template variable `BASE_URL`)
* we fill in email field from `TEST_EMAIL` variable
* we fill in username input with random dummy name generated by [faker template expression](https://docs.puppetry.app/template)
* we fill in password and password confirmation fields from earlier defined `TEST_PASSWORD` template variable
* we click submit button
* we wait until form submitted and the page with GDPR form loaded
* we make a screenshot
* we tick on consent checkboxes
* we click submit button

So we have registered the user. The next test case we can simply place next ot the first one. But think ahead - we my have multiple flows for logged-in state and which one we will need to start with login procedure. So we rather create a snippet for login and reuse it in future test cases

### Creating Login Snippet

Next we press **Snippets** in the main menu

![](/files/-LhKUbZcNfulFziTZuDj)

Snippets are quite similar to suites. They have local targets and test cases.

We start by defining targets for the [login form](https://docs.puppetry.app/examples)&#x20;

![Snippet's targets](/files/-LhteYkX9-Nb9nNCI6h9)

Now we just into **Snippets** tab and create the test case:

![Snippet's body](/files/-LhtebqfeI2B6isv0EOI)

As we are done with the snippet we can navigate back to the suite:

![](/files/-LhKVU37KbBdLz2SP7SG)

We create an test case "user gets logged in" and click under it to the **Add a reference** button

![](/files/-LhKVVvC2SI2kU6Pa939)

In the following modal window we select our recently created snippet:

![Adding a reference](/files/-LhGZ-kUmnE3TXWm0nrD)

After extending the test case with other test steps it looks like that:

![User gets logged in test case](/files/-LhKAOnFenEsOFa_5FUa)

During the test run first all the test steps of the snippet are executed, so user gets logged in. We wait until the page is ready (login form doesn't contain .categories selector, but landing page does). Now we can assert that page header changed (has #user-header-name).&#x20;

## Running Snippets with Diverse Input

In the example above we used `TEST_EMAIL` template variable defined during registration flow. But imagine that we have a number of already registered accounts  (let's say one is inactive, one is active, one is privileged) and we want sequencely log in with each one and assert that the application responds as intended. What we can do  is reusing **"user logs in"** snippet, but with diverse emails. Click on the **Edit** action next to the reference test step:

![](/files/-LhKW2lYViomIr_L-PRF)

You get a modal window that we know from the example below. Click to **Local Template Variables** to expand the template editing interface and add `TEST_EMAIL` variable:

![Reference local variables ](/files/-LhKIiuqBr6Nma4MhKdk)

Now you clone the reference and edit it again. Remove TEST\_EMAIL and add it again with a new value. As you run the tests the references will execute the snippet with the values we assigned.


# Running tests

When we are ready with our test suite we can run the tests. Click on **Run..** item in the main menu (or press **F6**).

![Run tests menu item](/files/-Lh_4MS0_BVJ9fXnADzm)

You get **Run reports** modal window

![Run reports modal window](/files/-Lh_4isNh4JBLSb0YVQK)

By default Puppetry runs tests in headless mode, meaning it launches Chromium in background. You can make to run the tests in Chrome browser with **Run in browser** switch:

![Chmonim options](/files/-Lh_57y6IMojqXU2vGKg)

This opens an input for [Chromium command line options](https://peter.sh/experiments/chromium-command-line-switches/)

&#x20;The modal window also allows you to select a [target environment](https://docs.puppetry.app/template):

![Selecting target environment](/files/-Lh_F9wkSHAae-ZuBKyV)

Finally we can select which suites we are going to run

After pressing **Run** button Puppetry opens panel **Test report** with test results. When test ran successfully we get a page like that:

![Successful test run ](/files/-Lh_Huv9P4Ovol73bklZ)

If we click on Open directory with generated screenshots link we can see generated folder named after the test case where we can find the screenshot:

![Generated screenshot](/files/-LhLfspmHAqA5yqYGgzv)

However if test failed the page look as follows:

![Errored test run](/files/-Lh_HxTy-iF-xQh72dOr)

If the error message doesn't clarify the failure enough, you can click on **Error details** link:

![Test run error details](/files/-Lh_HzwhzekG3ElMNu6M)


# Troubleshooting

On Linux you may run into "Failed to launch chrome" issue

![](/files/-LirOwb-tCrJE9xxksL8)

In order to protect the host environment from untrusted web content, Chrome uses [multiple layers of sandboxing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux_sandboxing.md). For this to work properly, the host should be configured first. If there's no good sandbox for Chrome to use we get this problem. You can find how to configure sandbox [here](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox). But actually if you trust the content of the application under test you can simply run with `--no-sandbox` option.

![](/files/-LirQ8PmcSoEEoOV6JBT)


# Exporting Tests for CI

Project suites can be exported as [Jest test project](https://jestjs.io/) and be ran in command line by, for example, Continuous Integration server.&#x20;

As we press **Ctrl+Shift-E** (⌘⇧E) or click on **File/Export Project for CI...** menu item:

![](/files/-Lh_M5h0f52GlwWzjdRl)

you get **Export Project** modal window:

![Export Project modal window](/files/-Lh_OnZBZunuUTD8tGSf)

It is quite  similar to [Test Reports ](https://docs.puppetry.app/running-tests) window. So we can choose target test environment, which suites to include  into Jest project and finally where to save the generated project.

After exporting we can jump to the given export directory and install npm dependencies:

```
npm install
```

then we run the tests:

```
npm test
```

We are expected to get output like that:

![Running Puppetry export in CLI](/files/-Lh_PBEsCtFWw0UmxBco)


# Template Expressions

Many of command/assertion parameters accept templates. So you can use template variables and expressions. For example, `page.goto("https://github.com/{{ USER }}/puppetry/")` or `TARGET.type("name{{ counter() }}")`

## Template variables

By using **Settings/Template Variables** panel we can define a set of variables per environment. For an instance, we can declare a separate target app URL for every environment (test.acme.com, stage.acme.com, [www.acme.com](http://www.acme.com)). Before running/exporting test project we specify the desired environment and the corresponding template tags will be replaced with the value (URL) given for that environment.

### Managing Variables

To edit template variables click on **Settings...** in the main menu:

![Settings in the main menu](/files/-LhKOcoIJMBcZO6mR6_A)

Under the table Template variables you will find a manageable table with variables (by default it's empty):

![Editing template variables](/files/-LhtfYBDHIRz4d5HQcA-)

You can use this UI the same way to do with targets, suits, and test cases. The difference is that the displayed variables belong to the selected environment:

![](/files/-LhKRoSTVQCnEoU-Pv-y)

As you add a new variable let's say to **test** environment it appears in all available environments. But when you update it's value, the value stays unique per environment.

Also note the **edit** action link next to the environment selector. When you click it you get a modal window where you can manage the list of environments:

![Editing environment list](/files/-LhKOlKjegZqHlF2L4CK)

After we assigned the variables and running the tests (or exporting them for CI) in the modal window we can choose what environment we target:

![Choose target environment](/files/-LhKSKTyBU-H60IEL5al)

### Template Syntax

#### Syntax

```
{{ VARIABLE_NAME }}
```

The tag makes Puppetry injecting the variable value assigned for the selected environment into the container string

#### Examples

```
{{ BASE_URL }}/api/{{ API_VERSION }}/method
```

{% hint style="info" %}
Template variables can be also assigned dynamically. E.g. you can use `page.assignVar` to define a variable by using expressions or `page.assignVarRemotely` to define a variable by using REST API ([learn more](https://docs.puppetry.app/testing-emails)).
{% endhint %}

## Template Expressions

Template expressions are basically functions that we may want to use for building template string.

Test step parameters supporting templates provided with a helper widget, which you can you to autofill the field:

![Template helper widget](/files/-LhKU2k4JJRqBMrO4ovS)

### Environment Variables

#### Syntax

```
{{ env( NAME ) }}
```

Function returns system [environment variable](https://en.wikipedia.org/wiki/Environment_variable) by a given name

#### Examples

```
{{ env( "SECRET" ) }}
```

### Counter

#### Syntax

```
{{ counter() }}
```

Function increments every time it called, but is unique per command/assertion&#x20;

#### Examples

```
joe.{{ counter() }}@acme.com
```

This template resolves in email addresses <joe.1@acme.com>, <joe.2@acme.com> and so on

### Iterate

#### Syntax

```
{{ iterate( LIST ) }}
```

Function iterates through a given list. It starts from the beginning as it reaches the end

#### Examples

```
{{ iterate(["sansas@got.com", "yarag@got.com", "gendryb@got.com"]) }}
```

This template resolves in <sansas@got.com>, <yarag@got.com>, <gendryb@got.com>, <sansas@got.com>, ...

### Random

#### Syntax

```
{{ random( LIST ) }}
```

Function picks a random value out of a given list.

#### Examples

```
{{ random(["sansas@got.com", "yarag@got.com", "gendryb@got.com"]) }}
```

### HtmlOf

#### Syntax

```
{{ htmlOf( TARGET ) }}
```

Function extracts content (innerHTML) of a given TARGET.

#### Examples

```
{{ htmlOf("FOO") }}
```

### AttributeOf

#### Syntax

```
{{ attributeOf( TARGET, ATTRIBUTE_NAME ) }}
```

Function retrieve attribute value of a given TARGET.

#### Examples

```
{{ attributeOf("FOO", "href") }}
```

### PropertyOf

#### Syntax

```
{{ propertyOf( TARGET, PROPERTY_NAME ) }}
```

Function retrieve property value of a given TARGET.

#### Examples

```
{{ propertyOf("FOO", "checked") }}
```

### Faker

#### Syntax

```
{{ fake( FAKER_METHOD, LOCALE ) }}
```

Function generates dummy data, by using [Faker.js methods](https://github.com/marak/Faker.js)

#### Examples

```
 {{ faker("address.streetSuffix", "en_GB") }}
```


# Version Control

Version control is a crucial part of development. Puppetry has an integrated [GIT client](https://git-scm.com/). Let's go together though a tutorial to examine how we can benefit of it.

## Working with Version History

First we take a look at how we can keep version history and navigate through it.

So we open a demo project and navigate to **Settings** application panel. There we click on **GIT** tab.

![Configuring GIT](/files/-LhtsMwLAK5nvyTOTeuU)

Here we provide test author name and email. This information will be used to sign project version.&#x20;

After saving the changes we go in the main menu and click on **File / GIT / Initialize**

![Initializing local repository](/files/-Lhtttpq__GUKdC5rN7J)

We've just created a local GIT repository for our project. There will be kept snapshots of all the committed versions. Let's see how it works.

We open a suite and jump to **Targets**. We add anew target `TEST_SELECTOR` with value `.ver1`

![](/files/-LhtuembW7Gb_40Jc9m8)

Next in the main menu we click on **File / GIT / Commit**

![Commit menu](/files/-Lhtv4G8_Kc-ssaU6vEv)

It opens **New Commit** modal window:

![Committing ver 1 ](/files/-LhtvIFbH52v6sYav9qS)

We name the commit as **ver. 1** and click **Commit** button. Thus we get the first committed version.

Now we change the value of the target:

![](/files/-LhtvrrlMxxGiP6zBqPO)

Then we commit the change (**File / GIT / Commit**).

![Committing ver 2](/files/-Lhtw5a_XXXcGjpBr4ey)

Well, we have at least two versions of the project, so it makes some history.  Let's see what we really have. Click on  **File / GIT / Checkout**

![Git Checkout menu](/files/-LhtxR6d2N2PThDbDb2f)

It opens the following modal window:

![Project local history](/files/-LhtwtzUYGzdLVijxnxB)

Here we can see our previously committed changes.  Now we just to the **ver. 1** by clicking on **Checkout** link.

![Browsing version history](/files/-LhtxO7TnZ1-WECO808g)

As we see `TEST_SELECTOR` has value `.ver1` of the previously saved version. As it says we are on a detached version. It's not the working one, so we can simply examine the contents and copy assets to paste into the head of history.

As we done we click on **Checkout master** button:&#x20;

![Checkout master button](/files/-Lhtxx78wpbwE282w2IG)

We've back to the working (last) version:

![](/files/-LhtybKD86BT0g81tD1D)

## Team Collaboration

Version control is especially good for collaboration. Let's imagine we have QA engineers Samwell Tarly and  Davos Seaworth. Sam is just committed a new version of the project and want to share it with Davos. For that guys need a remote repository. Let's take [Bitbucket](https://bitbucket.org) as service provider. So we register and create a new project:

![](/files/-LdyKf7-heeT5RF-CKK-)

That will bring us to the project page, which currently filled in with instruction to get started:

![Project page at Bitbucket](/files/-LhuT-iHOEg9MMEBaNO-)

Now we go back to Puppetry, open **Settings** application menu and jump to **GIT** tab. Next we scroll down to **Git Remote Repository** section:

![Configuring remote repository](/files/-LhuTNCM3pSpA8CGFIrB)

Here we need to provide the remote repository URL (from Bitbucket) and user credentials.  Now we can synchronize our local repository to the remote one. Click **File / GIT / Sync**&#x20;

![Sync with remote menu](/files/-LhuUbjWl7_YoZ3oAria)

It opens the following modal window:

![Sync with remote modal window](/files/-LhuUgVCH8WA52YXvovw)

As we confirm the synchronization we can refresh the page on Bitbucket:

![Project artefacts on remote repository](/files/-LhuUqqScJ1f7SaeEf4n)

We can see that our project artifacts now are available there.&#x20;

Now Davos opens Puppetry on his machine and configure GIt:

![Davos' GIT configuration](/files/-LhuVE7zgGhJlMHl0K9B)

He opens **File / GIT / Clone** in the main menu and gets the following modal window:

![Clone Project modal window](/files/-LhuU_N7EMIe1-SlxTyQ)

As he selects a destination and clicks on Clone, the version of Sam's project gets delivered to Davos' Puppetry.&#x20;

Well, let's really collaborate. Davos changes the target again:

![](/files/-LhuVspeEbN0Qs7IKLvJ)

and commits the changes. Then he synchronizes with the remote repository (**File / Git / Sync with remote** ). Sam also synchronizes and sees the changes from Davos arrived:

![](/files/-LhuWODH266CgMUK--xS)


# Testing Transactional Emails

Transactional email is a sort of email that require an action from the receiver side like in signup confirmation emails, password resets, purchase notifications and others. In order to automate testing of user flows involving transactional emails we need to be able to request the inbox contents from the testing tool. That can be achieved by using an email server with REST API or a specialized service such as [Sendgrid](https://sendgrid.com), [Mailgun](https://www.mailgun.com), [Email Yak](http://www.emailyak.com/), [Postmark](http://www.postmarkapp.com). These services are normally quite expensive, so alternatively we can develop a bridge one, which uses IMAP or, let’s say, Google API to access a public email server. Just to give it a try we can even go with Restmail.net, which is free and requires no registration or set up.

Puppetry provides `page.assignVarRemotely` command, which polls a given email server REST API until the desired email received (ot timeout). It retrieves the parsed the parsed value (e.g. activation link) and assigns it to a dynamic template variable accessible in all following test steps. The command can be configured to parse the received content.

## Testing Signup Flow&#x20;

To get a better grip on the idea we are going to create a simplified test project for[ imaginary ACME forum](https://docs.puppetry.app/examples) built with [NodeBB](https://nodebb.org/). So the test scenario would be to type in required fields on the registration form, submit the form and use the  link sent by email to activate the account.

### Template Variables

Let's open Puppetry, create a new project and navigate to **Settings**. There we define our template variables:

![Signup flow variables](/files/-LhtfPKB-6ZBLvS8949r)

What we need are `BASE_URL` (<http://localhost:4567/> NodeBB default one for development environment), which depends on testing environment and `TEST_PASSWORD` (fixture password used for registration) that once defined we can reuse across the project.&#x20;

### Test Targets

Now we can define the test targets for the flow:

![Signup flow targets](/files/-LhFGUUw7VfzW1AYDcdT)

The registration form ([see screenshot](https://docs.puppetry.app/examples)) has email, username, password, password confirmation inputs and submit button. We introduce these elements into Puppetry as `REG_EMAIL_INPUT`, `REG_USERNAME_INPUT`, `REG_PASSWORD_INPUT`, `REG_CON_PASSWORD_INPUT`, `REG_SUBMIT_BTN`. After submitting the form the application brings us to the GDPR form ([see screenshot](https://docs.puppetry.app/examples)), where we need to tick on consent checkboxes and click on Register button.  Those elements we refer as `REG_AGREE_EMAIL_CHECKBOX`, `REG_AGREE_DATA_CHECKBOX`, `REG_REGISTER_BTN`.

### Test Case

![Signup test case, part 1](/files/-LiILIXkJiqd6Dv66BbD)

First we need to ensure that we do no register an existing user. So we need to use a unique email address. That we achieve by dynamically  assigning a template variable `TEST_EMAIL` that will accept during test run iterating values like <test1@acme.com>, <test2@acme.com> and so on.

Next we use page.goto to navigate to the signup page.

We type in email address generated in `TEST_EMAIL`.

We type in username, dynamically built with faker expression.

We type in password and password confirmation by using `TEST_PASSWORD`  template variable, which we defined above.&#x20;

Finally we click on Submit button.

![Signup test case, part 2](/files/-LiINM6rGWy49-Hlz_M8)

That is supposed to bring us to the next page with the GDPR form.

So we wait until the page is ready

We make a screenshot.

We tick on the consent checkboxes.

We click on Register button.&#x20;

We wait until the sent email arrives to the inbox and assign the parsed activation link to template variable `ACTIVATION_LINK`.

The we follow the link and make a new screenshot.&#x20;

### Remote Service

Now we can proceed with any of the following examples

{% content-ref url="/pages/-LiIIGQpJ4ZCNOyobqYs" %}
[Example with Restmail.net](/master/testing-emails/example-with-restmail.net)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIINWgz9-8ov38y1a9" %}
[Example with Mailinator](/master/testing-emails/example-with-...)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIIRGKI2RbQ5uVlR0W" %}
[Example with IMAP bridge](/master/testing-emails/example-with-imap-bridge)
{% endcontent-ref %}

{% content-ref url="/pages/-LiIIX4abjMa2OBN441x" %}
[Example with Google API](/master/testing-emails/example-with-google-api)
{% endcontent-ref %}


# Example with Restmail.net

[Restmail.net](http://restmail.net/) is probably the simplest toll to start with. It's free, it's requires no registration, it allows to create dynamically inboxes, it exposes a REST API to read received emails. However all the sent messages are public.&#x20;

Let's take the previous test suite and modify it a bit. For Restmail.net we need user alias instead of email to check the inbox. So we can generate it like that.

![User alias for Restmail.net](/files/-LiDgHAJHk-1Av_S9oCV)

As you see we also changed `TEST_EMAIL` template variable generation. Now it build based on earlier created `TEST_USER_ALIAS` one.

Next we find `page.assignVarRemotely` command and update the URL for of of Restmail.net:

```
http://restmail.net/mail/{{ TEST_USER_ALIAS }}
```

What we need now is a custom parser function in `page.assignVarRemotely` :

```javascript
( json, payload ) => {
  const parseActivationLink = ( text ) => {
          const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
                res = text.match( re );
          return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
        },
        sentAt =  payload.sentAt,
        unseen = json.find( msg => new Date( msg.receivedAt ) > new Date( sentAt ) );

  if ( !unseen ) {
    return null;
  }

  return parseActivationLink( unseen.html );
}
```

The function has second parameter `payload` with `sentAt` property that contains sending event timestamp. Here we check if there are any messages received  later then the application under test sent the activation email. If any found we parse it for the activation link.

Our `page.assignVarRemotely` command configuration shall look now like that:

![page.assignVarRemotely configuration ](/files/-LiIP-V5ybN7WKbDU9N_)

Now we can save the changes and run the tests. On the generated screenshots we can see that a new account was created and activated.


# Example with Mailinator

Similar to [Restmail.net](http://restmail.net/) we need user name alias to use with the REST API. so we modify the first steps in the case case as follows:

![Assigning TEST\_USER\_ALIAS and TEST\_EMAIL](/files/-LiIyYpg-JJw8k2sPpTA)

Following [Mailinator API](https://manybrain.github.io/m8rdocs/#message-api) we need to set URL for `page.assignVarRemotely`:

```
https://api.mailinator.com/api/inbox?token={{ env( "EID_SECRET" ) }}&to={{ TEST_USER_ALIAS }}
```

![page.assignVarRemotely configuration for Mailinator (part 1)](/files/-LiIzsEM_hNQomumkFPz)

However [Mailinator](https://www.mailinator.com/) unlike [Restmail.net](http://restmail.net/)  doesn't allow to receive last message content with a single request. So we need to take advantage of optional `request function` command parameter:

So we alternate the default fetch logic of `page.assignVarRemotely` as follows:

```javascript
async ( url ) => {
   const inbox = await ( await fetch( url ) ).json(),
         msgHead = inbox.messages
           .reverse()
           .find( msg => msg.subject.includes( "Account Validation" ) ),
         fulMsg = await ( await fetch( `https://api.mailinator.com/api/message?token=YourAPIToken&id=${ msgHead.id }` ) ).json();
  return fulMsg.data.parts.map( p => p.body ).join();
}
```

Thus the command will read the listing and find a message with subject containing `"Account Validation"` string. Then it retrieves and returns the raw body of the matching mail. So the parse function may look like:

```javascript
( text ) => {
    const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
              res = text.match( re );
    return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
}
```

![page.assignVarRemotely configuration for Mailinator (part 2)](/files/-LiJ-iBkOEomJK7TUkzr)


# Example with IMAP bridge

Nowadays there are many specialized services providing a REST API to access your inbox remotely.  But it comes with an expense. If you want to obtain REST API  for free, you can simply use a bridge server connected to your email provider (e.g. Gmail) by IMAP or SMTP. Puppetry is distributed with an [example of a bridge to Gmail](https://github.com/dsheiko/puppetry/tree/master/examples/gmail-imap-bridge).&#x20;

### Setting up Gmail Bridge Demo

To setup and run the bridge demo we perform the following steps:

* download and extract content of the folder to an arbitrary location (e.g.   `/var/www/gmail-bridge` )
* install dependencies `npm install`
* define required [environment variables](https://en.wikipedia.org/wiki/Environment_variable):

```bash
EID_EMAIL=joe@gmail.com
EID_PASSWORD=gmail password
EID_SECRET=secret
EID_NODE_SERVER_PORT=3500
EID_NODE_SERVER_HOST=127.0.0.1
```

{% hint style="info" %}
To make it really work with Gmail, please, [enable 2-step verification](https://www.google.com/landing/2step/) and generate an [App password](https://security.google.com/settings/security/apppasswords). This password you shall use for `EID_PASSWORD`
{% endhint %}

* adjust the body of `parseActivationLink` function to parse the desired value (e.g. activation link) from email body
* start the server `npm start`

### Testing Signup Flow&#x20;

Gmail allows to have multiple user name aliases on the same account by using pattern: `username+VariableSuffix@gmail.com`. We can leverage this to emulate email server. So we change the `page.assignVar` for the following value:

```
joe+test{{ counter() }}@gmail.com
```

It is supposed to resolve in the email addresses like: <joe+test1@gmail.com>, <joe+test2@gmail.com> and so on.

The command `page.assignVarRemotely` takes in the following configuration:

![page.assignVarRemotely configuration](/files/-LhFM2kyaQ9EBckBdDJW)

Basically we  configure `page.assignVarRemotely` to request the bridge for `TEST_EMAIL` with intervals of 1 seconds and the timeout of 600 seconds. The bridge is expected to parse email body if any available for the activation link. In case of NodeBB it uses the following function:

```javascript
function parseActivationLink( text ) {
  const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
        res = text.match( re );
  return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
}
```

The parser function of command configuration simply proxies the value from a result such as`{ value: "ACTIVATION_LINK" }`. Here you can put the parsing logic if your bridge/API doesn't do the parsing. Any falsy return of this function causes the command to proceed polling.&#x20;

Note also that we use environment variable `EID_SECRET` to pass the api key to the bridge service.

As we run tests we get from the bridge an output like this one:

![Gmail Bridge output](/files/-LhFREC-bzGIEkt9P1G8)


# Example with Google API

In the previous example we used IMAP client to connect to Gmail inbox. That can be also done by using Gmail API.  You can find on GitHub a [quick start example](https://github.com/gsuitedevs/node-samples/tree/master/gmail/quickstart) by Google, which implements OAuth authorization and retrieves list of labels from user's mailbox.

The example app is surprisingly easy to set up. You just need to follow [the steps](https://developers.google.com/gmail/api/quickstart/nodejs). &#x20;

As you run the app first time it asks you to follow a printed URL to Google Services where you will be prompted to register the app. At the end you receive a code, which hand back to the application.

But we do not need labels, but last received email addressed to a given user name alias. Well we can modify a bit  [index.js](https://github.com/gsuitedevs/node-samples/blob/master/gmail/quickstart/index.js). First let's get rid of `fs.readFile('credentials.json', (err, content) => ...` block. We rather get OAuth client with an asynchronous function instead of coping with callback hell:

```javascript
/**
 * @returns {Promise} 
 */
function getOAuth2Client() {
  const credentials = fs.readFileSync( "credentials.json", "utf8" );
  return new Promise(( resolve ) => {
    authorize(JSON.parse( credentials ), ( auth ) => {
      resolve( auth );
    });
  });
}
```

Now implement a function that accepts [a filter query](https://support.google.com/mail/answer/7190?hl=en)  and returns either raw body of the first matching the query message or nothing if no matches found:

```javascript
/**
 * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
 * @param {string} q - Google searchbox query
 * @returns {Promise} 
 */
async function getUnreadEmail( auth, q ) {
  const gmail = google.gmail({ version: 'v1', auth }),
        userId = "me";

  try {
    const listRsp = await gmail.users.messages.list({
      userId,
      q
    });

    if ( !listRsp.data.messages ) {
      return;
    }

    const [ msg ] = listRsp.data.messages,

          msgRsp  = await gmail.users.messages.get({
            userId,
            id: msg.id,
            format: "raw"
          }),

          rawBody = Buffer.from( msgRsp.data.raw || "", "base64" )
            .toString( "utf8" );

    return rawBody;
  } catch ( e ) {
    console.error( e );
  }
}
```

Well, now we can use these functions from the bridge code like that:

```javascript
const oAuth2Client = await getOAuth2Client();
const rawBody = await getUnreadEmail( oAuth2Client, "to:joe+test1 is:unread" );
// Parse rawBody for the activation link
```


# Testing Forms with Captcha

The whole point of Captcha (e.g. [Recaptcha](https://www.google.com/recaptcha/intro/v3.html)) to ensure the forms filled out by a human. So we cannot simply bypass it with automating tests. However we can still test the forms if we persuade the application under test that we are a trusted source. For example, the application uses a token-based authentication. From Puppetry side by using `page.runjs` command we can login under a test account and provide to the application the received token as certificate. The application checks if the token valid and disable Captcha for the session (obviously not on the production environment). Let's say we feed to `page.runjs` the following function:

```javascript
const rsp = await fetch( "https://rest-api-sandbox.local/api/v1/login", {
    method: 'POST',         
    body: JSON.stringify({
      email: process.env.QA_EMAIL
      password: process.env.QA_PASSWORD
    })
});
ENV.SESSION_TOKEN = rsp.json().token;
```

{% hint style="info" %}
As you can see define a dynamic template variable  as `ENV.<VARIABLE NAME>`. The same way template variables can be accessed in the code .&#x20;
{% endhint %}

Next we can use `page.setCookie` command to pass the retrieved token to the app server:

![](/files/-LhG7dAUYg2IS86LxIEP)

The application server validates the token if any available in TRUSTED\_CLIENT\_TOKEN cookie and disables the captcha for the session.


# Command API

API available via page.runjs command

## Puppeteer methods

* **bs** - object representing browsing session
  * **browser** - instance of [Browser](https://pptr.dev/#?product=Puppeteer\&version=v1.15.0\&show=api-class-browser), which is being created in test `setup` with configuration provided in Puppetry app
  * **page** - instance of [Page](https://pptr.dev/#?product=Puppeteer\&version=v1.15.0\&show=api-class-page) (alias of `bs.browser.page`)
  * **target(**`element: ElementHandle`**)** - helpers&#x20;
    * `getProp(prop: string): Promise<string>` - obtain element property value
    * `getAttr(attr: string): Promise<string>` -  obtain element attribute value
    * `isVisible(): Promise<boolean>` - find out f element visible
    * `select(value: string): Promise<void>` - set a value on select element

Examples:

```javascript
// Navigating to "https://dsheiko.github.io/react-html5-form/"
  await bs.page.goto( "https://dsheiko.github.io/react-html5-form/", {
    timeout: 30000,
    waitUntil: "load"
  });
  
  // Emulating device "Blackberry PlayBook landscape"
  await bs.page.emulate( devices[ "Blackberry PlayBook landscape" ] );
  
  const title = await bs.page.title();
```

### Working with targets

Assuming we have targets `SUBMIT_BTN`, `EMAIL_INPUT` defined with Puppetry app

```javascript
// Emulating mouse click
await ( await SUBMIT_BTN() ).click();   
// Emulating user input
await ( await EMAIL_INPUT() ).type( "json@snow.got" );
// Obtaining "name" attribute's value of EMAIL_INPUT 
const name = await bs.target( await EMAIL_INPUT() ).getAttr( "name" );       
```

## Jest tools

Please find available API in [official documentation of Jest framework](https://jestjs.io/docs/en/getting-started). Most demanded tool for the context is expect. In the scope of the test it is extended with the following methods:

* **expect( result )**
  * `toBeOk( expectedValue, assertionSource)` - assert the received value is truthy
  * `toBeEqual( expectedValue, assertionSource)` -  assert the received value equals expected one
  * `toIncludeSubstring(expectedSubstring, assertionSource)` - assert the received string contains expected one
  * `toPassCondition(operator, valueToCompare, assertionSource)` - assert the received value satisfies a given Puppetry condition
  * `toMatchBoundingBoxSnapshot(snapshot, assertionSource)` - assert the received bounding box matches a given snapshot
  * `toMatchPosition(position, target, counterpart, assertionSource)` - assert the received position object matches a given one

Examples:

```javascript
// Asserting that page HTML satisfies the given constraint
result = await bs.page.content();       
expect( result ).toIncludeSubstring( "demo", "page.assertContent" );

// Asserting that window scroll offset satisfies the given constraint
result = await ( await bs.page.evaluate( () => window.scrollX ) );       
expect( result ).toPassCondition( "lt", 10000, "page.assertScroll" );

// Asserting that EMAIL_INPUT element is visible
result = await bs.target( await EMAIL_INPUT() ).isVisible();       
expect( result ).toBeOk( "EMAIL_INPUT.assertVisible" );

// Asserting that the bounding box of the element satisfies the given constraint
result = await ( await SUBMIT_BTN() ).boundingBox();    
expect( result ).toMatchBoundingBoxSnapshot( {
       "xOperator": "gt",
       "xValue": 0,
       "yOperator": "gt",
       "yValue": 0,
       "wOperator": "gt",
       "wValue": 0,
       "hOperator": "gt",
       "hValue": 0
     }, "SUBMIT_BTN.assertBoundingBox" );

 // Asserting that the bounding box of the element satisfies the given constraint
result = {
        target: await ( await DAY_SELECT() ).boundingBox(),
        counterpart: await ( await MONTH_SELECT() ).boundingBox()
      };       
expect( result ).toMatchPosition( "left", "DAY_SELECT", "MONTH_SELECT", "DAY_SELECT.assertPosition" );
```

## Helpers

Helpers are functions available in specification (suite) scope

* **util**&#x20;
  * **png** - build screenshot option&#x20;
  * **pollForValue** - poll for value (used by `page.assignVarRemotely`)
  * **exp** - template expressions functions
* **fetch** - [Node.js-implementation](https://www.npmjs.com/package/node-fetch) of window\.fetch
* **localStorage** - [Node.js-implementation](https://www.npmjs.com/package/node-localstorage) of localStorage

`png( title, options ): Object` - build screenshot options

Example:

```javascript
await bs.page.screenshot( util.png( "Just for test", {
  "fullPage":true
}));
```

This command saves screenshot in `./screenshots/All--available--methods/Just-for-test.png` given that test title is *All available methods*

## Node.js

In the test scope one can access [Node.js modules](https://nodejs.org/en/).


# Test Application

## ACME Forum&#x20;

ACME forum is an imaginary application built with [NodeBB](https://nodebb.org/) and used as an example test application in this documentation.

### RWD

User navigates to landing page `http://localhost:4567/` on **desktop** &#x20;

![ACME forum landing page on desktop](/files/-LhL9i3pnszz52EUiCil)

Observe: main menu and teasers are visible

User navigates the landing page on **mobile (iPhone 8)**

![ACME forum landing page on mobile](/files/-LhL9kloLNT5-PUjemTW)

Observe: main menu and teasers are hidden

### Signup flow

User navigates to `http://localhost:4567/register` and fill out the form

![ACME forum registration form](/files/-LhFGJQ9ofDiF8a1KqIQ)

User  ticks on consent checkboxes on the following form

![](/files/-LhFGPeIiWDQTsiA1Nel)

User click on **Submit** button.

Observe: the user is registered

{% hint style="info" %}
When **Require Email Confirmation** switch in admin/settings/user panel is set ON the flow extends with account activation flow ([learn more](https://docs.puppetry.app/testing-emails))
{% endhint %}

### Login flow

User navigates to `http://localhost:4567/login` and fill out the form

![ACME forum login form](/files/-LhJpPOGuvXLIbnCimBE)

and submit click **Login** button.

Observe: ACME header changed, now it contains user's picture.

![](/files/-LhK-vNrxO-1Ce8aFCcc)


