@wdio/cli 9.0.0 → 9.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/build/commands/config.d.ts.map +1 -1
  2. package/build/index.js +5 -3
  3. package/build/templates/exampleFiles/browser/Component.css.ejs +121 -0
  4. package/build/templates/exampleFiles/browser/Component.lit.ejs +154 -0
  5. package/build/templates/exampleFiles/browser/Component.lit.test.ejs +24 -0
  6. package/build/templates/exampleFiles/browser/Component.preact.ejs +28 -0
  7. package/build/templates/exampleFiles/browser/Component.preact.test.ejs +59 -0
  8. package/build/templates/exampleFiles/browser/Component.react.ejs +29 -0
  9. package/build/templates/exampleFiles/browser/Component.react.test.ejs +58 -0
  10. package/build/templates/exampleFiles/browser/Component.solid.ejs +28 -0
  11. package/build/templates/exampleFiles/browser/Component.solid.test.ejs +58 -0
  12. package/build/templates/exampleFiles/browser/Component.stencil.ejs +43 -0
  13. package/build/templates/exampleFiles/browser/Component.stencil.test.ejs +45 -0
  14. package/build/templates/exampleFiles/browser/Component.svelte.ejs +47 -0
  15. package/build/templates/exampleFiles/browser/Component.svelte.test.ejs +58 -0
  16. package/build/templates/exampleFiles/browser/Component.vue.ejs +34 -0
  17. package/build/templates/exampleFiles/browser/Component.vue.test.ejs +62 -0
  18. package/build/templates/exampleFiles/browser/standalone.test.ejs +13 -0
  19. package/build/templates/exampleFiles/cucumber/step_definitions/steps.js.ejs +55 -0
  20. package/build/templates/exampleFiles/mochaJasmine/test.e2e.js.ejs +11 -0
  21. package/build/templates/exampleFiles/pageobjects/login.page.js.ejs +45 -0
  22. package/build/templates/exampleFiles/pageobjects/page.js.ejs +17 -0
  23. package/build/templates/exampleFiles/pageobjects/secure.page.js.ejs +20 -0
  24. package/build/templates/exampleFiles/serenity-js/common/config/serenity.properties.ejs +1 -0
  25. package/build/templates/exampleFiles/serenity-js/common/serenity/github-api/GitHubStatus.ts.ejs +41 -0
  26. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoList.ts.ejs +100 -0
  27. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoListItem.ts.ejs +36 -0
  28. package/build/templates/exampleFiles/serenity-js/cucumber/step-definitions/steps.ts.ejs +37 -0
  29. package/build/templates/exampleFiles/serenity-js/cucumber/support/parameter.config.ts.ejs +18 -0
  30. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/completing_items.feature.ejs +23 -0
  31. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/narrative.md.ejs +17 -0
  32. package/build/templates/exampleFiles/serenity-js/jasmine/example.spec.ts.ejs +86 -0
  33. package/build/templates/exampleFiles/serenity-js/mocha/example.spec.ts.ejs +88 -0
  34. package/build/templates/snippets/afterTest.ejs +20 -0
  35. package/build/templates/snippets/capabilities.ejs +57 -0
  36. package/build/templates/snippets/cucumber.ejs +50 -0
  37. package/build/templates/snippets/electronTest.js.ejs +7 -0
  38. package/build/templates/snippets/jasmine.ejs +20 -0
  39. package/build/templates/snippets/macosTest.js.ejs +11 -0
  40. package/build/templates/snippets/mocha.ejs +14 -0
  41. package/build/templates/snippets/reporters.ejs +14 -0
  42. package/build/templates/snippets/serenity.ejs +18 -0
  43. package/build/templates/snippets/services.ejs +18 -0
  44. package/build/templates/snippets/testWithPO.js.ejs +22 -0
  45. package/build/templates/snippets/testWithoutPO.js.ejs +19 -0
  46. package/build/templates/snippets/vscodeTest.js.ejs +9 -0
  47. package/build/templates/wdio.conf.tpl.ejs +416 -0
  48. package/build/utils.d.ts.map +1 -1
  49. package/package.json +4 -4
