Skip to content

Commit

Permalink
Aligned repository structure with WDIO recommendation
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Oct 10, 2023
1 parent 61b95a2 commit 76e7523
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 74 deletions.
1 change: 1 addition & 0 deletions serenity.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
serenity.project.name=serenity-js-mocha-webdriverio-template
1 change: 0 additions & 1 deletion src/github/index.ts

This file was deleted.

35 changes: 0 additions & 35 deletions src/serenity/Homepage.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/serenity/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Ensure, equals } from '@serenity-js/assertions';
import { Task } from '@serenity-js/core';
import { GetRequest, LastResponse, Send } from '@serenity-js/rest';
import { Ensure, equals } from '@serenity-js/assertions'
import { Task } from '@serenity-js/core'
import { GetRequest, LastResponse, Send } from '@serenity-js/rest'

/**
* Learn more about API testing with Serenity/JS
* https://serenity-js.org/handbook/api-testing/
*/
export class GitHubStatus {
private static readonly baseApiUrl = 'https://www.githubstatus.com/api/v2/';
private static readonly statusJson = this.baseApiUrl + 'status.json';
private static readonly baseApiUrl = 'https://www.githubstatus.com/api/v2/'
private static readonly statusJson = this.baseApiUrl + 'status.json'

static ensureAllSystemsOperational = () =>
Task.where(`#actor ensures all GitHub systems are operational`,
Send.a(GetRequest.to(this.statusJson)),
Ensure.that(LastResponse.status(), equals(200)),
Ensure.that(
LastResponse.body<StatusJSON>().status.description.describedAs('GitHub Status'),
LastResponse.body<StatusResponse>().status.description.describedAs('GitHub Status'),
equals('All Systems Operational')
),
)
Expand All @@ -25,16 +25,16 @@ export class GitHubStatus {
* Interfaces describing a simplified response structure returned by the GitHub Status Summary API:
* https://www.githubstatus.com/api/v2/summary.json
*/
interface StatusJSON {
interface StatusResponse {
page: {
id: string;
name: string;
url: string;
time_zone: string;
updated_at: string;
};
id: string
name: string
url: string
time_zone: string
updated_at: string
}
status: {
indicator: string;
description: string;
};
indicator: string
description: string
}
}
99 changes: 99 additions & 0 deletions test/serenity/todo-list-app/TodoList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { contain, Ensure, equals, includes, isGreaterThan } from '@serenity-js/assertions'
import { type Answerable, Check, d, type QuestionAdapter, Task, Wait } from '@serenity-js/core'
import { By, Enter, ExecuteScript, isVisible, Key, Navigate, Page, PageElement, PageElements, Press, Text } from '@serenity-js/web'

import { TodoListItem } from './TodoListItem'

export class TodoList {

// Public API captures the business domain-focused tasks
// that an actor interacting with a TodoList app can perform

static createEmptyList = () =>
Task.where('#actor creates an empty todo list',
Navigate.to('https://todo-app.serenity-js.org/'),
Ensure.that(
Page.current().title().describedAs('website title'),
equals('Serenity/JS TodoApp'),
),
Wait.until(this.newTodoInput(), isVisible()),
TodoList.emptyLocalStorageIfNeeded(),
)

static emptyLocalStorageIfNeeded = () =>
Task.where('#actor empties local storage if needed',
Check.whether(TodoList.persistedItems().length, isGreaterThan(0))
.andIfSo(
TodoList.emptyLocalStorage(),
Page.current().reload(),
)
)

static createListContaining = (itemNames: Array<Answerable<string>>) =>
Task.where(`#actor starts with a list containing ${ itemNames.length } items`,
TodoList.createEmptyList(),
...itemNames.map(itemName => this.recordItem(itemName))
)

static recordItem = (itemName: Answerable<string>) =>
Task.where(d `#actor records an item called ${ itemName }`,
Enter.theValue(itemName).into(this.newTodoInput()),
Press.the(Key.Enter).in(this.newTodoInput()),
Wait.until(Text.ofAll(this.items()), contain(itemName)),
)

static markAsCompleted = (itemNames: Array<Answerable<string>>) =>
Task.where(d`#actor marks the following items as completed: ${ itemNames }`,
...itemNames.map(itemName => TodoListItem.markAsCompleted(this.itemCalled(itemName)))
)

static markAsOutstanding = (itemNames: Array<Answerable<string>>) =>
Task.where(d`#actor marks the following items as outstanding: ${ itemNames }`,
...itemNames.map(itemName => TodoListItem.markAsOutstanding(this.itemCalled(itemName)))
)

static outstandingItemsCount = () =>
Text.of(PageElement.located(By.tagName('strong')).of(this.outstandingItemsLabel()))
.as(Number)
.describedAs('number of items left')

// Private API captures ways to locate interactive elements and data transformation logic.
// Private API supports the public API and is not used in the test scenario directly.

private static itemCalled = (name: Answerable<string>) =>
this.items()
.where(Text, includes(name))
.first()
.describedAs(d`an item called ${ name }`)

private static outstandingItemsLabel = () =>
PageElement.located(By.css('.todo-count'))
.describedAs('items left counter')

private static newTodoInput = () =>
PageElement.located(By.css('.new-todo'))
.describedAs('"What needs to be done?" input box')

private static items = () =>
PageElements.located(By.css('.todo-list li'))
.describedAs('displayed items')

private static persistedItems = () =>
Page.current()
.executeScript(`
return window.localStorage['serenity-js-todo-app']
? JSON.parse(window.localStorage['serenity-js-todo-app'])
: []
`).describedAs('persisted items') as QuestionAdapter<PersistedTodoItem[]>

private static emptyLocalStorage = () =>
Task.where('#actor empties local storage',
ExecuteScript.sync(`window.localStorage.removeItem('serenity-js-todo-app')`)
)
}

interface PersistedTodoItem {
id: number;
name: string;
completed: boolean;
}
36 changes: 36 additions & 0 deletions test/serenity/todo-list-app/TodoListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { contain, not } from '@serenity-js/assertions'
import { Check, d, QuestionAdapter, Task } from '@serenity-js/core'
import { By, Click, CssClasses, PageElement } from '@serenity-js/web'

export class TodoListItem {

// Public API captures the business domain-focused tasks
// that an actor interacting with a TodoListItem app can perform

static markAsCompleted = (item: QuestionAdapter<PageElement>) =>
Task.where(d `#actor marks ${ item } as completed`,
Check.whether(CssClasses.of(item), not(contain('completed')))
.andIfSo(this.toggle(item)),
)

static markAsOutstanding = (item: QuestionAdapter<PageElement>) =>
Task.where(d `#actor marks ${ item } as outstanding`,
Check.whether(CssClasses.of(item), contain('completed'))
.andIfSo(this.toggle(item)),
)

static toggle = (item: QuestionAdapter<PageElement>) =>
Task.where(d `#actor toggles the completion status of ${ item }`,
Click.on(
TodoListItem.toggleButton().of(item),
),
)

// Private API captures ways to locate interactive elements and data transformation logic.
// Private API supports the public API and is not used in the test scenario directly.

private static toggleButton = () =>
PageElement
.located(By.css('input.toggle'))
.describedAs('toggle button')
}
42 changes: 25 additions & 17 deletions spec/example.spec.ts → test/specs/example.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Ensure, equals } from '@serenity-js/assertions';
import { actorCalled } from '@serenity-js/core';
import { By, Navigate, PageElement, Text } from '@serenity-js/web';
import { describe, it } from 'mocha';
import { Ensure, equals } from '@serenity-js/assertions'
import { actorCalled } from '@serenity-js/core'
import { By, Navigate, PageElement, Text } from '@serenity-js/web'

