create-wdio 9.0.0 → 9.17.0

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 (83) hide show
  1. package/README.md +12 -7
  2. package/assets/demo.gif +0 -0
  3. package/build/cli/config.d.ts +51 -0
  4. package/build/cli/config.d.ts.map +1 -0
  5. package/build/cli/install.d.ts +12 -0
  6. package/build/cli/install.d.ts.map +1 -0
  7. package/build/cli/utils.d.ts +18 -0
  8. package/build/cli/utils.d.ts.map +1 -0
  9. package/build/config/cli.d.ts +2 -0
  10. package/build/config/cli.d.ts.map +1 -0
  11. package/build/config/cli.js +1632 -0
  12. package/build/constants.d.ts +248 -0
  13. package/build/constants.d.ts.map +1 -0
  14. package/build/index.d.ts +4 -0
  15. package/build/index.d.ts.map +1 -0
  16. package/build/index.js +1018 -54
  17. package/build/install/cli.d.ts +2 -0
  18. package/build/install/cli.d.ts.map +1 -0
  19. package/build/install/cli.js +1719 -0
  20. package/build/install.d.ts +4 -0
  21. package/build/install.d.ts.map +1 -0
  22. package/build/templates/EjsHelpers.d.ts +18 -0
  23. package/build/templates/EjsHelpers.d.ts.map +1 -0
  24. package/build/templates/exampleFiles/browser/Component.css.ejs +121 -0
  25. package/build/templates/exampleFiles/browser/Component.lit.ejs +154 -0
  26. package/build/templates/exampleFiles/browser/Component.lit.test.ejs +24 -0
  27. package/build/templates/exampleFiles/browser/Component.preact.ejs +28 -0
  28. package/build/templates/exampleFiles/browser/Component.preact.test.ejs +59 -0
  29. package/build/templates/exampleFiles/browser/Component.react.ejs +29 -0
  30. package/build/templates/exampleFiles/browser/Component.react.test.ejs +58 -0
  31. package/build/templates/exampleFiles/browser/Component.solid.ejs +28 -0
  32. package/build/templates/exampleFiles/browser/Component.solid.test.ejs +58 -0
  33. package/build/templates/exampleFiles/browser/Component.stencil.ejs +43 -0
  34. package/build/templates/exampleFiles/browser/Component.stencil.test.ejs +45 -0
  35. package/build/templates/exampleFiles/browser/Component.svelte.ejs +47 -0
  36. package/build/templates/exampleFiles/browser/Component.svelte.test.ejs +58 -0
  37. package/build/templates/exampleFiles/browser/Component.vue.ejs +34 -0
  38. package/build/templates/exampleFiles/browser/Component.vue.test.ejs +62 -0
  39. package/build/templates/exampleFiles/browser/standalone.test.ejs +13 -0
  40. package/build/templates/exampleFiles/cucumber/features/login.feature +12 -0
  41. package/build/templates/exampleFiles/cucumber/step_definitions/steps.js.ejs +55 -0
  42. package/build/templates/exampleFiles/mochaJasmine/test.e2e.js.ejs +11 -0
  43. package/build/templates/exampleFiles/pageobjects/login.page.js.ejs +45 -0
  44. package/build/templates/exampleFiles/pageobjects/page.js.ejs +17 -0
  45. package/build/templates/exampleFiles/pageobjects/secure.page.js.ejs +20 -0
  46. package/build/templates/exampleFiles/serenity-js/common/config/serenity.properties.ejs +1 -0
  47. package/build/templates/exampleFiles/serenity-js/common/serenity/github-api/GitHubStatus.ts.ejs +41 -0
  48. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoList.ts.ejs +100 -0
  49. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoListItem.ts.ejs +36 -0
  50. package/build/templates/exampleFiles/serenity-js/cucumber/step-definitions/steps.ts.ejs +37 -0
  51. package/build/templates/exampleFiles/serenity-js/cucumber/support/parameter.config.ts.ejs +18 -0
  52. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/completing_items.feature.ejs +23 -0
  53. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/narrative.md.ejs +17 -0
  54. package/build/templates/exampleFiles/serenity-js/jasmine/example.spec.ts.ejs +86 -0
  55. package/build/templates/exampleFiles/serenity-js/mocha/example.spec.ts.ejs +88 -0
  56. package/build/templates/snippets/afterTest.ejs +20 -0
  57. package/build/templates/snippets/capabilities.ejs +57 -0
  58. package/build/templates/snippets/cucumber.ejs +50 -0
  59. package/build/templates/snippets/electronTest.js.ejs +7 -0
  60. package/build/templates/snippets/jasmine.ejs +20 -0
  61. package/build/templates/snippets/macosTest.js.ejs +11 -0
  62. package/build/templates/snippets/mocha.ejs +14 -0
  63. package/build/templates/snippets/reporters.ejs +14 -0
  64. package/build/templates/snippets/serenity.ejs +18 -0
  65. package/build/templates/snippets/services.ejs +18 -0
  66. package/build/templates/snippets/testWithPO.js.ejs +22 -0
  67. package/build/templates/snippets/testWithoutPO.js.ejs +19 -0
  68. package/build/templates/snippets/vscodeTest.js.ejs +9 -0
  69. package/build/templates/wdio.conf.tpl.ejs +412 -0
  70. package/build/types.d.ts +99 -0
  71. package/build/types.d.ts.map +1 -0
  72. package/build/utils/index.d.ts +2 -0
  73. package/build/utils/index.d.ts.map +1 -0
  74. package/build/utils/index.js +849 -0
  75. package/build/utils.d.ts +78 -0
  76. package/build/utils.d.ts.map +1 -0
  77. package/package.json +60 -41
  78. package/.eslintrc.cjs +0 -37
  79. package/__mocks__/commander.ts +0 -18
  80. package/build/constants.js +0 -51
  81. package/build/types.js +0 -1
  82. package/build/utils.js +0 -38
  83. package/vitest.config.ts +0 -23