@@ -0,0 +1,45 @@
1
+ import { h } from '@stencil/core'
2
+ import { render } from '@wdio/browser-runner/stencil'
3
+ import { $, expect } from '@wdio/globals'
4
+
5
+ import { MyElement } from './Component.js'
6
+
7
+ describe('Stencil component testing', () => {
8
+ it('should increment value on click automatically', async () => {
9
+ await render({
10
+ components: [MyElement],
11
+ autoApplyChanges: true,
12
+ template: () => (
13
+ <my-element count={42}>WebdriverIO Component Testing</my-element>
14
+ )
15
+ })
16
+
17
+ const button = await $('my-element').$('button')
18
+ await expect(button).toHaveText('count is 42')
19
+
20
+ await button.click()
21
+ await button.click()
22
+
23
+ await expect(button).toHaveText('count is 44')
24
+ })
25
+
26
+ it('should increment value on click after flush', async () => {
27
+ const { flushAll } = await render({
28
+ components: [MyElement],
29
+ template: () => (
30
+ <my-element count={42}>WebdriverIO Component Testing</my-element>
31
+ )
32
+ })
33
+
34
+ const button = await $('my-element').$('button')
35
+ await expect(button).toHaveText('count is 42')
36
+
37
+ await button.click()
38
+ await button.click()
39
+ flushAll()
40
+
41
+ await expect(button).toHaveText('count is 44')<%-
42
+ answers.includeVisualTesting ? `
43
+ await expect(button).toMatchElementSnapshot('counterButton')` : '' %>
44
+ })
45
+ })
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ let count = 0
3
+ const increment = () => {
4
+ count += 1
5
+ }
6
+ </script>
7
+
8
+ <main id="root">
9
+ <div>
10
+ <a href="https://webdriver.io/docs/component-testing" target="_blank">
11
+ <img src="https://webdriver.io/assets/images/robot-3677788dd63849c56aa5cb3f332b12d5.svg" className="logo"
12
+ alt="WebdriverIO logo" />
13
+ </a>
14
+ </div>
15
+ <h1>WebdriverIO Component Testing</h1>
16
+
17
+ <div class="card">
18
+ <button on:click={increment}>
19
+ count is {count}
20
+ </button>
21
+ <p>
22
+ Edit <code>src/Component.test.<%- answers.isUsingTypeScript ? `ts` : 'js' %></code> and save to test HMR
23
+ </p>
24
+ </div>
25
+
26
+ <p class="read-the-docs">
27
+ Click on the Vite and Svelte logos to learn more
28
+ </p>
29
+ </main>
30
+
31
+ <style>
32
+ .logo {
33
+ height: 6em;
34
+ padding: 1.5em;
35
+ will-change: filter;
36
+ transition: filter 300ms;
37
+ }
38
+ .logo:hover {
39
+ filter: drop-shadow(0 0 2em #646cffaa);
40
+ }
41
+ .logo.svelte:hover {
42
+ filter: drop-shadow(0 0 2em #ff3e00aa);
43
+ }
44
+ .read-the-docs {
45
+ color: #888;
46
+ }
47
+ </style>
@@ -0,0 +1,58 @@
1
+ <%
2
+ const harnessImport = answers.installTestingLibrary
3
+ ? `import { render, fireEvent } from '@testing-library/svelte'`
4
+ : ``
5
+ const renderCommand = answers.installTestingLibrary
6
+ ? `render(ExampleComponent)`
7
+ : `new ExampleComponent({ target: container, props: {} })`
8
+ %>
9
+ import { $, expect } from '@wdio/globals'
10
+ <%- harnessImport %>
11
+ <% if (answers.installTestingLibrary) { %>
12
+ import * as matchers from '@testing-library/jest-dom/matchers'
13
+ expect.extend(matchers)
14
+ <% } %>
15
+ import ExampleComponent from './Component.svelte'
16
+ import './Component.css'
17
+
18
+ describe('Svelte Component Testing', () => {
19
+ <% if (answers.installTestingLibrary) { %>
20
+ it('should test component with Testing Library', async () => {
21
+ const { getByText } = render(ExampleComponent)
22
+
23
+ const component = getByText(/count is 0/i)
24
+ expect(component).toBeInTheDocument()
25
+
26
+ await fireEvent.click(component)
27
+ await fireEvent.click(component)
28
+
29
+ expect(getByText(/count is 2/i)).toBeInTheDocument()
30
+ })
31
+ <% } else { %>
32
+ let container<%- answers.isUsingTypeScript ? `: Element` : '' %>
33
+
34
+ beforeEach(() => {
35
+ container = document.createElement('div')
36
+ document.body.appendChild(container)
37
+ })
38
+
39
+ afterEach(() => {
40
+ container?.remove()
41
+ })
42
+ <% } %>
43
+
44
+ it('should test component with WebdriverIO', async () => {
45
+ <%- renderCommand %>
46
+
47
+ const component = await $('button*=count is')
48
+ await expect(component).toBePresent()
49
+ await expect(component).toHaveText('count is 0')
50
+
51
+ await component.click()
52
+ await component.click()
53
+
54
+ await expect(component).toHaveText('count is 2')<%-
55
+ answers.includeVisualTesting ? `
56
+ await expect(component).toMatchElementSnapshot('counterButton')` : '' %>
57
+ })
58
+ })
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ defineProps<{ msg: string }>()
5
+
6
+ const count = ref(0)
7
+ </script>
8
+
9
+ <template>
10
+ <div id="root">
11
+ <div>
12
+ <a href="https://webdriver.io/docs/component-testing" target="_blank">
13
+ <img src="https://webdriver.io/assets/images/robot-3677788dd63849c56aa5cb3f332b12d5.svg" className="logo"
14
+ alt="WebdriverIO logo" />
15
+ </a>
16
+ </div>
17
+ <h1>{{ msg }}</h1>
18
+
19
+ <div class="card">
20
+ <button type="button" @click="count++">count is {{ count }}</button>
21
+ <p>
22
+ Edit <code>src/Component.test.<%- answers.isUsingTypeScript ? `ts` : 'js' %></code> and save to test HMR
23
+ </p>
24
+ </div>
25
+
26
+ <p class="read-the-docs">Click on the WebdriverIO logo to learn more</p>
27
+ </div>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .read-the-docs {
32
+ color: #888;
33
+ }
34
+ </style>
@@ -0,0 +1,62 @@
1
+ <%
2
+ const harnessImport = answers.installTestingLibrary
3
+ ? `import { render, fireEvent } from '@testing-library/vue'`
4
+ : `import { createApp } from 'vue'`
5
+ const renderCommand = answers.installTestingLibrary
6
+ ? `render(ExampleComponent, { props: { msg: 'WebdriverIO Component Testing' } })`
7
+ : `createApp(ExampleComponent, { msg: 'WebdriverIO Component Testing' }).mount(container)`
8
+ %>
9
+ import { $, expect } from '@wdio/globals'
10
+ <%- harnessImport %>
11
+ <% if (answers.installTestingLibrary) { %>
12
+ import * as matchers from '@testing-library/jest-dom/matchers'
13
+ expect.extend(matchers)
14
+ <% } %>
15
+ import ExampleComponent from './Component.vue'
16
+ import './Component.css'
17
+
18
+ describe('Vue Component Testing', () => {
19
+ <% if (answers.installTestingLibrary) { %>
20
+ it('should test component with Testing Library', async () => {
21
+ // The render method returns a collection of utilities to query your component.
22
+ const { getByText } = render(ExampleComponent, {
23
+ props: { msg: 'WebdriverIO Component Testing' }
24
+ })
25
+
26
+ const component = getByText(/count is 0/i)
27
+ expect(component).toBeInTheDocument()
28
+
29
+ await fireEvent.click(component)
30
+ await fireEvent.click(component)
31
+
32
+ expect(getByText(/count is 2/i)).toBeInTheDocument()
33
+ })
34
+ <% } else { %>
35
+ let container<%- answers.isUsingTypeScript ? `: Element` : '' %>
36
+
37
+ beforeEach(() => {
38
+ container = document.createElement('div')
39
+ container.setAttribute('id', 'app')
40
+ document.body.appendChild(container)
41
+ })
42
+
43
+ afterEach(() => {
44
+ container?.remove()
45
+ })
46
+ <% } %>
47
+
48
+ it('should test component with WebdriverIO', async () => {
49
+ <%- renderCommand %>
50
+
51
+ const component = await $('button*=count is')
52
+ await expect(component).toBePresent()
53
+ await expect(component).toHaveText('count is 0')
54
+
55
+ await component.click()
56
+ await component.click()
57
+
58
+ await expect(component).toHaveText('count is 2')<%-
59
+ answers.includeVisualTesting ? `
60
+ await expect(component).toMatchElementSnapshot('counterButton')` : '' %>
61
+ })
62
+ })
@@ -0,0 +1,13 @@
1
+ import { $, expect } from '@wdio/globals'
2
+
3
+ describe('WebdriverIO Component Testing', () => {
4
+ it('should be able to render to the DOM and assert', async () => {
5
+ const component = document.createElement('button')
6
+ component.innerHTML = 'Hello World!'
7
+ document.body.appendChild(component)
8
+
9
+ await expect($('aria/Hello World!')).toBePresent()
10
+ component.remove()
11
+ await expect($('aria/Hello World!')).not.toBePresent()
12
+ })
13
+ })
@@ -0,0 +1,55 @@
1
+ <%- answers.isUsingTypeScript || answers.esmSupport
2
+ ? "import { Given, When, Then } from '@wdio/cucumber-framework';"
3
+ : "const { Given, When, Then } = require('@wdio/cucumber-framework');" %>
4
+ <%- answers.isUsingTypeScript || answers.esmSupport
5
+ ? `import { expect, $ } from '@wdio/globals'`
6
+ : `const { expect, $ } = require('@wdio/globals')` %>
7
+ <%
8
+ /**
9
+ * step definition without page objects
10
+ */
11
+ if (answers.usePageObjects) { %>
12
+ <%- answers.isUsingTypeScript || answers.esmSupport
13
+ ? `import LoginPage from '${answers.relativePath}/login.page${answers.esmSupport ? '.js' : ''}';`
14
+ : `const LoginPage = require('${answers.relativePath}/login.page');` %>
15
+ <%- answers.isUsingTypeScript || answers.esmSupport
16
+ ? `import SecurePage from '${answers.relativePath}/secure.page${answers.esmSupport ? '.js' : ''}';`
17
+ : `const SecurePage = require('${answers.relativePath}/secure.page');` %>
18
+
19
+ const pages = {
20
+ login: LoginPage
21
+ }
22
+
23
+ Given(/^I am on the (\w+) page$/, async (page) => {
24
+ await pages[page].open()
25
+ });
26
+
27
+ When(/^I login with (\w+) and (.+)$/, async (username, password) => {
28
+ await LoginPage.login(username, password)
29
+ });
30
+
31
+ Then(/^I should see a flash message saying (.*)$/, async (message) => {
32
+ await expect(SecurePage.flashAlert).toBeExisting();
33
+ await expect(SecurePage.flashAlert).toHaveTextContaining(message);
34
+ });
35
+ <% } else {
36
+
37
+ /**
38
+ * step definition with page objects
39
+ */
40
+ %>
41
+ Given(/^I am on the (\w+) page$/, async (page) => {
42
+ await browser.url(`https://the-internet.herokuapp.com/${page}`);
43
+ });
44
+
45
+ When(/^I login with (\w+) and (.+)$/, async (username, password) => {
46
+ await $('#username').setValue(username);
47
+ await $('#password').setValue(password);
48
+ await $('button[type="submit"]').click();
49
+ });
50
+
51
+ Then(/^I should see a flash message saying (.*)$/, async (message) => {
52
+ await expect($('#flash')).toBeExisting();
53
+ await expect($('#flash')).toHaveTextContaining(message);
54
+ });
55
+ <% } %>
@@ -0,0 +1,11 @@
1
+ <% if (answers.purpose === 'vscode') {
2
+ %><%- include('../../snippets/vscodeTest.js.ejs', { answers }) %><%
3
+ } else if (answers.purpose === 'electron') {
4
+ %><%- include('../../snippets/electronTest.js.ejs', { answers }) %><%
5
+ } else if (answers.purpose === 'macos') {
6
+ %><%- include('../../snippets/macosTest.js.ejs', { answers }) %><%
7
+ } else if (answers.usePageObjects) {
8
+ %><%- include('../../snippets/testWithPO.js.ejs', { answers }) %><%
9
+ } else if (!answers.usePageObjects) {
10
+ %><%- include('../../snippets/testWithoutPO.js.ejs', { answers }) %><%
11
+ } %>
@@ -0,0 +1,45 @@
1
+ <%- answers.isUsingTypeScript || answers.esmSupport
2
+ ? `import { $ } from '@wdio/globals'`
3
+ : `const { $ } = require('@wdio/globals')` %>
4
+ <%- answers.isUsingTypeScript || answers.esmSupport
5
+ ? `import Page from './page${answers.esmSupport ? '.js' : ''}';`
6
+ : "const Page = require('./page');" %>
7
+
8
+ /**
9
+ * sub page containing specific selectors and methods for a specific page
10
+ */
11
+ class LoginPage extends Page {
12
+ /**
13
+ * define selectors using getter methods
14
+ */
15
+ <%- answers.isUsingTypeScript ? "public " : "" %>get inputUsername () {
16
+ return $('#username');
17
+ }
18
+
19
+ <%- answers.isUsingTypeScript ? "public " : "" %>get inputPassword () {
20
+ return $('#password');
21
+ }
22
+
23
+ <%- answers.isUsingTypeScript ? "public " : "" %>get btnSubmit () {
24
+ return $('button[type="submit"]');
25
+ }
26
+
27
+ /**
28
+ * a method to encapsule automation code to interact with the page
29
+ * e.g. to login using username and password
30
+ */
31
+ <%- answers.isUsingTypeScript ? "public " : "" %>async login (username<%- answers.isUsingTypeScript ? ": string": "" %>, password<%- answers.isUsingTypeScript ? ": string": "" %>) {
32
+ await this.inputUsername.setValue(username);
33
+ await this.inputPassword.setValue(password);
34
+ await this.btnSubmit.click();
35
+ }
36
+
37
+ /**
38
+ * overwrite specific options to adapt it to page object
39
+ */
40
+ <%- answers.isUsingTypeScript ? "public " : "" %>open () {
41
+ return super.open('login');
42
+ }
43
+ }
44
+
45
+ <%- answers.isUsingTypeScript || answers.esmSupport ? "export default": "module.exports =" %> new LoginPage();
@@ -0,0 +1,17 @@
1
+ <%- answers.isUsingTypeScript || answers.esmSupport
2
+ ? `import { browser } from '@wdio/globals'`
3
+ : `const { browser } = require('@wdio/globals')` %>
4
+
5
+ /**
6
+ * main page object containing all methods, selectors and functionality
7
+ * that is shared across all page objects
8
+ */
9
+ <%- answers.isUsingTypeScript || answers.esmSupport ? "export default" : "module.exports =" %> class Page {
10
+ /**
11
+ * Opens a sub page of the page
12
+ * @param path path of the sub page (e.g. /path/to/page.html)
13
+ */
14
+ <%- answers.isUsingTypeScript ? "public " : "" %>open (path<%- answers.isUsingTypeScript ? ": string" : "" %>) {
15
+ return browser.url(`https://the-internet.herokuapp.com/${path}`)
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ <%- answers.isUsingTypeScript || answers.esmSupport
2
+ ? `import { $ } from '@wdio/globals'`
3
+ : `const { $ } = require('@wdio/globals')` %>
4
+ <%- answers.isUsingTypeScript || answers.esmSupport
5
+ ? `import Page from './page${answers.esmSupport ? '.js' : ''}';`
6
+ : "const Page = require('./page');" %>
7
+
8
+ /**
9
+ * sub page containing specific selectors and methods for a specific page
10
+ */
11
+ class SecurePage extends Page {
12
+ /**
13
+ * define selectors using getter methods
14
+ */
15
+ <%- answers.isUsingTypeScript ? "public " : "" %>get flashAlert () {
16
+ return $('#flash');
17
+ }
18
+ }
19
+
20
+ <%- answers.isUsingTypeScript || answers.esmSupport ? "export default": "module.exports =" %> new SecurePage();
@@ -0,0 +1 @@
1
+ serenity.project.name=<%= answers.projectName %>
@@ -0,0 +1,41 @@
1
+ <%- _.import('Ensure, equals', '@serenity-js/assertions' ) %>
2
+ <%- _.import('Task', '@serenity-js/core' ) %>
3
+ <%- _.import('GetRequest, LastResponse, Send', '@serenity-js/rest' ) %>
4
+
5
+ /**
6
+ * Learn more about API testing with Serenity/JS
7
+ * https://serenity-js.org/handbook/api-testing/
8
+ */
9
+ <%- _.export('class', 'GitHubStatus') %> {
10
+ static #baseApiUrl = 'https://www.githubstatus.com/api/v2/'
11
+ static #statusJson = this.#baseApiUrl + 'status.json'
12
+
13
+ static ensureAllSystemsOperational = () =>
14
+ Task.where(`#actor ensures all GitHub systems are operational`,
15
+ Send.a(GetRequest.to(this.#statusJson)),
16
+ Ensure.that(LastResponse.status(), equals(200)),
17
+ Ensure.that(
18
+ LastResponse.body<%- _.ifTs('<StatusResponse>') %>().status.description.describedAs('GitHub Status'),
19
+ equals('All Systems Operational')
20
+ ),
21
+ )
22
+ }
23
+ <% if (_.useTypeScript) { %>
24
+ /**
25
+ * Interfaces describing a simplified response structure returned by the GitHub Status Summary API:
26
+ * https://www.githubstatus.com/api/v2/summary.json
27
+ */
28
+ interface StatusResponse {
29
+ page: {
30
+ id: string
31
+ name: string
32
+ url: string
33
+ time_zone: string
34
+ updated_at: string
35
+ }
36
+ status: {
37
+ indicator: string
38
+ description: string
39
+ }
40
+ }
41
+ <% } %>
@@ -0,0 +1,100 @@
1
+ <%- _.import('contain, Ensure, equals, includes, isGreaterThan', '@serenity-js/assertions') %>
2
+ <%- _.import('type Answerable, Check, d, type QuestionAdapter, Task, Wait', '@serenity-js/core') %>
3
+ <%- _.import('By, Enter, ExecuteScript, isVisible, Key, Navigate, Page, PageElement, PageElements, Press, Text', '@serenity-js/web') %>
4
+
5
+ <%- _.import('TodoListItem', './TodoListItem') %>
6
+
7
+ <%- _.export('class', 'TodoList') %> {
8
+
9
+ // Public API captures the business domain-focused tasks
10
+ // that an actor interacting with a TodoList app can perform
11
+
12
+ static createEmptyList = () =>
13
+ Task.where('#actor creates an empty todo list',
14
+ Navigate.to('https://todo-app.serenity-js.org/'),
15
+ Ensure.that(
16
+ Page.current().title().describedAs('website title'),
17
+ equals('Serenity/JS TodoApp'),
18
+ ),
19
+ Wait.until(this.#newTodoInput(), isVisible()),
20
+ this.#emptyLocalStorageIfNeeded(),
21
+ )
22
+
23
+ static #emptyLocalStorageIfNeeded = () =>
24
+ Task.where('#actor empties local storage if needed',
25
+ Check.whether(this.#persistedItems().length, isGreaterThan(0))
26
+ .andIfSo(
27
+ this.#emptyLocalStorage(),
28
+ Page.current().reload(),
29
+ )
30
+ )
31
+
32
+ static createListContaining = (<%- _.param('itemNames', 'Array<Answerable<string>>') %>) =>
33
+ Task.where(`#actor starts with a list containing ${ itemNames.length } items`,
34
+ TodoList.createEmptyList(),
35
+ ...itemNames.map(itemName => this.recordItem(itemName))
36
+ )
37
+
38
+ static recordItem = (<%- _.param('itemName', 'Answerable<string>') %>) =>
39
+ Task.where(d `#actor records an item called ${ itemName }`,
40
+ Enter.theValue(itemName).into(this.#newTodoInput()),
41
+ Press.the(Key.Enter).in(this.#newTodoInput()),
42
+ Wait.until(Text.ofAll(this.#items()), contain(itemName)),
43
+ )
44
+
45
+ static markAsCompleted = (<%- _.param('itemNames', 'Array<Answerable<string>>') %>) =>
46
+ Task.where(d`#actor marks the following items as completed: ${ itemNames }`,
47
+ ...itemNames.map(itemName => TodoListItem.markAsCompleted(this.#itemCalled(itemName)))
48
+ )
49
+
50
+ static markAsOutstanding = (<%- _.param('itemNames', 'Array<Answerable<string>>') %>) =>
51
+ Task.where(d`#actor marks the following items as outstanding: ${ itemNames }`,
52
+ ...itemNames.map(itemName => TodoListItem.markAsOutstanding(this.#itemCalled(itemName)))
53
+ )
54
+
55
+ static outstandingItemsCount = () =>
56
+ Text.of(PageElement.located(By.tagName('strong')).of(this.#outstandingItemsLabel()))
57
+ .as(Number)
58
+ .describedAs('number of items left')
59
+
60
+ // Private API captures ways to locate interactive elements and data transformation logic.
61
+ // Private API supports the public API and is not used in the test scenario directly.
62
+
63
+ static #itemCalled = (<%- _.param('name', 'Answerable<string>') %>) =>
64
+ this.#items()
65
+ .where(Text, includes(name))
66
+ .first()
67
+ .describedAs(d`an item called ${ name }`)
68
+
69
+ static #outstandingItemsLabel = () =>
70
+ PageElement.located(By.css('.todo-count'))
71
+ .describedAs('items left counter')
72
+
73
+ static #newTodoInput = () =>
74
+ PageElement.located(By.css('.new-todo'))
75
+ .describedAs('"What needs to be done?" input box')
76
+
77
+ static #items = () =>
78
+ PageElements.located(By.css('.todo-list li'))
79
+ .describedAs('displayed items')
80
+
81
+ static #persistedItems = () =>
82
+ Page.current()
83
+ .executeScript(`
84
+ return window.localStorage['serenity-js-todo-app']
85
+ ? JSON.parse(window.localStorage['serenity-js-todo-app'])
86
+ : []
87
+ `).describedAs('persisted items')<%- _.ifTs(' as QuestionAdapter<PersistedTodoItem[]>') %>
88
+
89
+ static #emptyLocalStorage = () =>
90
+ Task.where('#actor empties local storage',
91
+ ExecuteScript.sync(`window.localStorage.removeItem('serenity-js-todo-app')`)
92
+ )
93
+ }
94
+ <% if (_.useTypeScript) { %>
95
+ interface PersistedTodoItem {
96
+ id: number;
97
+ name: string;
98
+ completed: boolean;
99
+ }
100
+ <% } %>
@@ -0,0 +1,36 @@
1
+ <%- _.import('contain, not', '@serenity-js/assertions') %>
2
+ <%- _.import('Check, d, type QuestionAdapter, Task', '@serenity-js/core') %>
3
+ <%- _.import('By, Click, CssClasses, PageElement', '@serenity-js/web') %>
4
+
5
+ <%- _.export('class', 'TodoListItem') %> {
6
+
7
+ // Public API captures the business domain-focused tasks
8
+ // that an actor interacting with a TodoListItem app can perform
9
+
10
+ static markAsCompleted = (<%- _.param('item', 'QuestionAdapter<PageElement>') %>) =>
11
+ Task.where(d `#actor marks ${ item } as completed`,
12
+ Check.whether(CssClasses.of(item), not(contain('completed')))
13
+ .andIfSo(this.toggle(item)),
14
+ )
15
+
16
+ static markAsOutstanding = (<%- _.param('item', 'QuestionAdapter<PageElement>') %>) =>
17
+ Task.where(d `#actor marks ${ item } as outstanding`,
18
+ Check.whether(CssClasses.of(item), contain('completed'))
19
+ .andIfSo(this.toggle(item)),
20
+ )
21
+
22
+ static toggle = (<%- _.param('item', 'QuestionAdapter<PageElement>') %>) =>
23
+ Task.where(d `#actor toggles the completion status of ${ item }`,
24
+ Click.on(
25
+ this.#toggleButton().of(item),
26
+ ),
27
+ )
28
+
29
+ // Private API captures ways to locate interactive elements and data transformation logic.
30
+ // Private API supports the public API and is not used in the test scenario directly.
31
+
32
+ static #toggleButton = () =>
33
+ PageElement
34
+ .located(By.css('input.toggle'))
35
+ .describedAs('toggle button')
36
+ }
@@ -0,0 +1,37 @@
1
+ <%- _.import('Given, When, Then, type DataTable', '@cucumber/cucumber') %>
2
+ <%- _.import('Ensure, equals', '@serenity-js/assertions') %>
3
+ <%- _.import('type Actor', '@serenity-js/core') %>
4
+ <%- _.import('TodoList', '../../serenity/todo-list-app/TodoList') %>
5
+
6
+ Given('{actor} starts/started with a list containing:', async (<%- _.param('actor', 'Actor') %>, <%- _.param('table', 'DataTable') %>) => {
7
+ await actor.attemptsTo(
8
+ TodoList.createListContaining(itemsFrom(table)),
9
+ )
10
+ })
11
+
12
+ When('{pronoun} marks/marked the following item(s) as completed:', async (<%- _.param('actor', 'Actor') %>, <%- _.param('table', 'DataTable') %>) => {
13
+ await actor.attemptsTo(
14
+ TodoList.markAsCompleted(itemsFrom(table)),
15
+ )
16
+ })
17
+
18
+ When('{pronoun} marks/marked the following item(s) as outstanding:', async (<%- _.param('actor', 'Actor') %>, <%- _.param('table', 'DataTable') %>) => {
19
+ await actor.attemptsTo(
20
+ TodoList.markAsOutstanding(itemsFrom(table)),
21
+ )
22
+ })
23
+
24
+ Then('{pronoun} should see that she has {int} item(s) outstanding', async (<%- _.param('actor', 'Actor') %>, <%- _.param('expectedCount', 'number') %>) => {
25
+ await actor.attemptsTo(
26
+ Ensure.that(TodoList.outstandingItemsCount(), equals(expectedCount)),
27
+ )
28
+ })
29
+
30
+ /**
31
+ * Extracts the data from a single-column Cucumber DataTable and returns it as an `Array<string>`
32
+ *
33
+ * @param table
34
+ */
35
+ function itemsFrom(<%- _.param('table', 'DataTable') %>)<%- _.returns('string[]') %> {
36
+ return table.raw().map(row => row[0])
37
+ }
@@ -0,0 +1,18 @@
1
+ <%- _.import('defineParameterType', '@cucumber/cucumber') %>
2
+ <%- _.import('actorCalled, actorInTheSpotlight', '@serenity-js/core') %>
3
+
4
+ defineParameterType({
5
+ regexp: /[A-Z][a-z]+/,
6
+ transformer(<%- _.param('name', 'string') %>) {
7
+ return actorCalled(name)
8
+ },
9
+ name: 'actor',
10
+ })
11
+
12
+ defineParameterType({
13
+ regexp: /he|she|they|his|her|their/,
14
+ transformer() {
15
+ return actorInTheSpotlight()
16
+ },
17
+ name: 'pronoun',
18
+ })