@serenity-js/web 3.0.0-rc.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 (250) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE.md +201 -0
  3. package/NOTICE.md +1 -0
  4. package/README.md +21 -0
  5. package/lib/errors/CookieMissingError.d.ts +4 -0
  6. package/lib/errors/CookieMissingError.js +11 -0
  7. package/lib/errors/CookieMissingError.js.map +1 -0
  8. package/lib/errors/index.d.ts +1 -0
  9. package/lib/errors/index.js +14 -0
  10. package/lib/errors/index.js.map +1 -0
  11. package/lib/expectations/ElementExpectation.d.ts +11 -0
  12. package/lib/expectations/ElementExpectation.js +27 -0
  13. package/lib/expectations/ElementExpectation.js.map +1 -0
  14. package/lib/expectations/index.d.ts +6 -0
  15. package/lib/expectations/index.js +19 -0
  16. package/lib/expectations/index.js.map +1 -0
  17. package/lib/expectations/isActive.d.ts +15 -0
  18. package/lib/expectations/isActive.js +22 -0
  19. package/lib/expectations/isActive.js.map +1 -0
  20. package/lib/expectations/isClickable.d.ts +20 -0
  21. package/lib/expectations/isClickable.js +30 -0
  22. package/lib/expectations/isClickable.js.map +1 -0
  23. package/lib/expectations/isEnabled.d.ts +14 -0
  24. package/lib/expectations/isEnabled.js +20 -0
  25. package/lib/expectations/isEnabled.js.map +1 -0
  26. package/lib/expectations/isPresent.d.ts +15 -0
  27. package/lib/expectations/isPresent.js +22 -0
  28. package/lib/expectations/isPresent.js.map +1 -0
  29. package/lib/expectations/isSelected.d.ts +14 -0
  30. package/lib/expectations/isSelected.js +23 -0
  31. package/lib/expectations/isSelected.js.map +1 -0
  32. package/lib/expectations/isVisible.d.ts +14 -0
  33. package/lib/expectations/isVisible.js +26 -0
  34. package/lib/expectations/isVisible.js.map +1 -0
  35. package/lib/index.d.ts +5 -0
  36. package/lib/index.js +18 -0
  37. package/lib/index.js.map +1 -0
  38. package/lib/input/Key.d.ts +73 -0
  39. package/lib/input/Key.js +84 -0
  40. package/lib/input/Key.js.map +1 -0
  41. package/lib/input/index.d.ts +1 -0
  42. package/lib/input/index.js +14 -0
  43. package/lib/input/index.js.map +1 -0
  44. package/lib/screenplay/abilities/BrowseTheWeb.d.ts +58 -0
  45. package/lib/screenplay/abilities/BrowseTheWeb.js +19 -0
  46. package/lib/screenplay/abilities/BrowseTheWeb.js.map +1 -0
  47. package/lib/screenplay/abilities/BrowserCapabilities.d.ts +5 -0
  48. package/lib/screenplay/abilities/BrowserCapabilities.js +3 -0
  49. package/lib/screenplay/abilities/BrowserCapabilities.js.map +1 -0
  50. package/lib/screenplay/abilities/index.d.ts +2 -0
  51. package/lib/screenplay/abilities/index.js +15 -0
  52. package/lib/screenplay/abilities/index.js.map +1 -0
  53. package/lib/screenplay/index.d.ts +4 -0
  54. package/lib/screenplay/index.js +17 -0
  55. package/lib/screenplay/index.js.map +1 -0
  56. package/lib/screenplay/interactions/Clear.d.ts +79 -0
  57. package/lib/screenplay/interactions/Clear.js +97 -0
  58. package/lib/screenplay/interactions/Clear.js.map +1 -0
  59. package/lib/screenplay/interactions/Click.d.ts +73 -0
  60. package/lib/screenplay/interactions/Click.js +85 -0
  61. package/lib/screenplay/interactions/Click.js.map +1 -0
  62. package/lib/screenplay/interactions/DoubleClick.d.ts +90 -0
  63. package/lib/screenplay/interactions/DoubleClick.js +101 -0
  64. package/lib/screenplay/interactions/DoubleClick.js.map +1 -0
  65. package/lib/screenplay/interactions/Enter.d.ts +73 -0
  66. package/lib/screenplay/interactions/Enter.js +86 -0
  67. package/lib/screenplay/interactions/Enter.js.map +1 -0
  68. package/lib/screenplay/interactions/EnterBuilder.d.ts +25 -0
  69. package/lib/screenplay/interactions/EnterBuilder.js +3 -0
  70. package/lib/screenplay/interactions/EnterBuilder.js.map +1 -0
  71. package/lib/screenplay/interactions/ExecuteScript.d.ts +206 -0
  72. package/lib/screenplay/interactions/ExecuteScript.js +312 -0
  73. package/lib/screenplay/interactions/ExecuteScript.js.map +1 -0
  74. package/lib/screenplay/interactions/Hover.d.ts +78 -0
  75. package/lib/screenplay/interactions/Hover.js +89 -0
  76. package/lib/screenplay/interactions/Hover.js.map +1 -0
  77. package/lib/screenplay/interactions/Navigate.d.ts +142 -0
  78. package/lib/screenplay/interactions/Navigate.js +198 -0
  79. package/lib/screenplay/interactions/Navigate.js.map +1 -0
  80. package/lib/screenplay/interactions/PageElementInteraction.d.ts +39 -0
  81. package/lib/screenplay/interactions/PageElementInteraction.js +54 -0
  82. package/lib/screenplay/interactions/PageElementInteraction.js.map +1 -0
  83. package/lib/screenplay/interactions/Press.d.ts +84 -0
  84. package/lib/screenplay/interactions/Press.js +171 -0
  85. package/lib/screenplay/interactions/Press.js.map +1 -0
  86. package/lib/screenplay/interactions/PressBuilder.d.ts +26 -0
  87. package/lib/screenplay/interactions/PressBuilder.js +3 -0
  88. package/lib/screenplay/interactions/PressBuilder.js.map +1 -0
  89. package/lib/screenplay/interactions/RightClick.d.ts +89 -0
  90. package/lib/screenplay/interactions/RightClick.js +100 -0
  91. package/lib/screenplay/interactions/RightClick.js.map +1 -0
  92. package/lib/screenplay/interactions/Scroll.d.ts +83 -0
  93. package/lib/screenplay/interactions/Scroll.js +97 -0
  94. package/lib/screenplay/interactions/Scroll.js.map +1 -0
  95. package/lib/screenplay/interactions/Select.d.ts +212 -0
  96. package/lib/screenplay/interactions/Select.js +291 -0
  97. package/lib/screenplay/interactions/Select.js.map +1 -0
  98. package/lib/screenplay/interactions/SelectBuilder.d.ts +33 -0
  99. package/lib/screenplay/interactions/SelectBuilder.js +3 -0
  100. package/lib/screenplay/interactions/SelectBuilder.js.map +1 -0
  101. package/lib/screenplay/interactions/Switch.d.ts +150 -0
  102. package/lib/screenplay/interactions/Switch.js +209 -0
  103. package/lib/screenplay/interactions/Switch.js.map +1 -0
  104. package/lib/screenplay/interactions/TakeScreenshot.d.ts +67 -0
  105. package/lib/screenplay/interactions/TakeScreenshot.js +86 -0
  106. package/lib/screenplay/interactions/TakeScreenshot.js.map +1 -0
  107. package/lib/screenplay/interactions/Wait.d.ts +143 -0
  108. package/lib/screenplay/interactions/Wait.js +242 -0
  109. package/lib/screenplay/interactions/Wait.js.map +1 -0
  110. package/lib/screenplay/interactions/WaitBuilder.d.ts +32 -0
  111. package/lib/screenplay/interactions/WaitBuilder.js +3 -0
  112. package/lib/screenplay/interactions/WaitBuilder.js.map +1 -0
  113. package/lib/screenplay/interactions/index.d.ts +16 -0
  114. package/lib/screenplay/interactions/index.js +29 -0
  115. package/lib/screenplay/interactions/index.js.map +1 -0
  116. package/lib/screenplay/models/Cookie.d.ts +117 -0
  117. package/lib/screenplay/models/Cookie.js +176 -0
  118. package/lib/screenplay/models/Cookie.js.map +1 -0
  119. package/lib/screenplay/models/CookieData.d.ts +89 -0
  120. package/lib/screenplay/models/CookieData.js +3 -0
  121. package/lib/screenplay/models/CookieData.js.map +1 -0
  122. package/lib/screenplay/models/ModalDialog.d.ts +9 -0
  123. package/lib/screenplay/models/ModalDialog.js +14 -0
  124. package/lib/screenplay/models/ModalDialog.js.map +1 -0
  125. package/lib/screenplay/models/Page.d.ts +83 -0
  126. package/lib/screenplay/models/Page.js +52 -0
  127. package/lib/screenplay/models/Page.js.map +1 -0
  128. package/lib/screenplay/models/PageElement.d.ts +30 -0
  129. package/lib/screenplay/models/PageElement.js +62 -0
  130. package/lib/screenplay/models/PageElement.js.map +1 -0
  131. package/lib/screenplay/models/PageElements.d.ts +20 -0
  132. package/lib/screenplay/models/PageElements.js +49 -0
  133. package/lib/screenplay/models/PageElements.js.map +1 -0
  134. package/lib/screenplay/models/index.d.ts +6 -0
  135. package/lib/screenplay/models/index.js +19 -0
  136. package/lib/screenplay/models/index.js.map +1 -0
  137. package/lib/screenplay/questions/Attribute.d.ts +83 -0
  138. package/lib/screenplay/questions/Attribute.js +103 -0
  139. package/lib/screenplay/questions/Attribute.js.map +1 -0
  140. package/lib/screenplay/questions/CssClasses.d.ts +93 -0
  141. package/lib/screenplay/questions/CssClasses.js +113 -0
  142. package/lib/screenplay/questions/CssClasses.js.map +1 -0
  143. package/lib/screenplay/questions/ElementQuestion.d.ts +34 -0
  144. package/lib/screenplay/questions/ElementQuestion.js +53 -0
  145. package/lib/screenplay/questions/ElementQuestion.js.map +1 -0
  146. package/lib/screenplay/questions/LastScriptExecution.d.ts +14 -0
  147. package/lib/screenplay/questions/LastScriptExecution.js +22 -0
  148. package/lib/screenplay/questions/LastScriptExecution.js.map +1 -0
  149. package/lib/screenplay/questions/Selected.d.ts +185 -0
  150. package/lib/screenplay/questions/Selected.js +210 -0
  151. package/lib/screenplay/questions/Selected.js.map +1 -0
  152. package/lib/screenplay/questions/Text.d.ts +99 -0
  153. package/lib/screenplay/questions/Text.js +131 -0
  154. package/lib/screenplay/questions/Text.js.map +1 -0
  155. package/lib/screenplay/questions/Value.d.ts +64 -0
  156. package/lib/screenplay/questions/Value.js +78 -0
  157. package/lib/screenplay/questions/Value.js.map +1 -0
  158. package/lib/screenplay/questions/index.d.ts +6 -0
  159. package/lib/screenplay/questions/index.js +19 -0
  160. package/lib/screenplay/questions/index.js.map +1 -0
  161. package/lib/stage/crew/index.d.ts +1 -0
  162. package/lib/stage/crew/index.js +14 -0
  163. package/lib/stage/crew/index.js.map +1 -0
  164. package/lib/stage/crew/photographer/Photographer.d.ts +83 -0
  165. package/lib/stage/crew/photographer/Photographer.js +102 -0
  166. package/lib/stage/crew/photographer/Photographer.js.map +1 -0
  167. package/lib/stage/crew/photographer/index.d.ts +2 -0
  168. package/lib/stage/crew/photographer/index.js +15 -0
  169. package/lib/stage/crew/photographer/index.js.map +1 -0
  170. package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.d.ts +28 -0
  171. package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.js +81 -0
  172. package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.js.map +1 -0
  173. package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.d.ts +18 -0
  174. package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.js +30 -0
  175. package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.js.map +1 -0
  176. package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.d.ts +17 -0
  177. package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.js +28 -0
  178. package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.js.map +1 -0
  179. package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.d.ts +19 -0
  180. package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.js +28 -0
  181. package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.js.map +1 -0
  182. package/lib/stage/crew/photographer/strategies/index.d.ts +4 -0
  183. package/lib/stage/crew/photographer/strategies/index.js +17 -0
  184. package/lib/stage/crew/photographer/strategies/index.js.map +1 -0
  185. package/lib/stage/index.d.ts +1 -0
  186. package/lib/stage/index.js +14 -0
  187. package/lib/stage/index.js.map +1 -0
  188. package/package.json +85 -0
  189. package/src/errors/CookieMissingError.ts +7 -0
  190. package/src/errors/index.ts +1 -0
  191. package/src/expectations/ElementExpectation.ts +32 -0
  192. package/src/expectations/index.ts +6 -0
  193. package/src/expectations/isActive.ts +22 -0
  194. package/src/expectations/isClickable.ts +32 -0
  195. package/src/expectations/isEnabled.ts +19 -0
  196. package/src/expectations/isPresent.ts +21 -0
  197. package/src/expectations/isSelected.ts +24 -0
  198. package/src/expectations/isVisible.ts +28 -0
  199. package/src/index.ts +5 -0
  200. package/src/input/Key.ts +83 -0
  201. package/src/input/index.ts +1 -0
  202. package/src/screenplay/abilities/BrowseTheWeb.ts +89 -0
  203. package/src/screenplay/abilities/BrowserCapabilities.ts +5 -0
  204. package/src/screenplay/abilities/index.ts +2 -0
  205. package/src/screenplay/index.ts +4 -0
  206. package/src/screenplay/interactions/Clear.ts +102 -0
  207. package/src/screenplay/interactions/Click.ts +86 -0
  208. package/src/screenplay/interactions/DoubleClick.ts +102 -0
  209. package/src/screenplay/interactions/Enter.ts +92 -0
  210. package/src/screenplay/interactions/EnterBuilder.ts +28 -0
  211. package/src/screenplay/interactions/ExecuteScript.ts +345 -0
  212. package/src/screenplay/interactions/Hover.ts +90 -0
  213. package/src/screenplay/interactions/Navigate.ts +209 -0
  214. package/src/screenplay/interactions/PageElementInteraction.ts +59 -0
  215. package/src/screenplay/interactions/Press.ts +194 -0
  216. package/src/screenplay/interactions/PressBuilder.ts +29 -0
  217. package/src/screenplay/interactions/RightClick.ts +100 -0
  218. package/src/screenplay/interactions/Scroll.ts +99 -0
  219. package/src/screenplay/interactions/Select.ts +317 -0
  220. package/src/screenplay/interactions/SelectBuilder.ts +36 -0
  221. package/src/screenplay/interactions/Switch.ts +225 -0
  222. package/src/screenplay/interactions/TakeScreenshot.ts +89 -0
  223. package/src/screenplay/interactions/Wait.ts +264 -0
  224. package/src/screenplay/interactions/WaitBuilder.ts +34 -0
  225. package/src/screenplay/interactions/index.ts +16 -0
  226. package/src/screenplay/models/Cookie.ts +219 -0
  227. package/src/screenplay/models/CookieData.ts +97 -0
  228. package/src/screenplay/models/ModalDialog.ts +19 -0
  229. package/src/screenplay/models/Page.ts +147 -0
  230. package/src/screenplay/models/PageElement.ts +95 -0
  231. package/src/screenplay/models/PageElements.ts +70 -0
  232. package/src/screenplay/models/index.ts +6 -0
  233. package/src/screenplay/questions/Attribute.ts +112 -0
  234. package/src/screenplay/questions/CssClasses.ts +118 -0
  235. package/src/screenplay/questions/ElementQuestion.ts +60 -0
  236. package/src/screenplay/questions/LastScriptExecution.ts +21 -0
  237. package/src/screenplay/questions/Selected.ts +212 -0
  238. package/src/screenplay/questions/Text.ts +153 -0
  239. package/src/screenplay/questions/Value.ts +82 -0
  240. package/src/screenplay/questions/index.ts +6 -0
  241. package/src/stage/crew/index.ts +1 -0
  242. package/src/stage/crew/photographer/Photographer.ts +108 -0
  243. package/src/stage/crew/photographer/index.ts +2 -0
  244. package/src/stage/crew/photographer/strategies/PhotoTakingStrategy.ts +116 -0
  245. package/src/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.ts +28 -0
  246. package/src/stage/crew/photographer/strategies/TakePhotosOfFailures.ts +26 -0
  247. package/src/stage/crew/photographer/strategies/TakePhotosOfInteractions.ts +26 -0
  248. package/src/stage/crew/photographer/strategies/index.ts +4 -0
  249. package/src/stage/index.ts +1 -0
  250. package/tsconfig.eslint.json +10 -0