@@ -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,12 @@
1
+ Feature: The Internet Guinea Pig Website
2
+
3
+ Scenario Outline: As a user, I can log into the secure area
4
+
5
+ Given I am on the login page
6
+ When I login with <username> and <password>
7
+ Then I should see a flash message saying <message>
8
+
9
+ Examples:
10
+ | username | password | message |
11
+ | tomsmith | SuperSecretPassword! | You logged into a secure area! |
12
+ | foobar | barfoo | Your username is invalid! |
@@ -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 with 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).toHaveText(expect.stringContaining(message));
34
+ });
35
+ <% } else {
36
+
37
+ /**
38
+ * step definition without 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')).toHaveText(expect.stringContaining(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
+ })
@@ -0,0 +1,23 @@
1
+ Feature: Completing items
2
+
3
+ Scenario: Counting outstanding items
4
+
5
+ Given Alice started with a list containing:
6
+ | Buy dog food |
7
+ | Feed the dog |
8
+ | Book a vet's appointment |
9
+ When she marks the following items as completed:
10
+ | Buy dog food |
11
+ | Feed the dog |
12
+ Then she should see that she has 1 item outstanding
13
+
14
+ Scenario: Marking items as not completed
15
+
16
+ Given Alice started with a list containing:
17
+ | Buy dog food |
18
+ | Feed the dog |
19
+ And she marked the following items as completed:
20
+ | Buy dog food |
21
+ When she marks the following items as outstanding:
22
+ | Buy dog food |
23
+ Then she should see that she has 2 items outstanding
@@ -0,0 +1,17 @@
1
+ Serenity/JS Todo List Example
2
+
3
+ Narrative:
4
+
5
+ This is a simple example of a Serenity/JS project, interacting with a [TodoMVC](http://todomvc.com/) application
6
+ available at [todo-app.serenity-js.org/](https://todo-app.serenity-js.org/).
7
+
8
+ This note is called _"the narrative"_. It can be used to provide the context around the business capability of your
9
+ product ("Reporting Results" in this case) and its features that help to enable this capability.
10
+
11
+ **Please note:** While [Cucumber](https://github.com/cucumber/cucumber-js) allows you to capture a description
12
+ of each feature in the `.feature` file, [Serenity/JS](https://serenityjs.org) allows us to group those `.feature`
13
+ files in directories corresponding to "epics", "themes" or "business capabilities" of your system and provide
14
+ each one of those with additional context using this `narrative.txt` file.
15
+
16
+ **By the way:** Did you notice that you can use **[markdown syntax](https://www.markdownguide.org/)** to better express
17
+ your thoughts?