import { GitHubStatus } from '../src/github';
import { Homepage } from '../src/serenity';
import { GitHubStatus } from '../serenity/github-api/GitHubStatus'
import { TodoList } from '../serenity/todo-list-app/TodoList'

/**
* Serenity/JS Screenplay Pattern test scenarios use one or more actors to represent people and external systems
Expand Down Expand Up @@ -40,7 +39,7 @@ describe('Serenity/JS Website', () => {
equals('Start automating 🚀')
),
)
});
})

/**
* This is a more advanced example of a Serenity/JS Screenplay Pattern test scenario.
Expand All @@ -51,28 +50,37 @@ describe('Serenity/JS Website', () => {
*
* In this scenario, rather than list all the interactions in the test itself, we use:
* - API Actions Class Pattern to group tasks concerning interacting with the GitHubStatus API
* - Page Objects Pattern to group tasks and questions concerning interacting with the Serenity/JS Homepage
* - Screenplay Pattern flavour of Page Objects to group tasks and questions concerning interacting with the Serenity/JS Todo List app
*
* Note that apart from strongly encouraging the Screenplay Pattern,
* Serenity/JS doesn't require you to organise your code in any particular way.
* This gives you the freedom to choose patterns and abstractions that work best for you, your team,
* and reflect the domain of your system under test.
*/
it(`tells people what the framework can help them do`, async () => {
it(`offers examples to help you practice test automation`, async () => {

// You can use API interactions to manage test data, or to ensure services are up and running before performing any UI checks.
// Since Serenity/JS website is deployed to GitHub Pages, before interacting with the website we ensure that GitHub is up and running.
// Since Serenity/JS demo website is deployed to GitHub Pages,
// before interacting with the website we ensure that GitHub itself is up and running.
await actorCalled('Apisitt').attemptsTo(
GitHubStatus.ensureAllSystemsOperational(),
);
)

// Once we know the system is up and running, Wendy can proceed with the web-based scenario.
await actorCalled('Wendy').attemptsTo(
Homepage.open(),
TodoList.createListContaining([
`Buy dog food`,
`Feed the dog`,
`Book a vet's appointment`,
]),
TodoList.markAsCompleted([
`Buy dog food`,
`Feed the dog`,
]),
Ensure.that(
Homepage.heroBannerHeadingText(),
equals('Enable collaborative test automation at any scale!')
TodoList.outstandingItemsCount(),
equals(1)
),
);
});
});
)
})
})
11 changes: 7 additions & 4 deletions wdio.conf.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {WebdriverIOConfig} from '@serenity-js/webdriverio';

// Run tests in headless mode on CI and non-headless otherwise
// Set to true/false to override
const headless = Boolean(process.env.CI);

export const config: WebdriverIOConfig = {

// =========================
Expand All @@ -26,7 +30,7 @@ export const config: WebdriverIOConfig = {
]
},

headless: true,
headless,
// You can output the logs to a file to avoid cluttering the console
// outputDir: 'target/logs',

Expand All @@ -45,7 +49,7 @@ export const config: WebdriverIOConfig = {
// then the current working directory is where your `package.json` resides, so `wdio`
// will be called from there.
specs: [
'./spec/**/*.spec.ts',
'./test/specs/**/*.ts'
],
// Patterns to exclude.
exclude: [
Expand Down Expand Up @@ -98,10 +102,9 @@ export const config: WebdriverIOConfig = {
'--allow-file-access',
'--disable-infobars',
'--ignore-certificate-errors',
'--headless',
'--disable-gpu',
'--window-size=1024x768',
]
].concat(headless ? ['--headless'] : []),
}
}],
//
Expand Down

0 comments on commit 76e7523

Please sign in to comment.