@@ -0,0 +1,147 @@
1
+ import { Adapter, Expectation, ExpectationMet, ExpectationOutcome, LogicError, Question } from '@serenity-js/core';
2
+ import { URL } from 'url';
3
+
4
+ import { BrowseTheWeb } from '../abilities';
5
+
6
+ export abstract class Page {
7
+ static current(): Question<Promise<Page>> & Adapter<Page> {
8
+ return Question.about<Promise<Page>>('current page', actor => {
9
+ return BrowseTheWeb.as(actor).currentPage();
10
+ });
11
+ }
12
+
13
+ static whichName(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
14
+ return Question.about(`page which name does ${ expectation }`, async actor => {
15
+ const pages = await BrowseTheWeb.as(actor).allPages();
16
+ const matcher = await actor.answer(expectation);
17
+
18
+ return Page.findMatchingPage(
19
+ `name does ${ expectation }`,
20
+ pages,
21
+ page => page.name().then(matcher)
22
+ );
23
+ });
24
+ }
25
+
26
+ static whichTitle(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
27
+ return Question.about(`page which title does ${ expectation }`, async actor => {
28
+ const pages = await BrowseTheWeb.as(actor).allPages();
29
+ const matcher = await actor.answer(expectation);
30
+
31
+ return Page.findMatchingPage(
32
+ `title does ${ expectation }`,
33
+ pages,
34
+ page => page.title().then(title => {
35
+ return matcher(title);
36
+ })
37
+ );
38
+ });
39
+ }
40
+
41
+ static whichUrl(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
42
+ return Question.about(`page which URL does ${ expectation }`, async actor => {
43
+ const pages = await BrowseTheWeb.as(actor).allPages();
44
+ const matcher = await actor.answer(expectation);
45
+
46
+ return Page.findMatchingPage(
47
+ `url does ${ expectation }`,
48
+ pages,
49
+ page => page.url().then(url => matcher(url.toString()))
50
+ );
51
+ });
52
+ }
53
+
54
+ private static async findMatchingPage(expectationDescription: string, pages: Page[], matcher: (page: Page) => Promise<ExpectationOutcome<any, any>>): Promise<Page> {
55
+ for (const page of pages) {
56
+ const outcome = await matcher(page);
57
+
58
+ if (outcome instanceof ExpectationMet) {
59
+ return page;
60
+ }
61
+ }
62
+
63
+ throw new LogicError(`Couldn't find a page which ${ expectationDescription }`);
64
+ }
65
+
66
+ constructor(
67
+ protected readonly handle: string,
68
+ ) {
69
+ }
70
+
71
+ /**
72
+ * @desc
73
+ * Retrieves the document title of the current top-level browsing context, equivalent to calling `document.title`.
74
+ *
75
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title
76
+ *
77
+ * @returns {Promise<string>}
78
+ */
79
+ abstract title(): Promise<string>;
80
+
81
+ /**
82
+ * @desc
83
+ * Retrieves the URL of the current top-level browsing context.
84
+ *
85
+ * @returns {Promise<URL>}
86
+ */
87
+ abstract url(): Promise<URL>;
88
+
89
+ /**
90
+ * @desc
91
+ * Retrieves the name of the current top-level browsing context.
92
+ *
93
+ * @returns {Promise<string>}
94
+ */
95
+ abstract name(): Promise<string>;
96
+
97
+ /**
98
+ * @desc
99
+ * Checks if a given window / tab / page is open and can be switched to.
100
+ *
101
+ * @returns {Promise<string>}
102
+ */
103
+ abstract isPresent(): Promise<boolean>;
104
+
105
+ /**
106
+ * @desc
107
+ * Returns the actual viewport size available for the given page,
108
+ * excluding any scrollbars.
109
+ *
110
+ * @returns {Promise<{ width: number, height: number }>}
111
+ */
112
+ abstract viewportSize(): Promise<{ width: number, height: number }>;
113
+
114
+ /**
115
+ *
116
+ * @param size
117
+ */
118
+ abstract setViewportSize(size: { width: number, height: number }): Promise<void>;
119
+
120
+ /**
121
+ * @desc
122
+ * Switches the current top-level browsing context to the given page
123
+ *
124
+ * @returns {Promise<void>}
125
+ */
126
+ abstract switchTo(): Promise<void>;
127
+
128
+ /**
129
+ * @desc
130
+ * Closes the given page.
131
+ *
132
+ * @returns {Promise<void>}
133
+ */
134
+ abstract close(): Promise<void>;
135
+
136
+ /**
137
+ * @desc
138
+ * Closes any open pages, except for this one.
139
+ *
140
+ * @returns {Promise<void>}
141
+ */
142
+ abstract closeOthers(): Promise<void>;
143
+
144
+ toString(): string {
145
+ return `page (handle=${ this.handle })`;
146
+ }
147
+ }
@@ -0,0 +1,95 @@
1
+ import { Adapter, Answerable, format, LogicError, Question } from '@serenity-js/core';
2
+
3
+ import { BrowseTheWeb } from '../abilities';
4
+
5
+ const d = format({ markQuestions: false });
6
+ const f = format({ markQuestions: true });
7
+
8
+ export abstract class PageElement<NativeElementContext = any, NativeElement = any> {
9
+ static of(childElement: Answerable<PageElement>, parentElement: Answerable<PageElement>): Question<Promise<PageElement>> & Adapter<PageElement> {
10
+ return Question.about(d`${ childElement } of ${ parentElement })`, async actor => {
11
+ const child = await actor.answer(childElement);
12
+ const parent = await actor.answer(parentElement);
13
+
14
+ return child.of(parent);
15
+ });
16
+ }
17
+
18
+ static locatedByCss(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> {
19
+ return Question.about(f`page element located by css (${selector})`, async actor => {
20
+ const cssSelector = await actor.answer(selector);
21
+
22
+ return BrowseTheWeb.as(actor).findByCss(cssSelector);
23
+ });
24
+ }
25
+
26
+ static locatedByCssContainingText(selector: Answerable<string>, text: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> {
27
+ return Question.about(f`page element located by css (${selector}) containing text ${ text }`, async actor => {
28
+ const cssSelector = await actor.answer(selector);
29
+ const desiredText = await actor.answer(text);
30
+
31
+ return BrowseTheWeb.as(actor).findByCssContainingText(cssSelector, desiredText);
32
+ });
33
+ }
34
+
35
+ static locatedById(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> {
36
+ return Question.about(f`page element located by id (${selector})`, async actor => {
37
+ const idSelector = await actor.answer(selector);
38
+
39
+ return BrowseTheWeb.as(actor).findById(idSelector);
40
+ });
41
+ }
42
+
43
+ static locatedByTagName(tagName: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> {
44
+ return Question.about(f`page element located by tag name (${tagName})`, async actor => {
45
+ const tagNameSelector = await actor.answer(tagName);
46
+
47
+ return BrowseTheWeb.as(actor).findByTagName(tagNameSelector);
48
+ });
49
+ }
50
+
51
+ static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> {
52
+ return Question.about(f`page element located by xpath (${selector})`, async actor => {
53
+ const xpathSelector = await actor.answer(selector);
54
+
55
+ return BrowseTheWeb.as(actor).findByXPath(xpathSelector);
56
+ });
57
+ }
58
+
59
+ constructor(
60
+ protected readonly context: () => Promise<NativeElementContext> | NativeElementContext,
61
+ protected readonly locator: (root: NativeElementContext) => Promise<NativeElement> | NativeElement
62
+ ) {
63
+ }
64
+
65
+ abstract of(parent: PageElement): PageElement;
66
+
67
+ async nativeElement(): Promise<NativeElement> {
68
+ try {
69
+ const context = await this.context();
70
+ return this.locator(context);
71
+ }
72
+ catch (error) {
73
+ throw new LogicError(`Couldn't find element`, error);
74
+ }
75
+ }
76
+
77
+ abstract enterValue(value: string | number | Array<string | number>): Promise<void>;
78
+ abstract clearValue(): Promise<void>;
79
+ abstract click(): Promise<void>;
80
+ abstract doubleClick(): Promise<void>;
81
+ abstract scrollIntoView(): Promise<void>;
82
+ abstract hoverOver(): Promise<void>;
83
+ abstract rightClick(): Promise<void>; // todo: should this be a click() call with a parameter?
84
+
85
+ abstract attribute(name: string): Promise<string>;
86
+ abstract text(): Promise<string>;
87
+ abstract value(): Promise<string>;
88
+
89
+ abstract isActive(): Promise<boolean>;
90
+ abstract isClickable(): Promise<boolean>;
91
+ abstract isDisplayed(): Promise<boolean>;
92
+ abstract isEnabled(): Promise<boolean>;
93
+ abstract isPresent(): Promise<boolean>;
94
+ abstract isSelected(): Promise<boolean>;
95
+ }
@@ -0,0 +1,70 @@
1
+ import { Adapter, Answerable, format, LogicError, Question } from '@serenity-js/core';
2
+
3
+ import { BrowseTheWeb } from '../abilities';
4
+ import { PageElement } from './PageElement';
5
+
6
+ const d = format({ markQuestions: false });
7
+ const f = format({ markQuestions: true });
8
+
9
+ export abstract class PageElements<NativeElementContext = any, NativeElementList = any, NativeElement = any>
10
+ // todo: implements List (Attribute.spec.ts)
11
+ {
12
+ static of(childElements: Answerable<PageElements>, parentElement: Answerable<PageElement>): Question<Promise<PageElements>> & Adapter<PageElements> {
13
+ return Question.about(d `${ childElements } of ${ parentElement })`, async actor => {
14
+ const children = await actor.answer(childElements);
15
+ const parent = await actor.answer(parentElement);
16
+
17
+ return children.of(parent);
18
+ });
19
+ }
20
+
21
+ static locatedByCss(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> {
22
+ return Question.about(f `page elements located by css (${selector})`, async actor => {
23
+ const value = await actor.answer(selector);
24
+ return BrowseTheWeb.as(actor).findAllByCss(value);
25
+ });
26
+ }
27
+
28
+ static locatedByTagName(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> {
29
+ return Question.about(f `page elements located by tag name (${selector})`, async actor => {
30
+ const tagNameSelector = await actor.answer(selector);
31
+ return BrowseTheWeb.as(actor).findAllByTagName(tagNameSelector);
32
+ });
33
+ }
34
+
35
+ static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> {
36
+ return Question.about(f `page elements located by xpath (${selector})`, async actor => {
37
+ const xpathSelector = await actor.answer(selector);
38
+ return BrowseTheWeb.as(actor).findAllByXPath(xpathSelector);
39
+ });
40
+ }
41
+
42
+ constructor(
43
+ protected readonly context: () => Promise<NativeElementContext> | NativeElementContext,
44
+ protected readonly locator: (root: NativeElementContext) => Promise<NativeElementList> | NativeElementList
45
+ ) {
46
+ }
47
+
48
+ abstract of(parent: PageElement): PageElements;
49
+
50
+ async nativeElementList(): Promise<NativeElementList> {
51
+ try {
52
+ const context = await this.context();
53
+ return this.locator(context);
54
+ }
55
+ catch (error) {
56
+ throw new LogicError(`Couldn't find elements`, error);
57
+ }
58
+ }
59
+
60
+ abstract count(): Promise<number>;
61
+ abstract first(): Promise<PageElement<NativeElementContext, NativeElement>>;
62
+ abstract last(): Promise<PageElement<NativeElementContext, NativeElement>>;
63
+ abstract get(index: number): Promise<PageElement<NativeElementContext, NativeElement>>;
64
+
65
+ abstract map<O>(fn: (element: PageElement, index?: number, elements?: PageElements) => Promise<O> | O): Promise<O[]>;
66
+
67
+ abstract filter(fn: (element: PageElement, index?: number) => Promise<boolean> | boolean): PageElements;
68
+
69
+ abstract forEach(fn: (element: PageElement, index?: number) => Promise<void> | void): Promise<void>;
70
+ }
@@ -0,0 +1,6 @@
1
+ export * from './Cookie';
2
+ export * from './CookieData';
3
+ export * from './ModalDialog';
4
+ export * from './Page';
5
+ export * from './PageElement';
6
+ export * from './PageElements';
@@ -0,0 +1,112 @@
1
+ import { Answerable, AnswersQuestions, LogicError, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core';
2
+
3
+ import { PageElement } from '../models';
4
+ import { ElementQuestion } from './ElementQuestion';
5
+
6
+ /**
7
+ * @desc
8
+ * Returns the value of the given HTML attribute of a given {@link WebElement},
9
+ * represented by Answerable<{@link @wdio/types~Element}>
10
+ *
11
+ * @example <caption>Example widget</caption>
12
+ * <ul id="shopping-list" data-items-left="2">
13
+ * <li data-state="bought">Coffee<li>
14
+ * <li data-state="buy">Honey<li>
15
+ * <li data-state="buy">Chocolate<li>
16
+ * </ul>
17
+ *
18
+ * @example <caption>Retrieve a HTML attribute of a given WebElement</caption>
19
+ * import { actorCalled } from '@serenity-js/core';
20
+ * import { Ensure, equals } from '@serenity-js/assertions';
21
+ * import { Attribute, by, BrowseTheWeb, Target } from '@serenity-js/webdriverio';
22
+ *
23
+ * const shoppingList = () =>
24
+ * Target.the('shopping list').located(by.id('shopping-list'))
25
+ *
26
+ * actorCalled('Lisa')
27
+ * .whoCan(BrowseTheWeb.using(browser))
28
+ * .attemptsTo(
29
+ * Ensure.that(Attribute.called('data-items-left').of(shoppingList()), equals('2')),
30
+ * )
31
+ *
32
+ * @example <caption>Find WebElements with a given attribute</caption>
33
+ * import { actorCalled } from '@serenity-js/core';
34
+ * import { Ensure, includes } from '@serenity-js/assertions';
35
+ * import { Attribute, BrowseTheWeb, by, Target } from '@serenity-js/webdriverio';
36
+ *
37
+ * class ShoppingList {
38
+ * static items = () =>
39
+ * Target.all('items')
40
+ * .located(by.css('#shopping-list li'))
41
+ *
42
+ * static outstandingItems = () =>
43
+ * ShoppingList.items
44
+ * .where(Attribute.called('data-state'), includes('buy'))
45
+ * }
46
+ *
47
+ * actorCalled('Lisa')
48
+ * .whoCan(BrowseTheWeb.using(browser))
49
+ * .attemptsTo(
50
+ * Ensure.that(
51
+ * Text.ofAll(ShoppingList.outstandingItems()),
52
+ * equals([ 'Honey', 'Chocolate' ])
53
+ * ),
54
+ * )
55
+ *
56
+ * @extends {ElementQuestion}
57
+ * @implements {@serenity-js/core/lib/screenplay/questions~MetaQuestion}
58
+ */
59
+ export class Attribute
60
+ extends ElementQuestion<Promise<string>>
61
+ implements MetaQuestion<Answerable<PageElement>, Promise<string>>
62
+ {
63
+ /**
64
+ * @param {Answerable<string>} name
65
+ * @returns {Attribute}
66
+ */
67
+ static called(name: Answerable<string>): Attribute {
68
+ return new Attribute(name);
69
+ }
70
+
71
+ /**
72
+ * @param {Answerable<string>} name
73
+ * @param {@serenity-js/core/lib/screenplay~Answerable<Element>} [element]
74
+ */
75
+ constructor(
76
+ private readonly name: Answerable<string>,
77
+ private readonly element?: Answerable<PageElement>,
78
+ ) {
79
+ super(`"${ name }" attribute of ${ element }`);
80
+ }
81
+
82
+ /**
83
+ * @desc
84
+ * Resolves to the value of a HTML attribute of the `target` element,
85
+ * located in the context of a `parent` element.
86
+ *
87
+ * @param {Answerable<PageElement>} parent
88
+ * @returns {Question<Promise<string[]>>}
89
+ *
90
+ * @see {@link Target.all}
91
+ * @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
92
+ */
93
+ of(parent: Answerable<PageElement>): Question<Promise<string>> {
94
+ return new Attribute(
95
+ this.name,
96
+ this.element
97
+ ? PageElement.of(this.element, parent)
98
+ : parent
99
+ );
100
+ }
101
+
102
+ async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string> {
103
+ if (! this.element) {
104
+ throw new LogicError(`Target not specified`); // todo: better error message?
105
+ }
106
+
107
+ const element = await actor.answer(this.element);
108
+ const name = await actor.answer(this.name);
109
+
110
+ return element.attribute(name);
111
+ }
112
+ }
@@ -0,0 +1,118 @@
1
+ import { Answerable, AnswersQuestions, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core';
2
+ import { formatted } from '@serenity-js/core/lib/io';
3
+
4
+ import { PageElement } from '../models';
5
+ import { ElementQuestion } from './ElementQuestion';
6
+
7
+ /**
8
+ * @desc
9
+ * Resolves to an array of [CSS classes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-class)
10
+ * of a given {@link WebElement}, represented by Answerable<{@link @wdio/types~Element}>.
11
+ *
12
+ * @example <caption>Example widget</caption>
13
+ * <ul id="shopping-list" class="active favourite">
14
+ * <li class="bought">Coffee<li>
15
+ * <li class="buy">Honey<li>
16
+ * <li class="buy">Chocolate<li>
17
+ * </ul>
18
+ *
19
+ * @example <caption>Retrieve CSS classes of a given WebElement</caption>
20
+ * import { actorCalled } from '@serenity-js/core';
21
+ * import { Ensure, equals } from '@serenity-js/assertions';
22
+ * import { BrowseTheWeb, by, CssClasses, Target } from '@serenity-js/webdriverio';
23
+ *
24
+ * const shoppingList = () =>
25
+ * Target.the('shopping list').located(by.id('shopping-list'))
26
+ *
27
+ * actorCalled('Lisa')
28
+ * .whoCan(BrowseTheWeb.using(browser))
29
+ * .attemptsTo(
30
+ * Ensure.that(CssClasses.of(shoppingList()), equals([ 'active', 'favourite' ])),
31
+ * )
32
+ *
33
+ * @example <caption>Find WebElements with a given class</caption>
34
+ * import { actorCalled } from '@serenity-js/core';
35
+ * import { Ensure, contain } from '@serenity-js/assertions';
36
+ * import { BrowseTheWeb, by, CssClasses, Target } from '@serenity-js/webdriverio';
37
+ *
38
+ * class ShoppingList {
39
+ * static items = () =>
40
+ * Target.all('items')
41
+ * .located(by.css('#shopping-list li'))
42
+ *
43
+ * static outstandingItems = () =>
44
+ * ShoppingList.items
45
+ * .where(CssClasses, contain('buy'))
46
+ * }
47
+ *
48
+ * actorCalled('Lisa')
49
+ * .whoCan(BrowseTheWeb.using(browser))
50
+ * .attemptsTo(
51
+ * Ensure.that(
52
+ * Text.ofAll(ShoppingList.outstandingItems()),
53
+ * equals([ 'Honey', 'Chocolate' ])
54
+ * ),
55
+ * )
56
+ *
57
+ * @extends {ElementQuestion}
58
+ * @implements {@serenity-js/core/lib/screenplay/questions~MetaQuestion}
59
+ */
60
+ export class CssClasses
61
+ extends ElementQuestion<Promise<string[]>>
62
+ implements MetaQuestion<Answerable<PageElement>, Promise<string[]>>
63
+ {
64
+ /**
65
+ * @param {Question<PageElement> | PageElement} target
66
+ * @returns {CssClasses}
67
+ */
68
+ static of(target: Answerable<PageElement>): CssClasses {
69
+ return new CssClasses(target);
70
+ }
71
+
72
+ /**
73
+ * @param {Question<PageElement> | PageElement} target
74
+ */
75
+ constructor(private readonly target: Answerable<PageElement>) {
76
+ super(formatted `CSS classes of ${ target}`);
77
+ }
78
+
79
+ /**
80
+ * @desc
81
+ * Resolves to an array of CSS classes of the `target` element,
82
+ * located in the context of a `parent` element.
83
+ *
84
+ * @param {@serenity-js/core/lib/screenplay~Answerable<Element>} parent
85
+ * @returns {Question<Promise<string[]>>}
86
+ *
87
+ * @see {@link Target.all}
88
+ * @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
89
+ */
90
+ of(parent: Answerable<PageElement>): Question<Promise<string[]>> {
91
+ return new CssClasses(PageElement.of(this.target, parent));
92
+ }
93
+
94
+ /**
95
+ * @desc
96
+ * Makes the provided {@link @serenity-js/core/lib/screenplay/actor~Actor}
97
+ * answer this {@link @serenity-js/core/lib/screenplay~Question}.
98
+ *
99
+ * @param {AnswersQuestions & UsesAbilities} actor
100
+ * @returns {Promise<void>}
101
+ *
102
+ * @see {@link @serenity-js/core/lib/screenplay/actor~Actor}
103
+ * @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
104
+ * @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
105
+ */
106
+ async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string[]> {
107
+ const element = await this.resolve(actor, this.target);
108
+
109
+ return element.attribute('class')
110
+ .then(attribute => attribute ?? '')
111
+ .then(attribute => attribute
112
+ .replace(/\s+/, ' ')
113
+ .trim()
114
+ .split(' ')
115
+ .filter(cssClass => !! cssClass),
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,60 @@
1
+ import { Answerable, AnswersQuestions, LogicError, Question } from '@serenity-js/core';
2
+ import { formatted } from '@serenity-js/core/lib/io';
3
+
4
+ import { PageElement, PageElements } from '../models';
5
+
6
+ /**
7
+ * @desc
8
+ * A base class for questions about {@link PageElement}s.
9
+ *
10
+ * @extends {@serenity-js/core/lib/screenplay~Question}
11
+ */
12
+ // todo: remove
13
+ export abstract class ElementQuestion<T>
14
+ extends Question<T>
15
+ {
16
+ constructor(protected subject: string) {
17
+ super();
18
+ }
19
+
20
+ /**
21
+ * @desc
22
+ * Changes the description of this question's subject.
23
+ *
24
+ * @param {string} subject
25
+ * @returns {Question<T>}
26
+ */
27
+ describedAs(subject: string): this {
28
+ this.subject = subject;
29
+ return this;
30
+ }
31
+
32
+ toString(): string {
33
+ return this.subject;
34
+ }
35
+
36
+ /**
37
+ * @desc
38
+ * Returns the resolved {@link PageElement}, or throws a {@link @serenity-js/core/lib/errors~LogicError}
39
+ * if the element is `undefined`.
40
+ *
41
+ * @param {@serenity-js/core/lib/screenplay/actor~AnswersQuestions} actor
42
+ * @param {@serenity-js/core/lib/screenplay~Answerable<Element|ElementList>} element
43
+ *
44
+ * @returns {Promise<PageElement|PageElements>}
45
+ *
46
+ * @protected
47
+ */
48
+ protected async resolve<T=PageElement|PageElements>(
49
+ actor: AnswersQuestions,
50
+ element: Answerable<T>,
51
+ ): Promise<T> {
52
+ const resolved = await actor.answer(element);
53
+
54
+ if (! resolved) {
55
+ throw new LogicError(formatted `Couldn't find ${ element }`);
56
+ }
57
+
58
+ return resolved;
59
+ }
60
+ }
@@ -0,0 +1,21 @@
1
+ import { Question } from '@serenity-js/core';
2
+
3
+ import { BrowseTheWeb } from '../abilities';
4
+
5
+ /**
6
+ * @desc
7
+ * Returns the result of last script executed via {@link ExecuteScript}
8
+ */
9
+ export class LastScriptExecution {
10
+
11
+ /**
12
+ * @desc
13
+ * Enables asserting on the result of a function executed via {@link ExecuteScript}.
14
+ *
15
+ * @returns {Question<R>}
16
+ */
17
+ static result<R>(): Question<R> {
18
+ return Question.about(`last script execution result`, actor =>
19
+ BrowseTheWeb.as(actor).lastScriptExecutionResult());
20
+ }
21
+ }