@serenity-js/webdriverio 3.0.0-rc.8 → 3.0.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 (139) hide show
  1. package/CHANGELOG.md +245 -119
  2. package/README.md +66 -45
  3. package/lib/adapter/TestRunnerLoader.d.ts +3 -4
  4. package/lib/adapter/TestRunnerLoader.d.ts.map +1 -0
  5. package/lib/adapter/TestRunnerLoader.js +1 -4
  6. package/lib/adapter/TestRunnerLoader.js.map +1 -1
  7. package/lib/adapter/WebdriverIOConfig.d.ts +115 -109
  8. package/lib/adapter/WebdriverIOConfig.d.ts.map +1 -0
  9. package/lib/adapter/WebdriverIOFrameworkAdapter.d.ts +1 -3
  10. package/lib/adapter/WebdriverIOFrameworkAdapter.d.ts.map +1 -0
  11. package/lib/adapter/WebdriverIOFrameworkAdapter.js +14 -8
  12. package/lib/adapter/WebdriverIOFrameworkAdapter.js.map +1 -1
  13. package/lib/adapter/WebdriverIOFrameworkAdapterFactory.d.ts +2 -1
  14. package/lib/adapter/WebdriverIOFrameworkAdapterFactory.d.ts.map +1 -0
  15. package/lib/adapter/WebdriverIOFrameworkAdapterFactory.js +1 -1
  16. package/lib/adapter/WebdriverIONotifier.d.ts +1 -0
  17. package/lib/adapter/WebdriverIONotifier.d.ts.map +1 -0
  18. package/lib/adapter/WebdriverIONotifier.js +5 -5
  19. package/lib/adapter/WebdriverIONotifier.js.map +1 -1
  20. package/lib/adapter/index.d.ts +1 -1
  21. package/lib/adapter/index.d.ts.map +1 -0
  22. package/lib/adapter/index.js +5 -2
  23. package/lib/adapter/index.js.map +1 -1
  24. package/lib/adapter/reporter/BrowserCapabilitiesReporter.d.ts +1 -0
  25. package/lib/adapter/reporter/BrowserCapabilitiesReporter.d.ts.map +1 -0
  26. package/lib/adapter/reporter/BrowserCapabilitiesReporter.js +4 -1
  27. package/lib/adapter/reporter/BrowserCapabilitiesReporter.js.map +1 -1
  28. package/lib/adapter/reporter/InitialisesReporters.d.ts +1 -0
  29. package/lib/adapter/reporter/InitialisesReporters.d.ts.map +1 -0
  30. package/lib/adapter/reporter/OutputStreamBuffer.d.ts +2 -1
  31. package/lib/adapter/reporter/OutputStreamBuffer.d.ts.map +1 -0
  32. package/lib/adapter/reporter/OutputStreamBufferPrinter.d.ts +2 -1
  33. package/lib/adapter/reporter/OutputStreamBufferPrinter.d.ts.map +1 -0
  34. package/lib/adapter/reporter/OutputStreamBufferPrinter.js.map +1 -1
  35. package/lib/adapter/reporter/ProvidesWriteStream.d.ts +2 -1
  36. package/lib/adapter/reporter/ProvidesWriteStream.d.ts.map +1 -0
  37. package/lib/adapter/reporter/TagPrinter.d.ts +1 -0
  38. package/lib/adapter/reporter/TagPrinter.d.ts.map +1 -0
  39. package/lib/adapter/reporter/index.d.ts +1 -0
  40. package/lib/adapter/reporter/index.d.ts.map +1 -0
  41. package/lib/adapter/reporter/index.js +5 -1
  42. package/lib/adapter/reporter/index.js.map +1 -1
  43. package/lib/index.d.ts +1 -0
  44. package/lib/index.d.ts.map +1 -0
  45. package/lib/index.js +5 -1
  46. package/lib/index.js.map +1 -1
  47. package/lib/screenplay/abilities/BrowseTheWebWithWebdriverIO.d.ts +28 -197
  48. package/lib/screenplay/abilities/BrowseTheWebWithWebdriverIO.d.ts.map +1 -0
  49. package/lib/screenplay/abilities/BrowseTheWebWithWebdriverIO.js +29 -296
  50. package/lib/screenplay/abilities/BrowseTheWebWithWebdriverIO.js.map +1 -1
  51. package/lib/screenplay/abilities/index.d.ts +1 -0
  52. package/lib/screenplay/abilities/index.d.ts.map +1 -0
  53. package/lib/screenplay/abilities/index.js +5 -1
  54. package/lib/screenplay/abilities/index.js.map +1 -1
  55. package/lib/screenplay/index.d.ts +1 -0
  56. package/lib/screenplay/index.d.ts.map +1 -0
  57. package/lib/screenplay/index.js +5 -1
  58. package/lib/screenplay/index.js.map +1 -1
  59. package/lib/screenplay/models/WebdriverIOBrowsingSession.d.ts +24 -0
  60. package/lib/screenplay/models/WebdriverIOBrowsingSession.d.ts.map +1 -0
  61. package/lib/screenplay/models/WebdriverIOBrowsingSession.js +125 -0
  62. package/lib/screenplay/models/WebdriverIOBrowsingSession.js.map +1 -0
  63. package/lib/screenplay/models/WebdriverIOCookie.d.ts +6 -0
  64. package/lib/screenplay/models/WebdriverIOCookie.d.ts.map +1 -0
  65. package/lib/screenplay/models/WebdriverIOCookie.js +7 -2
  66. package/lib/screenplay/models/WebdriverIOCookie.js.map +1 -1
  67. package/lib/screenplay/models/WebdriverIOErrorHandler.d.ts +9 -0
  68. package/lib/screenplay/models/WebdriverIOErrorHandler.d.ts.map +1 -0
  69. package/lib/screenplay/models/WebdriverIOErrorHandler.js +23 -0
  70. package/lib/screenplay/models/WebdriverIOErrorHandler.js.map +1 -0
  71. package/lib/screenplay/models/WebdriverIOModalDialogHandler.d.ts +29 -0
  72. package/lib/screenplay/models/WebdriverIOModalDialogHandler.d.ts.map +1 -0
  73. package/lib/screenplay/models/WebdriverIOModalDialogHandler.js +77 -0
  74. package/lib/screenplay/models/WebdriverIOModalDialogHandler.js.map +1 -0
  75. package/lib/screenplay/models/WebdriverIOPage.d.ts +29 -5
  76. package/lib/screenplay/models/WebdriverIOPage.d.ts.map +1 -0
  77. package/lib/screenplay/models/WebdriverIOPage.js +166 -54
  78. package/lib/screenplay/models/WebdriverIOPage.js.map +1 -1
  79. package/lib/screenplay/models/WebdriverIOPageElement.d.ts +10 -10
  80. package/lib/screenplay/models/WebdriverIOPageElement.d.ts.map +1 -0
  81. package/lib/screenplay/models/WebdriverIOPageElement.js +164 -46
  82. package/lib/screenplay/models/WebdriverIOPageElement.js.map +1 -1
  83. package/lib/screenplay/models/WebdriverIOPuppeteerModalDialogHandler.d.ts +32 -0
  84. package/lib/screenplay/models/WebdriverIOPuppeteerModalDialogHandler.d.ts.map +1 -0
  85. package/lib/screenplay/models/WebdriverIOPuppeteerModalDialogHandler.js +82 -0
  86. package/lib/screenplay/models/WebdriverIOPuppeteerModalDialogHandler.js.map +1 -0
  87. package/lib/screenplay/models/WebdriverProtocolErrorCode.d.ts +39 -0
  88. package/lib/screenplay/models/WebdriverProtocolErrorCode.d.ts.map +1 -0
  89. package/lib/screenplay/models/WebdriverProtocolErrorCode.js +43 -0
  90. package/lib/screenplay/models/WebdriverProtocolErrorCode.js.map +1 -0
  91. package/lib/screenplay/models/index.d.ts +2 -1
  92. package/lib/screenplay/models/index.d.ts.map +1 -0
  93. package/lib/screenplay/models/index.js +6 -2
  94. package/lib/screenplay/models/index.js.map +1 -1
  95. package/lib/screenplay/models/locators/WebdriverIOLocator.d.ts +18 -5
  96. package/lib/screenplay/models/locators/WebdriverIOLocator.d.ts.map +1 -0
  97. package/lib/screenplay/models/locators/WebdriverIOLocator.js +84 -5
  98. package/lib/screenplay/models/locators/WebdriverIOLocator.js.map +1 -1
  99. package/lib/screenplay/models/locators/WebdriverIORootLocator.d.ts +17 -0
  100. package/lib/screenplay/models/locators/WebdriverIORootLocator.d.ts.map +1 -0
  101. package/lib/screenplay/models/locators/WebdriverIORootLocator.js +32 -0
  102. package/lib/screenplay/models/locators/WebdriverIORootLocator.js.map +1 -0
  103. package/lib/screenplay/models/locators/index.d.ts +2 -1
  104. package/lib/screenplay/models/locators/index.d.ts.map +1 -0
  105. package/lib/screenplay/models/locators/index.js +6 -2
  106. package/lib/screenplay/models/locators/index.js.map +1 -1
  107. package/package.json +31 -32
  108. package/src/adapter/TestRunnerLoader.ts +3 -5
  109. package/src/adapter/WebdriverIOConfig.ts +114 -109
  110. package/src/adapter/WebdriverIOFrameworkAdapter.ts +20 -12
  111. package/src/adapter/WebdriverIOFrameworkAdapterFactory.ts +1 -1
  112. package/src/adapter/WebdriverIONotifier.ts +8 -6
  113. package/src/adapter/index.ts +0 -1
  114. package/src/adapter/reporter/OutputStreamBuffer.ts +1 -1
  115. package/src/adapter/reporter/OutputStreamBufferPrinter.ts +1 -1
  116. package/src/adapter/reporter/ProvidesWriteStream.ts +1 -1
  117. package/src/screenplay/abilities/BrowseTheWebWithWebdriverIO.ts +29 -339
  118. package/src/screenplay/models/WebdriverIOBrowsingSession.ts +171 -0
  119. package/src/screenplay/models/WebdriverIOCookie.ts +7 -2
  120. package/src/screenplay/models/WebdriverIOErrorHandler.ts +25 -0
  121. package/src/screenplay/models/WebdriverIOModalDialogHandler.ts +100 -0
  122. package/src/screenplay/models/WebdriverIOPage.ts +222 -63
  123. package/src/screenplay/models/WebdriverIOPageElement.ts +181 -62
  124. package/src/screenplay/models/WebdriverIOPuppeteerModalDialogHandler.ts +97 -0
  125. package/src/screenplay/models/WebdriverProtocolErrorCode.ts +38 -0
  126. package/src/screenplay/models/index.ts +1 -1
  127. package/src/screenplay/models/locators/WebdriverIOLocator.ts +122 -24
  128. package/src/screenplay/models/locators/WebdriverIORootLocator.ts +33 -0
  129. package/src/screenplay/models/locators/index.ts +1 -1
  130. package/tsconfig.build.json +17 -0
  131. package/lib/screenplay/models/WebdriverIOModalDialog.d.ts +0 -11
  132. package/lib/screenplay/models/WebdriverIOModalDialog.js +0 -40
  133. package/lib/screenplay/models/WebdriverIOModalDialog.js.map +0 -1
  134. package/lib/screenplay/models/locators/WebdriverIONativeElementRoot.d.ts +0 -2
  135. package/lib/screenplay/models/locators/WebdriverIONativeElementRoot.js +0 -3
  136. package/lib/screenplay/models/locators/WebdriverIONativeElementRoot.js.map +0 -1
  137. package/src/screenplay/models/WebdriverIOModalDialog.ts +0 -45
  138. package/src/screenplay/models/locators/WebdriverIONativeElementRoot.ts +0 -3
  139. package/tsconfig.eslint.json +0 -10
@@ -0,0 +1,171 @@
1
+ import { LogicError } from '@serenity-js/core';
2
+ import { CorrelationId } from '@serenity-js/core/lib/model';
3
+ import { BrowsingSession, ModalDialogHandler } from '@serenity-js/web';
4
+ import type { Page } from 'puppeteer-core/lib/cjs/puppeteer/api/Page';
5
+ import * as wdio from 'webdriverio';
6
+
7
+ import { WebdriverIOPage } from '../models';
8
+ import { WebdriverIOErrorHandler } from './WebdriverIOErrorHandler';
9
+ import { WebdriverIOModalDialogHandler } from './WebdriverIOModalDialogHandler';
10
+ import { WebdriverIOPuppeteerModalDialogHandler } from './WebdriverIOPuppeteerModalDialogHandler';
11
+
12
+ /**
13
+ * WebdriverIO-specific implementation of {@apilink BrowsingSession}.
14
+ *
15
+ * @group Models
16
+ */
17
+ export class WebdriverIOBrowsingSession extends BrowsingSession<WebdriverIOPage> {
18
+
19
+ constructor(protected readonly browser: wdio.Browser<'async'>) {
20
+ super();
21
+
22
+ if (! browser.$ || ! browser.$$) {
23
+ throw new LogicError(`WebdriverIO browser object is not initialised yet, so can't be assigned to an actor. Are you trying to instantiate an actor outside of a test or a test hook?`)
24
+ }
25
+ }
26
+
27
+ async allPages(): Promise<Array<WebdriverIOPage>> {
28
+ // scan all the active window handles and add any newly opened windows if needed
29
+ const windowHandles: string[] = await this.browser.getWindowHandles();
30
+
31
+ // remove pages that are no longer open
32
+ const closedPageIds = this.registeredPageIds()
33
+ .filter(id => ! windowHandles.includes(id.value));
34
+
35
+ this.deregister(...closedPageIds);
36
+
37
+ // add any new pages that might have been opened (e.g. popup windows)
38
+ const registeredWindowHandles = new Set(this.registeredPageIds().map(id => id.value));
39
+ const newlyOpenedWindowHandles = windowHandles.filter(windowHandle => ! registeredWindowHandles.has(windowHandle));
40
+
41
+ for (const newlyOpenedWindowHandle of newlyOpenedWindowHandles) {
42
+ const errorHandler = new WebdriverIOErrorHandler();
43
+ this.register(
44
+ new WebdriverIOPage(
45
+ this,
46
+ this.browser,
47
+ await this.modalDialogHandlerFor(newlyOpenedWindowHandle, errorHandler),
48
+ errorHandler,
49
+ new CorrelationId(newlyOpenedWindowHandle)
50
+ )
51
+ );
52
+ }
53
+
54
+ return super.allPages();
55
+ }
56
+
57
+ /**
58
+ * @override
59
+ * @param page
60
+ */
61
+ async changeCurrentPageTo(page: WebdriverIOPage): Promise<void> {
62
+ const currentPage = await this.currentPage();
63
+
64
+ // are we already on this page?
65
+ if (currentPage.id.equals(page.id)) {
66
+ return void 0;
67
+ }
68
+
69
+ // does the new page exist, or has it been closed in the meantime by user action, script, or similar?
70
+ if (! await page.isPresent()) {
71
+ return void 0;
72
+ }
73
+
74
+ // the page seems to be legit, switch to it
75
+ await this.browser.switchToWindow(page.id.value);
76
+
77
+ // and update the cached reference
78
+ await super.changeCurrentPageTo(page);
79
+ }
80
+
81
+ private async activeWindowHandle(): Promise<string> {
82
+ try {
83
+ return await this.browser.getWindowHandle();
84
+ }
85
+ catch (error) {
86
+ // If the window is closed by user action Webdriver will still hold the reference to the closed window.
87
+ if (['NoSuchWindowError', 'no such window'].includes(error.name)) {
88
+ const allHandles = await this.browser.getWindowHandles();
89
+ if (allHandles.length > 0) {
90
+ const handle = allHandles[allHandles.length - 1];
91
+ await this.browser.switchToWindow(handle);
92
+
93
+ return handle;
94
+ }
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ override async currentPage(): Promise<WebdriverIOPage> {
101
+ const actualCurrentPageHandle = await this.activeWindowHandle();
102
+ const actualCurrentPageId = CorrelationId.fromJSON(actualCurrentPageHandle);
103
+
104
+ if (this.currentBrowserPage && this.currentBrowserPage.id.equals(actualCurrentPageId)) {
105
+ return this.currentBrowserPage;
106
+ }
107
+
108
+ // Looks like the actual current page is not what we thought the current page was.
109
+ // Is it one of the pages we are aware of?
110
+
111
+ const allPages = await this.allPages();
112
+ const found = allPages.find(page => page.id.equals(actualCurrentPageId));
113
+ if (found) {
114
+ this.currentBrowserPage = found;
115
+ return this.currentBrowserPage;
116
+ }
117
+
118
+ // OK, so that's a handle that we haven't seen before, let's register it and set as current page.
119
+ this.currentBrowserPage = await this.registerCurrentPage();
120
+
121
+ return this.currentBrowserPage;
122
+ }
123
+
124
+ protected async registerCurrentPage(): Promise<WebdriverIOPage> {
125
+ const windowHandle = await this.browser.getWindowHandle();
126
+
127
+ const errorHandler = new WebdriverIOErrorHandler();
128
+
129
+ const page = new WebdriverIOPage(
130
+ this,
131
+ this.browser,
132
+ await this.modalDialogHandlerFor(windowHandle, errorHandler),
133
+ errorHandler,
134
+ new CorrelationId(windowHandle)
135
+ );
136
+
137
+ this.register(page)
138
+
139
+ return page;
140
+ }
141
+
142
+ private async modalDialogHandlerFor(windowHandle: string, errorHandler: WebdriverIOErrorHandler): Promise<ModalDialogHandler> {
143
+ return this.browser.isDevTools
144
+ ? new WebdriverIOPuppeteerModalDialogHandler(await this.puppeteerPageFor(windowHandle))
145
+ : new WebdriverIOModalDialogHandler(this.browser, errorHandler);
146
+ }
147
+
148
+ private async puppeteerPageFor(windowHandle: string): Promise<Page> {
149
+ const puppeteer = await this.browser.getPuppeteer();
150
+ const pages = await puppeteer.pages();
151
+
152
+ const handles = await this.browser.getWindowHandles();
153
+
154
+ if (handles.length !== pages.length) {
155
+ throw new LogicError(`The number of registered Puppeteer pages doesn't match WebdriverIO window handles.`)
156
+ }
157
+
158
+ const index = handles.indexOf(windowHandle);
159
+
160
+ // We cast to `unknown` first because the version of Page in Puppeteer-core
161
+ // might be slightly out-of-sync with what the WebdriverIO uses.
162
+ // This doesn't really matter since we're only using it to work with Dialogs.
163
+ const page = pages[index] as unknown as Page;
164
+
165
+ if (! page) {
166
+ throw new LogicError(`Couldn't find Puppeteer page for WebdriverIO window handle ${ windowHandle }.`)
167
+ }
168
+
169
+ return page;
170
+ }
171
+ }
@@ -3,6 +3,11 @@ import { Cookie, CookieData, CookieMissingError } from '@serenity-js/web';
3
3
  import { ensure, isDefined } from 'tiny-types';
4
4
  import * as wdio from 'webdriverio';
5
5
 
6
+ /**
7
+ * WebdriverIO-specific implementation of {@apilink Cookie}.
8
+ *
9
+ * @group Models
10
+ */
6
11
  export class WebdriverIOCookie extends Cookie {
7
12
 
8
13
  constructor(
@@ -14,7 +19,7 @@ export class WebdriverIOCookie extends Cookie {
14
19
  }
15
20
 
16
21
  async delete(): Promise<void> {
17
- return this.browser.deleteCookies(this.cookieName) as Promise<void>;
22
+ await this.browser.deleteCookies(this.cookieName);
18
23
  }
19
24
 
20
25
  protected async read(): Promise<CookieData> {
@@ -34,7 +39,7 @@ export class WebdriverIOCookie extends Cookie {
34
39
  value: cookie.value,
35
40
  domain: cookie.domain,
36
41
  path: cookie.path,
37
- expiry: expiry !== undefined
42
+ expiry: typeof expiry === 'number' && expiry >= 0
38
43
  ? Timestamp.fromTimestampInSeconds(Math.round(expiry))
39
44
  : undefined,
40
45
  httpOnly: cookie.httpOnly,
@@ -0,0 +1,25 @@
1
+ import { WebdriverProtocolErrorCode } from './WebdriverProtocolErrorCode';
2
+
3
+ export class WebdriverIOErrorHandler {
4
+
5
+ constructor(private readonly handlers: Map<WebdriverProtocolErrorCode, (error: Error) => Promise<void> | void> = new Map()) {
6
+ }
7
+
8
+ async executeIfHandled<T>(error: Error, action: () => Promise<T> | T): Promise<T> {
9
+ if (! this.handlers.has(error.name as WebdriverProtocolErrorCode)) {
10
+ throw error;
11
+ }
12
+
13
+ await this.handlers.get(error.name as WebdriverProtocolErrorCode)(error);
14
+
15
+ return action();
16
+ }
17
+
18
+ setHandlerFor(errorType: WebdriverProtocolErrorCode, handler: (error: Error) => Promise<void> | void): void {
19
+ this.handlers.set(errorType, handler);
20
+ }
21
+
22
+ unsetHandlerFor(errorType: WebdriverProtocolErrorCode): void {
23
+ this.handlers.delete(errorType);
24
+ }
25
+ }
@@ -0,0 +1,100 @@
1
+ import { AbsentModalDialog, AcceptedModalDialog, DismissedModalDialog, ModalDialog, ModalDialogHandler } from '@serenity-js/web';
2
+ import * as wdio from 'webdriverio';
3
+
4
+ import { WebdriverIOErrorHandler } from './WebdriverIOErrorHandler';
5
+ import { WebdriverProtocolErrorCode } from './WebdriverProtocolErrorCode';
6
+
7
+ /**
8
+ * WebdriverIO-specific implementation of {@apilink ModalDialogHandler},
9
+ * used with the [WebDriver protocol](https://webdriver.io/docs/api/webdriver).
10
+ *
11
+ * ## Learn more
12
+ * - {@apilink WebdriverIOPuppeteerModalDialogHandler}
13
+ *
14
+ * @group Models
15
+ */
16
+ export class WebdriverIOModalDialogHandler extends ModalDialogHandler {
17
+
18
+ private readonly defaultHandler: () => Promise<void> =
19
+ async () => {
20
+ const message = await this.browser.getAlertText();
21
+
22
+ await this.browser.dismissAlert();
23
+
24
+ this.modalDialog = new DismissedModalDialog(message);
25
+ }
26
+
27
+ private currentHandler: () => Promise<void>;
28
+
29
+ constructor(
30
+ private readonly browser: wdio.Browser<'async'>,
31
+ private readonly errorHandler: WebdriverIOErrorHandler,
32
+ ) {
33
+ super();
34
+
35
+ this.currentHandler = this.defaultHandler;
36
+
37
+ this.errorHandler.setHandlerFor(WebdriverProtocolErrorCode.UnexpectedAlertOpenError, error_ => this.tryToHandleDialog());
38
+ }
39
+
40
+ async tryToHandleDialog(): Promise<void> {
41
+ try {
42
+ await this.currentHandler()
43
+ }
44
+ catch (error) {
45
+ if (error.name === WebdriverProtocolErrorCode.NoSuchAlertError) {
46
+ this.modalDialog = new AbsentModalDialog();
47
+ return;
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ async acceptNext(): Promise<void> {
54
+ this.currentHandler = async () => {
55
+ const message = await this.browser.getAlertText();
56
+
57
+ await this.browser.acceptAlert();
58
+
59
+ this.modalDialog = new AcceptedModalDialog(message);
60
+ };
61
+ }
62
+
63
+ async acceptNextWithValue(text: string | number): Promise<void> {
64
+ this.currentHandler = async () => {
65
+ await this.browser.sendAlertText(String(text));
66
+ const message = await this.browser.getAlertText();
67
+
68
+ await this.browser.acceptAlert();
69
+
70
+ this.modalDialog = new AcceptedModalDialog(message);
71
+ };
72
+ }
73
+
74
+ async dismissNext(): Promise<void> {
75
+ this.currentHandler = async () => {
76
+ const message = await this.browser.getAlertText();
77
+
78
+ await this.browser.dismissAlert();
79
+
80
+ this.modalDialog = new DismissedModalDialog(message);
81
+ }
82
+ }
83
+
84
+ async reset(): Promise<void> {
85
+ this.modalDialog = new AbsentModalDialog();
86
+ this.currentHandler = this.defaultHandler;
87
+ }
88
+
89
+ /**
90
+ * @override
91
+ */
92
+ async last(): Promise<ModalDialog> {
93
+
94
+ if (this.modalDialog instanceof AbsentModalDialog) {
95
+ await this.tryToHandleDialog();
96
+ }
97
+
98
+ return this.modalDialog;
99
+ }
100
+ }
@@ -1,37 +1,203 @@
1
- import { Page } from '@serenity-js/web';
1
+ import { LogicError } from '@serenity-js/core';
2
+ import { CorrelationId } from '@serenity-js/core/lib/model';
3
+ import { BrowserWindowClosedError, Cookie, CookieData, Key, ModalDialogHandler, Page, PageElement, PageElements, Selector } from '@serenity-js/web';
2
4
  import { URL } from 'url';
3
5
  import * as wdio from 'webdriverio';
4
6
 
5
- export class WebdriverIOPage extends Page {
7
+ import { WebdriverIOLocator, WebdriverIORootLocator } from './locators';
8
+ import { WebdriverIOBrowsingSession } from './WebdriverIOBrowsingSession';
9
+ import { WebdriverIOCookie } from './WebdriverIOCookie';
10
+ import { WebdriverIOErrorHandler } from './WebdriverIOErrorHandler';
11
+ import { WebdriverIOPageElement } from './WebdriverIOPageElement';
12
+
13
+ /**
14
+ * WebdriverIO-specific implementation of {@apilink Page}.
15
+ *
16
+ * @group Models
17
+ */
18
+ export class WebdriverIOPage extends Page<wdio.Element<'async'>> {
19
+
20
+ private lastScriptExecutionSummary: LastScriptExecutionSummary;
21
+
6
22
  constructor(
23
+ session: WebdriverIOBrowsingSession,
7
24
  private readonly browser: wdio.Browser<'async'>,
8
- handle: string
25
+ modalDialogHandler: ModalDialogHandler,
26
+ private readonly errorHandler: WebdriverIOErrorHandler,
27
+ pageId: CorrelationId,
9
28
  ) {
10
- super(handle);
29
+ super(
30
+ session,
31
+ new WebdriverIORootLocator(browser),
32
+ modalDialogHandler,
33
+ pageId,
34
+ );
35
+ }
36
+
37
+ locate(selector: Selector): PageElement<wdio.Element<'async'>> {
38
+ return new WebdriverIOPageElement(
39
+ new WebdriverIOLocator(this.rootLocator, selector, this.errorHandler)
40
+ )
41
+ }
42
+
43
+ locateAll(selector: Selector): PageElements<wdio.Element<'async'>> {
44
+ return new PageElements(
45
+ new WebdriverIOLocator(this.rootLocator, selector, this.errorHandler)
46
+ );
47
+ }
48
+
49
+ async navigateTo(destination: string): Promise<void> {
50
+ await this.inContextOfThisPage(() => this.browser.url(destination));
51
+ }
52
+
53
+ async navigateBack(): Promise<void> {
54
+ await this.inContextOfThisPage(() => this.browser.back());
55
+ }
56
+
57
+ async navigateForward(): Promise<void> {
58
+ await this.inContextOfThisPage(() => this.browser.forward());
59
+ }
60
+
61
+ async reload(): Promise<void> {
62
+ await this.inContextOfThisPage(() => this.browser.refresh());
63
+ }
64
+
65
+ async sendKeys(keys: Array<Key | string>): Promise<void> {
66
+ const keySequence = keys.map(key => {
67
+ if (! Key.isKey(key)) {
68
+ return key;
69
+ }
70
+
71
+ if (this.browser.isDevTools) {
72
+ return key.devtoolsName;
73
+ }
74
+
75
+ return key.utf16codePoint;
76
+ });
77
+
78
+ await this.inContextOfThisPage(() => this.browser.keys(keySequence));
79
+ }
80
+
81
+ async executeScript<Result, InnerArguments extends any[]>(
82
+ script: string | ((...parameters: InnerArguments) => Result),
83
+ ...args: InnerArguments
84
+ ): Promise<Result> {
85
+ const innerArguments = [] as InnerArguments;
86
+ for (const arg of args) {
87
+ const innerArgument = arg instanceof WebdriverIOPageElement
88
+ ? await arg.nativeElement()
89
+ : arg;
90
+
91
+ innerArguments.push(innerArgument);
92
+ }
93
+
94
+ const result = await this.inContextOfThisPage<Result>(() => {
95
+ return this.browser.execute(script, ...innerArguments);
96
+ });
97
+
98
+ this.lastScriptExecutionSummary = new LastScriptExecutionSummary(result);
99
+
100
+ return result;
101
+ }
102
+
103
+ async executeAsyncScript<Result, Parameters extends any[]>(
104
+ script: string | ((...args: [...parameters: Parameters, callback: (result: Result) => void]) => void),
105
+ ...args: Parameters
106
+ ): Promise<Result> {
107
+ const parameters = [] as Parameters;
108
+ for (const arg of args) {
109
+ const parameter = arg instanceof WebdriverIOPageElement
110
+ ? await arg.nativeElement()
111
+ : arg;
112
+
113
+ parameters.push(parameter);
114
+ }
115
+
116
+ const result = await this.inContextOfThisPage<Result>(() => {
117
+ return this.browser.executeAsync<Result, Parameters>(script, ...parameters);
118
+ });
119
+
120
+ this.lastScriptExecutionSummary = new LastScriptExecutionSummary(result);
121
+
122
+ return result;
123
+ }
124
+
125
+ lastScriptExecutionResult<Result = any>(): Result {
126
+ if (! this.lastScriptExecutionSummary) {
127
+ throw new LogicError(`Make sure to execute a script before checking on the result`);
128
+ }
129
+
130
+ // Selenium returns `null` when the script it executed returns `undefined`
131
+ // so we're mapping the result back.
132
+ return this.lastScriptExecutionSummary.result === null
133
+ ? undefined
134
+ : this.lastScriptExecutionSummary.result;
11
135
  }
12
136
 
13
- title(): Promise<string> {
14
- return this.switchToAndPerform(async browser => {
15
- return browser.getTitle();
137
+ async takeScreenshot(): Promise<string> {
138
+ return await this.inContextOfThisPage(async () => {
139
+ try {
140
+ return await this.browser.takeScreenshot();
141
+ }
142
+ catch (error) {
143
+
144
+ if (error.name === 'ProtocolError' && error.message.includes('Target closed')) {
145
+ throw new BrowserWindowClosedError(
146
+ `Couldn't take screenshot since the browser window is already closed`,
147
+ error
148
+ );
149
+ }
150
+
151
+ throw error;
152
+ }
16
153
  });
17
154
  }
18
155
 
19
- name(): Promise<string> {
20
- return this.switchToAndPerform(async browser => {
21
- return browser.execute(`return window.name`);
156
+ async cookie(name: string): Promise<Cookie> {
157
+ return new WebdriverIOCookie(this.browser, name);
158
+ }
159
+
160
+ async setCookie(cookieData: CookieData): Promise<void> {
161
+ return await this.inContextOfThisPage(() => {
162
+ return this.browser.setCookies({
163
+ name: cookieData.name,
164
+ value: cookieData.value,
165
+ path: cookieData.path,
166
+ domain: cookieData.domain,
167
+ secure: cookieData.secure,
168
+ httpOnly: cookieData.httpOnly,
169
+ expiry: cookieData.expiry
170
+ ? cookieData.expiry.toSeconds()
171
+ : undefined,
172
+ sameSite: cookieData.sameSite,
173
+ });
22
174
  });
23
175
  }
24
176
 
177
+ async deleteAllCookies(): Promise<void> {
178
+ return await this.inContextOfThisPage(() => {
179
+ return this.browser.deleteCookies() as Promise<void>;
180
+ });
181
+ }
182
+
183
+ async title(): Promise<string> {
184
+ return await this.inContextOfThisPage(() => this.browser.getTitle());
185
+ }
186
+
187
+ async name(): Promise<string> {
188
+ return await this.inContextOfThisPage(() => this.browser.execute(`return window.name`));
189
+ }
190
+
25
191
  async url(): Promise<URL> {
26
- return this.switchToAndPerform(async browser => {
27
- return new URL(await browser.getUrl());
192
+ return await this.inContextOfThisPage(async () => {
193
+ return new URL(await this.browser.getUrl());
28
194
  });
29
195
  }
30
196
 
31
197
  async viewportSize(): Promise<{ width: number, height: number }> {
32
- return this.switchToAndPerform(async browser => {
33
- if (! browser.isDevTools) {
34
- const calculatedViewportSize = await browser.execute(`
198
+ return await this.inContextOfThisPage(async () => {
199
+ if (! this.browser.isDevTools) {
200
+ const calculatedViewportSize = await this.browser.execute(`
35
201
  return {
36
202
  width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
37
203
  height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
@@ -44,77 +210,70 @@ export class WebdriverIOPage extends Page {
44
210
  }
45
211
  }
46
212
 
47
- return browser.getWindowSize();
213
+ return this.browser.getWindowSize();
48
214
  });
49
215
  }
50
216
 
51
- setViewportSize(size: { width: number, height: number }): Promise<void> {
52
- return this.switchToAndPerform(async browser => {
217
+ async setViewportSize(size: { width: number, height: number }): Promise<void> {
218
+ return await this.inContextOfThisPage(async () => {
53
219
  let desiredWindowSize = size;
54
220
 
55
- if (! browser.isDevTools) {
56
- desiredWindowSize = await browser.execute(`
57
- var currentViewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
58
- var currentViewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
59
-
60
- return {
61
- width: Math.max(window.outerWidth - currentViewportWidth + ${ size.width }, ${ size.width }),
62
- height: Math.max(window.outerHeight - currentViewportHeight + ${ size.height }, ${ size.height }),
63
- };
64
- `);
221
+ if (! this.browser.isDevTools) {
222
+ desiredWindowSize = await this.browser.execute(`
223
+ var currentViewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
224
+ var currentViewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
225
+
226
+ return {
227
+ width: Math.max(window.outerWidth - currentViewportWidth + ${ size.width }, ${ size.width }),
228
+ height: Math.max(window.outerHeight - currentViewportHeight + ${ size.height }, ${ size.height }),
229
+ };
230
+ `);
65
231
  }
66
232
 
67
- return browser.setWindowSize(desiredWindowSize.width, desiredWindowSize.height);
233
+ return this.browser.setWindowSize(desiredWindowSize.width, desiredWindowSize.height);
68
234
  });
69
235
  }
70
236
 
71
237
  async close(): Promise<void> {
72
- return this.switchToAndPerform(browser => browser.closeWindow());
238
+ await this.inContextOfThisPage(() => this.browser.closeWindow());
73
239
  }
74
240
 
75
241
  async closeOthers(): Promise<void> {
76
- const windowHandles = await this.browser.getWindowHandles();
242
+ await this.session.closePagesOtherThan(this);
243
+ }
77
244
 
78
- for (const handle of windowHandles) {
79
- if (handle !== this.handle) {
80
- await this.browser.switchToWindow(handle);
81
- await this.browser.closeWindow();
245
+ async isPresent(): Promise<boolean> {
246
+ const allPages = await this.session.allPages();
247
+ for (const page of allPages) {
248
+ if (page === this) {
249
+ return true;
82
250
  }
83
251
  }
84
-
85
- await this.browser.switchToWindow(this.handle);
252
+ return false;
86
253
  }
87
254
 
88
- async isPresent(): Promise<boolean> {
89
- const currentPageHandle = await this.browser.getWindowHandle();
90
- const desiredPageHandle = this.handle;
255
+ private async inContextOfThisPage<T>(action: () => Promise<T> | T): Promise<T> {
256
+ let originalCurrentPage;
91
257
 
92
- const isOpen = await this.browser.switchToWindow(desiredPageHandle).then(() => true, _error => false);
258
+ try {
259
+ originalCurrentPage = await this.session.currentPage();
93
260
 
94
- await this.browser.switchToWindow(currentPageHandle);
261
+ await this.session.changeCurrentPageTo(this);
95
262
 
96
- return isOpen;
97
- }
98
-
99
- async switchTo(): Promise<void> {
100
- await this.browser.switchToWindow(this.handle);
101
- }
102
-
103
- private async switchToAndPerform<T>(action: (browser: wdio.Browser<'async'>) => Promise<T> | T): Promise<T> {
104
- const currentPageHandle = await this.browser.getWindowHandle();
105
- const desiredPageHandle = this.handle;
106
- const shouldSwitch = currentPageHandle !== desiredPageHandle;
107
-
108
- if (shouldSwitch) {
109
- await this.browser.switchToWindow(desiredPageHandle);
263
+ return await action();
110
264
  }
111
-
112
- const result = await action(this.browser);
113
-
114
- if (shouldSwitch) {
115
- await this.browser.switchToWindow(currentPageHandle);
265
+ catch (error) {
266
+ return await this.errorHandler.executeIfHandled(error, action);
267
+ }
268
+ finally {
269
+ await this.session.changeCurrentPageTo(originalCurrentPage);
116
270
  }
117
-
118
- return result;
119
271
  }
120
272
  }
273
+
274
+ /**
275
+ * @package
276
+ */
277
+ class LastScriptExecutionSummary<Result = any> {
278
+ constructor(public readonly result: Result) {}
279
+ }