@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.
- package/CHANGELOG.md +50 -0
- package/LICENSE.md +201 -0
- package/NOTICE.md +1 -0
- package/README.md +21 -0
- package/lib/errors/CookieMissingError.d.ts +4 -0
- package/lib/errors/CookieMissingError.js +11 -0
- package/lib/errors/CookieMissingError.js.map +1 -0
- package/lib/errors/index.d.ts +1 -0
- package/lib/errors/index.js +14 -0
- package/lib/errors/index.js.map +1 -0
- package/lib/expectations/ElementExpectation.d.ts +11 -0
- package/lib/expectations/ElementExpectation.js +27 -0
- package/lib/expectations/ElementExpectation.js.map +1 -0
- package/lib/expectations/index.d.ts +6 -0
- package/lib/expectations/index.js +19 -0
- package/lib/expectations/index.js.map +1 -0
- package/lib/expectations/isActive.d.ts +15 -0
- package/lib/expectations/isActive.js +22 -0
- package/lib/expectations/isActive.js.map +1 -0
- package/lib/expectations/isClickable.d.ts +20 -0
- package/lib/expectations/isClickable.js +30 -0
- package/lib/expectations/isClickable.js.map +1 -0
- package/lib/expectations/isEnabled.d.ts +14 -0
- package/lib/expectations/isEnabled.js +20 -0
- package/lib/expectations/isEnabled.js.map +1 -0
- package/lib/expectations/isPresent.d.ts +15 -0
- package/lib/expectations/isPresent.js +22 -0
- package/lib/expectations/isPresent.js.map +1 -0
- package/lib/expectations/isSelected.d.ts +14 -0
- package/lib/expectations/isSelected.js +23 -0
- package/lib/expectations/isSelected.js.map +1 -0
- package/lib/expectations/isVisible.d.ts +14 -0
- package/lib/expectations/isVisible.js +26 -0
- package/lib/expectations/isVisible.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/input/Key.d.ts +73 -0
- package/lib/input/Key.js +84 -0
- package/lib/input/Key.js.map +1 -0
- package/lib/input/index.d.ts +1 -0
- package/lib/input/index.js +14 -0
- package/lib/input/index.js.map +1 -0
- package/lib/screenplay/abilities/BrowseTheWeb.d.ts +58 -0
- package/lib/screenplay/abilities/BrowseTheWeb.js +19 -0
- package/lib/screenplay/abilities/BrowseTheWeb.js.map +1 -0
- package/lib/screenplay/abilities/BrowserCapabilities.d.ts +5 -0
- package/lib/screenplay/abilities/BrowserCapabilities.js +3 -0
- package/lib/screenplay/abilities/BrowserCapabilities.js.map +1 -0
- package/lib/screenplay/abilities/index.d.ts +2 -0
- package/lib/screenplay/abilities/index.js +15 -0
- package/lib/screenplay/abilities/index.js.map +1 -0
- package/lib/screenplay/index.d.ts +4 -0
- package/lib/screenplay/index.js +17 -0
- package/lib/screenplay/index.js.map +1 -0
- package/lib/screenplay/interactions/Clear.d.ts +79 -0
- package/lib/screenplay/interactions/Clear.js +97 -0
- package/lib/screenplay/interactions/Clear.js.map +1 -0
- package/lib/screenplay/interactions/Click.d.ts +73 -0
- package/lib/screenplay/interactions/Click.js +85 -0
- package/lib/screenplay/interactions/Click.js.map +1 -0
- package/lib/screenplay/interactions/DoubleClick.d.ts +90 -0
- package/lib/screenplay/interactions/DoubleClick.js +101 -0
- package/lib/screenplay/interactions/DoubleClick.js.map +1 -0
- package/lib/screenplay/interactions/Enter.d.ts +73 -0
- package/lib/screenplay/interactions/Enter.js +86 -0
- package/lib/screenplay/interactions/Enter.js.map +1 -0
- package/lib/screenplay/interactions/EnterBuilder.d.ts +25 -0
- package/lib/screenplay/interactions/EnterBuilder.js +3 -0
- package/lib/screenplay/interactions/EnterBuilder.js.map +1 -0
- package/lib/screenplay/interactions/ExecuteScript.d.ts +206 -0
- package/lib/screenplay/interactions/ExecuteScript.js +312 -0
- package/lib/screenplay/interactions/ExecuteScript.js.map +1 -0
- package/lib/screenplay/interactions/Hover.d.ts +78 -0
- package/lib/screenplay/interactions/Hover.js +89 -0
- package/lib/screenplay/interactions/Hover.js.map +1 -0
- package/lib/screenplay/interactions/Navigate.d.ts +142 -0
- package/lib/screenplay/interactions/Navigate.js +198 -0
- package/lib/screenplay/interactions/Navigate.js.map +1 -0
- package/lib/screenplay/interactions/PageElementInteraction.d.ts +39 -0
- package/lib/screenplay/interactions/PageElementInteraction.js +54 -0
- package/lib/screenplay/interactions/PageElementInteraction.js.map +1 -0
- package/lib/screenplay/interactions/Press.d.ts +84 -0
- package/lib/screenplay/interactions/Press.js +171 -0
- package/lib/screenplay/interactions/Press.js.map +1 -0
- package/lib/screenplay/interactions/PressBuilder.d.ts +26 -0
- package/lib/screenplay/interactions/PressBuilder.js +3 -0
- package/lib/screenplay/interactions/PressBuilder.js.map +1 -0
- package/lib/screenplay/interactions/RightClick.d.ts +89 -0
- package/lib/screenplay/interactions/RightClick.js +100 -0
- package/lib/screenplay/interactions/RightClick.js.map +1 -0
- package/lib/screenplay/interactions/Scroll.d.ts +83 -0
- package/lib/screenplay/interactions/Scroll.js +97 -0
- package/lib/screenplay/interactions/Scroll.js.map +1 -0
- package/lib/screenplay/interactions/Select.d.ts +212 -0
- package/lib/screenplay/interactions/Select.js +291 -0
- package/lib/screenplay/interactions/Select.js.map +1 -0
- package/lib/screenplay/interactions/SelectBuilder.d.ts +33 -0
- package/lib/screenplay/interactions/SelectBuilder.js +3 -0
- package/lib/screenplay/interactions/SelectBuilder.js.map +1 -0
- package/lib/screenplay/interactions/Switch.d.ts +150 -0
- package/lib/screenplay/interactions/Switch.js +209 -0
- package/lib/screenplay/interactions/Switch.js.map +1 -0
- package/lib/screenplay/interactions/TakeScreenshot.d.ts +67 -0
- package/lib/screenplay/interactions/TakeScreenshot.js +86 -0
- package/lib/screenplay/interactions/TakeScreenshot.js.map +1 -0
- package/lib/screenplay/interactions/Wait.d.ts +143 -0
- package/lib/screenplay/interactions/Wait.js +242 -0
- package/lib/screenplay/interactions/Wait.js.map +1 -0
- package/lib/screenplay/interactions/WaitBuilder.d.ts +32 -0
- package/lib/screenplay/interactions/WaitBuilder.js +3 -0
- package/lib/screenplay/interactions/WaitBuilder.js.map +1 -0
- package/lib/screenplay/interactions/index.d.ts +16 -0
- package/lib/screenplay/interactions/index.js +29 -0
- package/lib/screenplay/interactions/index.js.map +1 -0
- package/lib/screenplay/models/Cookie.d.ts +117 -0
- package/lib/screenplay/models/Cookie.js +176 -0
- package/lib/screenplay/models/Cookie.js.map +1 -0
- package/lib/screenplay/models/CookieData.d.ts +89 -0
- package/lib/screenplay/models/CookieData.js +3 -0
- package/lib/screenplay/models/CookieData.js.map +1 -0
- package/lib/screenplay/models/ModalDialog.d.ts +9 -0
- package/lib/screenplay/models/ModalDialog.js +14 -0
- package/lib/screenplay/models/ModalDialog.js.map +1 -0
- package/lib/screenplay/models/Page.d.ts +83 -0
- package/lib/screenplay/models/Page.js +52 -0
- package/lib/screenplay/models/Page.js.map +1 -0
- package/lib/screenplay/models/PageElement.d.ts +30 -0
- package/lib/screenplay/models/PageElement.js +62 -0
- package/lib/screenplay/models/PageElement.js.map +1 -0
- package/lib/screenplay/models/PageElements.d.ts +20 -0
- package/lib/screenplay/models/PageElements.js +49 -0
- package/lib/screenplay/models/PageElements.js.map +1 -0
- package/lib/screenplay/models/index.d.ts +6 -0
- package/lib/screenplay/models/index.js +19 -0
- package/lib/screenplay/models/index.js.map +1 -0
- package/lib/screenplay/questions/Attribute.d.ts +83 -0
- package/lib/screenplay/questions/Attribute.js +103 -0
- package/lib/screenplay/questions/Attribute.js.map +1 -0
- package/lib/screenplay/questions/CssClasses.d.ts +93 -0
- package/lib/screenplay/questions/CssClasses.js +113 -0
- package/lib/screenplay/questions/CssClasses.js.map +1 -0
- package/lib/screenplay/questions/ElementQuestion.d.ts +34 -0
- package/lib/screenplay/questions/ElementQuestion.js +53 -0
- package/lib/screenplay/questions/ElementQuestion.js.map +1 -0
- package/lib/screenplay/questions/LastScriptExecution.d.ts +14 -0
- package/lib/screenplay/questions/LastScriptExecution.js +22 -0
- package/lib/screenplay/questions/LastScriptExecution.js.map +1 -0
- package/lib/screenplay/questions/Selected.d.ts +185 -0
- package/lib/screenplay/questions/Selected.js +210 -0
- package/lib/screenplay/questions/Selected.js.map +1 -0
- package/lib/screenplay/questions/Text.d.ts +99 -0
- package/lib/screenplay/questions/Text.js +131 -0
- package/lib/screenplay/questions/Text.js.map +1 -0
- package/lib/screenplay/questions/Value.d.ts +64 -0
- package/lib/screenplay/questions/Value.js +78 -0
- package/lib/screenplay/questions/Value.js.map +1 -0
- package/lib/screenplay/questions/index.d.ts +6 -0
- package/lib/screenplay/questions/index.js +19 -0
- package/lib/screenplay/questions/index.js.map +1 -0
- package/lib/stage/crew/index.d.ts +1 -0
- package/lib/stage/crew/index.js +14 -0
- package/lib/stage/crew/index.js.map +1 -0
- package/lib/stage/crew/photographer/Photographer.d.ts +83 -0
- package/lib/stage/crew/photographer/Photographer.js +102 -0
- package/lib/stage/crew/photographer/Photographer.js.map +1 -0
- package/lib/stage/crew/photographer/index.d.ts +2 -0
- package/lib/stage/crew/photographer/index.js +15 -0
- package/lib/stage/crew/photographer/index.js.map +1 -0
- package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.d.ts +28 -0
- package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.js +81 -0
- package/lib/stage/crew/photographer/strategies/PhotoTakingStrategy.js.map +1 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.d.ts +18 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.js +30 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.js.map +1 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.d.ts +17 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.js +28 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfFailures.js.map +1 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.d.ts +19 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.js +28 -0
- package/lib/stage/crew/photographer/strategies/TakePhotosOfInteractions.js.map +1 -0
- package/lib/stage/crew/photographer/strategies/index.d.ts +4 -0
- package/lib/stage/crew/photographer/strategies/index.js +17 -0
- package/lib/stage/crew/photographer/strategies/index.js.map +1 -0
- package/lib/stage/index.d.ts +1 -0
- package/lib/stage/index.js +14 -0
- package/lib/stage/index.js.map +1 -0
- package/package.json +85 -0
- package/src/errors/CookieMissingError.ts +7 -0
- package/src/errors/index.ts +1 -0
- package/src/expectations/ElementExpectation.ts +32 -0
- package/src/expectations/index.ts +6 -0
- package/src/expectations/isActive.ts +22 -0
- package/src/expectations/isClickable.ts +32 -0
- package/src/expectations/isEnabled.ts +19 -0
- package/src/expectations/isPresent.ts +21 -0
- package/src/expectations/isSelected.ts +24 -0
- package/src/expectations/isVisible.ts +28 -0
- package/src/index.ts +5 -0
- package/src/input/Key.ts +83 -0
- package/src/input/index.ts +1 -0
- package/src/screenplay/abilities/BrowseTheWeb.ts +89 -0
- package/src/screenplay/abilities/BrowserCapabilities.ts +5 -0
- package/src/screenplay/abilities/index.ts +2 -0
- package/src/screenplay/index.ts +4 -0
- package/src/screenplay/interactions/Clear.ts +102 -0
- package/src/screenplay/interactions/Click.ts +86 -0
- package/src/screenplay/interactions/DoubleClick.ts +102 -0
- package/src/screenplay/interactions/Enter.ts +92 -0
- package/src/screenplay/interactions/EnterBuilder.ts +28 -0
- package/src/screenplay/interactions/ExecuteScript.ts +345 -0
- package/src/screenplay/interactions/Hover.ts +90 -0
- package/src/screenplay/interactions/Navigate.ts +209 -0
- package/src/screenplay/interactions/PageElementInteraction.ts +59 -0
- package/src/screenplay/interactions/Press.ts +194 -0
- package/src/screenplay/interactions/PressBuilder.ts +29 -0
- package/src/screenplay/interactions/RightClick.ts +100 -0
- package/src/screenplay/interactions/Scroll.ts +99 -0
- package/src/screenplay/interactions/Select.ts +317 -0
- package/src/screenplay/interactions/SelectBuilder.ts +36 -0
- package/src/screenplay/interactions/Switch.ts +225 -0
- package/src/screenplay/interactions/TakeScreenshot.ts +89 -0
- package/src/screenplay/interactions/Wait.ts +264 -0
- package/src/screenplay/interactions/WaitBuilder.ts +34 -0
- package/src/screenplay/interactions/index.ts +16 -0
- package/src/screenplay/models/Cookie.ts +219 -0
- package/src/screenplay/models/CookieData.ts +97 -0
- package/src/screenplay/models/ModalDialog.ts +19 -0
- package/src/screenplay/models/Page.ts +147 -0
- package/src/screenplay/models/PageElement.ts +95 -0
- package/src/screenplay/models/PageElements.ts +70 -0
- package/src/screenplay/models/index.ts +6 -0
- package/src/screenplay/questions/Attribute.ts +112 -0
- package/src/screenplay/questions/CssClasses.ts +118 -0
- package/src/screenplay/questions/ElementQuestion.ts +60 -0
- package/src/screenplay/questions/LastScriptExecution.ts +21 -0
- package/src/screenplay/questions/Selected.ts +212 -0
- package/src/screenplay/questions/Text.ts +153 -0
- package/src/screenplay/questions/Value.ts +82 -0
- package/src/screenplay/questions/index.ts +6 -0
- package/src/stage/crew/index.ts +1 -0
- package/src/stage/crew/photographer/Photographer.ts +108 -0
- package/src/stage/crew/photographer/index.ts +2 -0
- package/src/stage/crew/photographer/strategies/PhotoTakingStrategy.ts +116 -0
- package/src/stage/crew/photographer/strategies/TakePhotosBeforeAndAfterInteractions.ts +28 -0
- package/src/stage/crew/photographer/strategies/TakePhotosOfFailures.ts +26 -0
- package/src/stage/crew/photographer/strategies/TakePhotosOfInteractions.ts +26 -0
- package/src/stage/crew/photographer/strategies/index.ts +4 -0
- package/src/stage/index.ts +1 -0
- package/tsconfig.eslint.json +10 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Answerable, 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
|
+
* Represents options and values selected in a
|
|
9
|
+
* [HTML `<select>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select).
|
|
10
|
+
*
|
|
11
|
+
* @see {@link Select}
|
|
12
|
+
*/
|
|
13
|
+
export class Selected {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @desc
|
|
17
|
+
* Represents the value of a single option selected in a
|
|
18
|
+
* [HTML `<select>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select).
|
|
19
|
+
*
|
|
20
|
+
* @example <caption>Example widget</caption>
|
|
21
|
+
* <select data-test='countries'>
|
|
22
|
+
* <option value='UK'>United Kingdom</option>
|
|
23
|
+
* <option value='PL'>Poland</option>
|
|
24
|
+
* <option value='US'>United States</option>
|
|
25
|
+
* </select>
|
|
26
|
+
*
|
|
27
|
+
* @example <caption>Lean Page Object</caption>
|
|
28
|
+
* import { Target } from '@serenity-js/protractor';
|
|
29
|
+
* import { browser, by } from 'protractor';
|
|
30
|
+
*
|
|
31
|
+
* class Countries {
|
|
32
|
+
* static dropdown = Target.the('countries dropdown')
|
|
33
|
+
* .located(by.css('[data-test="countries"]'));
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* @example <caption>Retrieving the selected value</caption>
|
|
37
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
38
|
+
* import { Accept, BrowseTheWeb, Select, Selected } from '@serenity-js/protractor';
|
|
39
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
40
|
+
* import { protractor } from 'protractor';
|
|
41
|
+
*
|
|
42
|
+
* actorCalled('Nick')
|
|
43
|
+
* .whoCan(BrowseTheWeb.using(protractor.browser))
|
|
44
|
+
* .attemptsTo(
|
|
45
|
+
* Select.value('UK').from(Countries.dropdown),
|
|
46
|
+
* Ensure.that(Selected.valueOf(Countries.dropdown), equals('UK')),
|
|
47
|
+
* );
|
|
48
|
+
*
|
|
49
|
+
* @param {Answerable<PageElement>} pageElement
|
|
50
|
+
* A {@link PageElement} identifying the `<select>` element of interest
|
|
51
|
+
*
|
|
52
|
+
* @returns {Question<Promise<string>>}
|
|
53
|
+
*
|
|
54
|
+
* @see {@link Select.value}
|
|
55
|
+
*/
|
|
56
|
+
static valueOf(pageElement: Answerable<PageElement>): Question<Promise<string>> {
|
|
57
|
+
return PageElement.locatedByCss('option:checked')
|
|
58
|
+
.of(pageElement)
|
|
59
|
+
.value()
|
|
60
|
+
.describedAs(formatted `value selected in ${ pageElement }`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @desc
|
|
65
|
+
* Represents values of options selected in a
|
|
66
|
+
* [HTML `<select multiple>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-multiple)
|
|
67
|
+
*
|
|
68
|
+
* @example <caption>Example widget</caption>
|
|
69
|
+
* <select multiple data-test='countries'>
|
|
70
|
+
* <option value='UK'>United Kingdom</option>
|
|
71
|
+
* <option value='PL'>Poland</option>
|
|
72
|
+
* <option value='US'>United States</option>
|
|
73
|
+
* </select>
|
|
74
|
+
*
|
|
75
|
+
* @example <caption>Lean Page Object</caption>
|
|
76
|
+
* import { Target } from '@serenity-js/protractor';
|
|
77
|
+
* import { browser, by } from 'protractor';
|
|
78
|
+
*
|
|
79
|
+
* class Countries {
|
|
80
|
+
* static dropdown = Target.the('countries dropdown')
|
|
81
|
+
* .located(by.css('[data-test="countries"]'));
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* @example <caption>Retrieving the selected value</caption>
|
|
85
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
86
|
+
* import { Accept, BrowseTheWeb, Select, Selected } from '@serenity-js/protractor';
|
|
87
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
88
|
+
* import { protractor } from 'protractor';
|
|
89
|
+
*
|
|
90
|
+
* actorCalled('Nick')
|
|
91
|
+
* .whoCan(BrowseTheWeb.using(protractor.browser))
|
|
92
|
+
* .attemptsTo(
|
|
93
|
+
* Select.values('UK').from(Countries.dropdown),
|
|
94
|
+
* Ensure.that(Selected.valuesOf(Countries.dropdown), equals([ 'UK' ])),
|
|
95
|
+
* );
|
|
96
|
+
*
|
|
97
|
+
* @param {Answerable<PageElement>} pageElement
|
|
98
|
+
* A {@link Target} identifying the `<select>` element of interest
|
|
99
|
+
*
|
|
100
|
+
* @returns {Question<Promise<string[]>>}
|
|
101
|
+
*
|
|
102
|
+
* @see {@link Select.values}
|
|
103
|
+
*/
|
|
104
|
+
static valuesOf(pageElement: Answerable<PageElement>): Question<Promise<string[]>> {
|
|
105
|
+
return PageElements.locatedByCss('option:checked')
|
|
106
|
+
.of(pageElement)
|
|
107
|
+
.map(item => item.value())
|
|
108
|
+
.describedAs(formatted `values selected in ${ pageElement }`) as Question<Promise<string[]>>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @desc
|
|
113
|
+
* Represents a single option selected in a
|
|
114
|
+
* [HTML `<select>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-multiple)
|
|
115
|
+
*
|
|
116
|
+
* @example <caption>Example widget</caption>
|
|
117
|
+
* <select data-test='countries'>
|
|
118
|
+
* <option value='UK'>United Kingdom</option>
|
|
119
|
+
* <option value='PL'>Poland</option>
|
|
120
|
+
* <option value='US'>United States</option>
|
|
121
|
+
* </select>
|
|
122
|
+
*
|
|
123
|
+
* @example <caption>Lean Page Object</caption>
|
|
124
|
+
* import { Target } from '@serenity-js/protractor';
|
|
125
|
+
* import { browser, by } from 'protractor';
|
|
126
|
+
*
|
|
127
|
+
* class Countries {
|
|
128
|
+
* static dropdown = Target.the('countries dropdown')
|
|
129
|
+
* .located(by.css('[data-test="countries"]'));
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* @example <caption>Retrieving the selected value</caption>
|
|
133
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
134
|
+
* import { Accept, BrowseTheWeb, Select, Selected } from '@serenity-js/protractor';
|
|
135
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
136
|
+
* import { protractor } from 'protractor';
|
|
137
|
+
*
|
|
138
|
+
* actorCalled('Nick')
|
|
139
|
+
* .whoCan(BrowseTheWeb.using(protractor.browser))
|
|
140
|
+
* .attemptsTo(
|
|
141
|
+
* Select.option('Poland').from(Countries.dropdown),
|
|
142
|
+
* Ensure.that(
|
|
143
|
+
* Selected.optionIn(Countries.dropdown),
|
|
144
|
+
* equals('Poland')
|
|
145
|
+
* ),
|
|
146
|
+
* );
|
|
147
|
+
*
|
|
148
|
+
* @param {Answerable<PageElement>} pageElement
|
|
149
|
+
* A {@link Target} identifying the `<select>` element of interest
|
|
150
|
+
*
|
|
151
|
+
* @returns {Question<Promise<string>>}
|
|
152
|
+
*
|
|
153
|
+
* @see {@link Select.option}
|
|
154
|
+
*/
|
|
155
|
+
static optionIn(pageElement: Answerable<PageElement>): Question<Promise<string>> {
|
|
156
|
+
return PageElement.locatedByCss('option:checked')
|
|
157
|
+
.of(pageElement)
|
|
158
|
+
.text()
|
|
159
|
+
.describedAs(formatted `option selected in ${ pageElement }`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @desc
|
|
164
|
+
* Represents options selected in a
|
|
165
|
+
* [HTML `<select multiple>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-multiple)
|
|
166
|
+
*
|
|
167
|
+
* @example <caption>Example widget</caption>
|
|
168
|
+
* <select multiple data-test='countries'>
|
|
169
|
+
* <option value='UK'>United Kingdom</option>
|
|
170
|
+
* <option value='PL'>Poland</option>
|
|
171
|
+
* <option value='US'>United States</option>
|
|
172
|
+
* </select>
|
|
173
|
+
*
|
|
174
|
+
* @example <caption>Lean Page Object</caption>
|
|
175
|
+
* import { Target } from '@serenity-js/protractor';
|
|
176
|
+
* import { browser, by } from 'protractor';
|
|
177
|
+
*
|
|
178
|
+
* class Countries {
|
|
179
|
+
* static dropdown = Target.the('countries dropdown')
|
|
180
|
+
* .located(by.css('[data-test="countries"]'));
|
|
181
|
+
* }
|
|
182
|
+
*
|
|
183
|
+
* @example <caption>Retrieving the selected value</caption>
|
|
184
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
185
|
+
* import { Accept, BrowseTheWeb, Select, Selected } from '@serenity-js/protractor';
|
|
186
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
187
|
+
* import { protractor } from 'protractor';
|
|
188
|
+
*
|
|
189
|
+
* actorCalled('Nick')
|
|
190
|
+
* .whoCan(BrowseTheWeb.using(protractor.browser))
|
|
191
|
+
* .attemptsTo(
|
|
192
|
+
* Select.options('Poland', 'United States').from(Countries.dropdown),
|
|
193
|
+
* Ensure.that(
|
|
194
|
+
* Selected.optionsIn(Countries.dropdown),
|
|
195
|
+
* equals([ 'Poland', 'United States' ])
|
|
196
|
+
* ),
|
|
197
|
+
* );
|
|
198
|
+
*
|
|
199
|
+
* @param {Answerable<PageElement>} pageElement
|
|
200
|
+
* A {@link Target} identifying the `<select>` element of interest
|
|
201
|
+
*
|
|
202
|
+
* @returns {Question<Promise<string[]>>}
|
|
203
|
+
*
|
|
204
|
+
* @see {@link Select.options}
|
|
205
|
+
*/
|
|
206
|
+
static optionsIn(pageElement: Answerable<PageElement>): Question<Promise<string[]>> {
|
|
207
|
+
return PageElements.locatedByCss('option:checked')
|
|
208
|
+
.of(pageElement)
|
|
209
|
+
.map(item => item.text())
|
|
210
|
+
.describedAs(formatted `options selected in ${ pageElement }`) as Question<Promise<string[]>>;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Adapter, Answerable, AnswersQuestions, createAdapter, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core';
|
|
2
|
+
|
|
3
|
+
import { PageElement, PageElements } from '../models';
|
|
4
|
+
import { ElementQuestion } from './ElementQuestion';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @desc
|
|
8
|
+
* Resolves to the visible (i.e. not hidden by CSS) `innerText` of:
|
|
9
|
+
* - a given {@link WebElement}, represented by Answerable<{@link @wdio/types~Element}>
|
|
10
|
+
* - a group of {@link WebElement}s, represented by Answerable<{@link @wdio/types~ElementList}>
|
|
11
|
+
*
|
|
12
|
+
* The result includes the visible text of any sub-elements, without any leading or trailing whitespace.
|
|
13
|
+
*
|
|
14
|
+
* @example <caption>Example widget</caption>
|
|
15
|
+
* <h1>Shopping list</h1>
|
|
16
|
+
* <ul id="shopping-list">
|
|
17
|
+
* <li>Coffee<li>
|
|
18
|
+
* <li class="bought">Honey<li>
|
|
19
|
+
* <li>Chocolate<li>
|
|
20
|
+
* </ul>
|
|
21
|
+
*
|
|
22
|
+
* @example <caption>Retrieve text of a single element</caption>
|
|
23
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
24
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
25
|
+
* import { BrowseTheWeb, by, Target, Text } from '@serenity-js/webdriverio';
|
|
26
|
+
*
|
|
27
|
+
* const header = () =>
|
|
28
|
+
* Target.the('header').located(by.tagName('h1'))
|
|
29
|
+
*
|
|
30
|
+
* actorCalled('Lisa')
|
|
31
|
+
* .whoCan(BrowseTheWeb.using(browser))
|
|
32
|
+
* .attemptsTo(
|
|
33
|
+
* Ensure.that(Text.of(header()), equals('Shopping list')),
|
|
34
|
+
* )
|
|
35
|
+
*
|
|
36
|
+
* @example <caption>Retrieve text of a multiple elements</caption>
|
|
37
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
38
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
39
|
+
* import { BrowseTheWeb, by, Target, Text } from '@serenity-js/webdriverio';
|
|
40
|
+
*
|
|
41
|
+
* const shoppingListItems = () =>
|
|
42
|
+
* Target.the('shopping list items').located(by.css('#shopping-list li'))
|
|
43
|
+
*
|
|
44
|
+
* actorCalled('Lisa')
|
|
45
|
+
* .whoCan(BrowseTheWeb.using(browser))
|
|
46
|
+
* .attemptsTo(
|
|
47
|
+
* Ensure.that(
|
|
48
|
+
* Text.ofAll(shoppingListItems()),
|
|
49
|
+
* equals([ 'Coffee', 'Honey', 'Chocolate' ])
|
|
50
|
+
* ),
|
|
51
|
+
* )
|
|
52
|
+
*
|
|
53
|
+
* @example <caption>Find element with matching text</caption>
|
|
54
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
55
|
+
* import { contain, Ensure } from '@serenity-js/assertions';
|
|
56
|
+
* import { BrowseTheWeb, by, CssClasses, Target, Text } from '@serenity-js/webdriverio';
|
|
57
|
+
*
|
|
58
|
+
* const shoppingListItemCalled = (name: string) =>
|
|
59
|
+
* Target.the('shopping list items').located(by.css('#shopping-list li'))
|
|
60
|
+
* .where(Text, equals(name))
|
|
61
|
+
* .first()
|
|
62
|
+
*
|
|
63
|
+
* actorCalled('Lisa')
|
|
64
|
+
* .whoCan(BrowseTheWeb.using(browser))
|
|
65
|
+
* .attemptsTo(
|
|
66
|
+
* Ensure.that(
|
|
67
|
+
* CssClasses.of(shoppingListItemCalled('Honey)),
|
|
68
|
+
* contain('bought')
|
|
69
|
+
* ),
|
|
70
|
+
* )
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
* @see {@link Target}
|
|
74
|
+
*/
|
|
75
|
+
export class Text {
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @desc
|
|
79
|
+
* Retrieves text of a single {@link WebElement},
|
|
80
|
+
* represented by Answerable<{@link @wdio/types~Element}>.
|
|
81
|
+
*
|
|
82
|
+
* @param {Answerable<PageElement>} element
|
|
83
|
+
* @returns {Question<Promise<string>> & MetaQuestion<Answerable<PageElement>, Promise<string>>}
|
|
84
|
+
*
|
|
85
|
+
* @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
|
|
86
|
+
*/
|
|
87
|
+
static of(element: Answerable<PageElement>):
|
|
88
|
+
Question<Promise<string>> & // eslint-disable-line @typescript-eslint/indent
|
|
89
|
+
MetaQuestion<Answerable<PageElement>, Promise<string>> & // eslint-disable-line @typescript-eslint/indent
|
|
90
|
+
Adapter<string> // eslint-disable-line @typescript-eslint/indent
|
|
91
|
+
{
|
|
92
|
+
return createAdapter<Promise<string>, ElementQuestion<Promise<string>> & MetaQuestion<Answerable<PageElement>, Promise<string>>>(
|
|
93
|
+
new TextOfSingleElement(element)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @desc
|
|
99
|
+
* Retrieves text of a group of {@link WebElement}s,
|
|
100
|
+
* represented by Answerable<{@link @wdio/types~ElementList}>
|
|
101
|
+
*
|
|
102
|
+
* @param {Answerable<PageElements>} elements
|
|
103
|
+
* @returns {Question<Promise<string[]>> & MetaQuestion<Answerable<PageElement>, Promise<string[]>>}
|
|
104
|
+
*
|
|
105
|
+
* @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
|
|
106
|
+
*/
|
|
107
|
+
static ofAll(elements: Answerable<PageElements>):
|
|
108
|
+
Question<Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent
|
|
109
|
+
MetaQuestion<Answerable<PageElement>, Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent
|
|
110
|
+
Adapter<string[]> // eslint-disable-line @typescript-eslint/indent
|
|
111
|
+
{
|
|
112
|
+
return createAdapter<Promise<string[]>, ElementQuestion<Promise<string[]>> & MetaQuestion<Answerable<PageElement>, Promise<string[]>>>(
|
|
113
|
+
new TextOfMultipleElements(elements)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class TextOfSingleElement
|
|
119
|
+
extends ElementQuestion<Promise<string>>
|
|
120
|
+
implements MetaQuestion<Answerable<PageElement>, Promise<string>>
|
|
121
|
+
{
|
|
122
|
+
constructor(private readonly element: Answerable<PageElement>) {
|
|
123
|
+
super(`the text of ${ element }`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
of(parent: Answerable<PageElement>): Question<Promise<string>> {
|
|
127
|
+
return new TextOfSingleElement(PageElement.of(this.element, parent));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string> {
|
|
131
|
+
const element = await this.resolve(actor, this.element);
|
|
132
|
+
|
|
133
|
+
return element.text();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class TextOfMultipleElements
|
|
138
|
+
extends ElementQuestion<Promise<string[]>>
|
|
139
|
+
implements MetaQuestion<Answerable<PageElement>, Promise<string[]>>
|
|
140
|
+
{
|
|
141
|
+
constructor(private readonly elements: Answerable<PageElements>) {
|
|
142
|
+
super(`the text of ${ elements }`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
of(parent: Answerable<PageElement>): Question<Promise<string[]>> {
|
|
146
|
+
return new TextOfMultipleElements(PageElements.of(this.elements, parent));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string[]> {
|
|
150
|
+
const elements = await this.resolve(actor, this.elements);
|
|
151
|
+
return elements.map(element => element.text());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* Returns the `value` attribute of a given {@link WebElement},
|
|
10
|
+
* represented by Answerable<{@link @wdio/types~Element}>
|
|
11
|
+
*
|
|
12
|
+
* @example <caption>Example widget</caption>
|
|
13
|
+
* <input type="text" id="username" value="Alice" />
|
|
14
|
+
*
|
|
15
|
+
* @example <caption>Retrieve CSS classes of a given WebElement</caption>
|
|
16
|
+
* import { actorCalled } from '@serenity-js/core';
|
|
17
|
+
* import { Ensure, equals } from '@serenity-js/assertions';
|
|
18
|
+
* import { BrowseTheWeb, by, Value, Target } from '@serenity-js/webdriverio';
|
|
19
|
+
*
|
|
20
|
+
* const usernameField = () =>
|
|
21
|
+
* Target.the('username field').located(by.id('username'))
|
|
22
|
+
*
|
|
23
|
+
* actorCalled('Lisa')
|
|
24
|
+
* .whoCan(BrowseTheWeb.using(browser))
|
|
25
|
+
* .attemptsTo(
|
|
26
|
+
* Ensure.that(Value.of(usernameField), equals('Alice')),
|
|
27
|
+
* )
|
|
28
|
+
*
|
|
29
|
+
* @extends {@serenity-js/core/lib/screenplay~Question}
|
|
30
|
+
* @implements {@serenity-js/core/lib/screenplay/questions~MetaQuestion}
|
|
31
|
+
*/
|
|
32
|
+
export class Value
|
|
33
|
+
extends ElementQuestion<Promise<string>>
|
|
34
|
+
implements MetaQuestion<Answerable<PageElement>, Promise<string>>
|
|
35
|
+
{
|
|
36
|
+
/**
|
|
37
|
+
* @param {Answerable<PageElement>} element
|
|
38
|
+
* @returns {Value}
|
|
39
|
+
*/
|
|
40
|
+
static of(element: Answerable<PageElement>): Question<Promise<string>> & MetaQuestion<Answerable<PageElement>, Promise<string>> {
|
|
41
|
+
return new Value(element);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Answerable<PageElement>} element
|
|
46
|
+
*/
|
|
47
|
+
constructor(private readonly element: Answerable<PageElement>) {
|
|
48
|
+
super(formatted`the value of ${ element }`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @desc
|
|
53
|
+
* Resolves to the value of a given [`input`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
|
|
54
|
+
* {@link WebElement}, located in the context of a `parent` element.
|
|
55
|
+
*
|
|
56
|
+
* @param {Answerable<PageElement>} parent
|
|
57
|
+
* @returns {Question<Promise<string>>}
|
|
58
|
+
*
|
|
59
|
+
* @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
|
|
60
|
+
*/
|
|
61
|
+
of(parent: Answerable<PageElement>): Question<Promise<string>> {
|
|
62
|
+
return new Value(PageElement.of(this.element, parent));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @desc
|
|
67
|
+
* Makes the provided {@link @serenity-js/core/lib/screenplay/actor~Actor}
|
|
68
|
+
* answer this {@link @serenity-js/core/lib/screenplay~Question}.
|
|
69
|
+
*
|
|
70
|
+
* @param {AnswersQuestions & UsesAbilities} actor
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*
|
|
73
|
+
* @see {@link @serenity-js/core/lib/screenplay/actor~Actor}
|
|
74
|
+
* @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
|
|
75
|
+
* @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
|
|
76
|
+
*/
|
|
77
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string> {
|
|
78
|
+
const element = await this.resolve(actor, this.element);
|
|
79
|
+
|
|
80
|
+
return element.value();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './photographer';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { LogicError } from '@serenity-js/core';
|
|
2
|
+
import { ActivityFinished, ActivityStarts, DomainEvent } from '@serenity-js/core/lib/events';
|
|
3
|
+
import { Stage, StageCrewMember } from '@serenity-js/core/lib/stage';
|
|
4
|
+
|
|
5
|
+
import { PhotoTakingStrategy } from './strategies';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @desc
|
|
9
|
+
* The Photographer is a {@link @serenity-js/core/lib/stage~StageCrewMember} who takes screenshots
|
|
10
|
+
* of the web browser that the {@link @serenity-js/core/lib/screenplay/actor~Actor} in the spotlight is using.
|
|
11
|
+
*
|
|
12
|
+
* @example <caption>Assigning the Photographer to the Stage</caption>
|
|
13
|
+
* // wdio.conf.ts
|
|
14
|
+
* import { ArtifactArchiver } from '@serenity-js/core';
|
|
15
|
+
* import { Photographer, TakePhotosOfFailures } from '@serenity-js/webdriverio';
|
|
16
|
+
*
|
|
17
|
+
* export const config = {
|
|
18
|
+
*
|
|
19
|
+
* serenity: {
|
|
20
|
+
* crew: [
|
|
21
|
+
* ArtifactArchiver.storingArtifactsAt(process.cwd(), 'target/site/serenity'),
|
|
22
|
+
* Photographer.whoWill(TakePhotosOfFailures),
|
|
23
|
+
* ]
|
|
24
|
+
* },
|
|
25
|
+
*
|
|
26
|
+
* // ... rest of the config omitted for brevity
|
|
27
|
+
* };
|
|
28
|
+
*
|
|
29
|
+
* @example <caption>Taking photos upon failures only</caption>
|
|
30
|
+
*
|
|
31
|
+
* import { Photographer, TakePhotosOfFailures } from '@serenity-js/webdriverio';
|
|
32
|
+
*
|
|
33
|
+
* Photographer.whoWill(TakePhotosOfFailures)
|
|
34
|
+
*
|
|
35
|
+
* @example <caption>Taking photos of all the interactions</caption>
|
|
36
|
+
*
|
|
37
|
+
* import { Photographer, TakePhotosOfInteractions } from '@serenity-js/webdriverio';
|
|
38
|
+
*
|
|
39
|
+
* Photographer.whoWill(TakePhotosOfInteractions)
|
|
40
|
+
*
|
|
41
|
+
* @example <caption>Taking photos before and after all the interactions</caption>
|
|
42
|
+
*
|
|
43
|
+
* import { Photographer, TakePhotosBeforeAndAfterInteractions } from '@serenity-js/webdriverio';
|
|
44
|
+
*
|
|
45
|
+
* Photographer.whoWill(TakePhotosBeforeAndAfterInteractions)
|
|
46
|
+
*
|
|
47
|
+
* @see {@link @serenity-js/core/lib/stage~Stage}
|
|
48
|
+
* @see {@link TakePhotosBeforeAndAfterInteractions}
|
|
49
|
+
* @see {@link TakePhotosOfFailures}
|
|
50
|
+
* @see {@link TakePhotosOfInteractions}
|
|
51
|
+
*/
|
|
52
|
+
export class Photographer implements StageCrewMember {
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @desc
|
|
56
|
+
* Instantiates a new {@link Photographer} configured to take photos (screenshots)
|
|
57
|
+
* as per the specified {@link PhotoTakingStrategy}.
|
|
58
|
+
*
|
|
59
|
+
* @param {Function} strategy - A no-arg constructor function that instantiates a {@link PhotoTakingStrategy}.
|
|
60
|
+
* @returns {StageCrewMember}
|
|
61
|
+
*/
|
|
62
|
+
static whoWill(strategy: new () => PhotoTakingStrategy): StageCrewMember {
|
|
63
|
+
return new Photographer(new strategy());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {PhotoTakingStrategy} photoTakingStrategy
|
|
68
|
+
* @param {Stage} stage
|
|
69
|
+
*/
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly photoTakingStrategy: PhotoTakingStrategy,
|
|
72
|
+
private stage?: Stage,
|
|
73
|
+
) {
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @desc
|
|
78
|
+
* Creates a new instance of this {@link StageCrewMember} and assigns it to a given {@link Stage}.
|
|
79
|
+
*
|
|
80
|
+
* @param {Stage} stage - An instance of a {@link Stage} this {@link StageCrewMember} will be assigned to
|
|
81
|
+
* @returns {StageCrewMember} - A new instance of this {@link StageCrewMember}
|
|
82
|
+
*/
|
|
83
|
+
assignedTo(stage: Stage): StageCrewMember {
|
|
84
|
+
return new Photographer(this.photoTakingStrategy, stage);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @desc
|
|
89
|
+
* Handles {@link DomainEvent} objects emitted by the {@link Stage}
|
|
90
|
+
* this {@link StageCrewMember} is assigned to.
|
|
91
|
+
*
|
|
92
|
+
* @param {DomainEvent} event
|
|
93
|
+
* @returns {void}
|
|
94
|
+
*/
|
|
95
|
+
notifyOf(event: DomainEvent): void {
|
|
96
|
+
if (! this.stage) {
|
|
97
|
+
throw new LogicError(`Photographer needs to be assigned to the Stage before it can be notified of any DomainEvents`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (! this.stage.theShowHasStarted()) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (event instanceof ActivityStarts || event instanceof ActivityFinished) {
|
|
105
|
+
this.photoTakingStrategy.considerTakingPhoto(event, this.stage);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Stage } from '@serenity-js/core';
|
|
2
|
+
import { ActivityFinished, ActivityRelatedArtifactGenerated, ActivityStarts, AsyncOperationAttempted, AsyncOperationCompleted, AsyncOperationFailed, DomainEvent } from '@serenity-js/core/lib/events';
|
|
3
|
+
import { CorrelationId, Description, Name, Photo } from '@serenity-js/core/lib/model';
|
|
4
|
+
|
|
5
|
+
import { BrowseTheWeb } from '../../../../screenplay';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @desc
|
|
9
|
+
* Configures the {@link Photographer} to take photos (a.k.a. screenshots)
|
|
10
|
+
* of the {@link @serenity-js/core/lib/screenplay~Activity} performed
|
|
11
|
+
* by the {@link @serenity-js/core/lib/screenplay/actor~Actor} in the spotlight
|
|
12
|
+
* under specific conditions.
|
|
13
|
+
*
|
|
14
|
+
* @abstract
|
|
15
|
+
*/
|
|
16
|
+
export abstract class PhotoTakingStrategy {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @desc
|
|
20
|
+
* Takes a photo of the web browser held by the {@link @serenity-js/core/lib/screenplay/actor~Actor} in the spotlight.
|
|
21
|
+
*
|
|
22
|
+
* @param {@serenity-js/core/lib/events~ActivityStarts | @serenity-js/core/lib/events~ActivityFinished} event
|
|
23
|
+
* @param {@serenity-js/core/lib/stage~Stage} stage - the Stage that holds reference to the Actor in the spotlight
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*
|
|
26
|
+
* @see {@link @serenity-js/core/lib/stage~Stage#theActorInTheSpotlight}
|
|
27
|
+
*/
|
|
28
|
+
async considerTakingPhoto(event: ActivityStarts | ActivityFinished, stage: Stage): Promise<void> {
|
|
29
|
+
if (! this.shouldTakeAPhotoOf(event)) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let browseTheWeb: BrowseTheWeb;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
browseTheWeb = BrowseTheWeb.as(stage.theActorInTheSpotlight());
|
|
37
|
+
} catch {
|
|
38
|
+
// actor doesn't have a browser, abort
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const
|
|
43
|
+
id = CorrelationId.create(),
|
|
44
|
+
nameSuffix = this.photoNameFor(event);
|
|
45
|
+
|
|
46
|
+
stage.announce(new AsyncOperationAttempted(
|
|
47
|
+
new Description(`[Photographer:${ this.constructor.name }] Taking screenshot of '${ nameSuffix }'...`),
|
|
48
|
+
id,
|
|
49
|
+
));
|
|
50
|
+
|
|
51
|
+
let dialogIsPresent: boolean;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
dialogIsPresent = await browseTheWeb.modalDialog().then(dialog => dialog.isPresent());
|
|
55
|
+
|
|
56
|
+
if (dialogIsPresent) {
|
|
57
|
+
return stage.announce(new AsyncOperationCompleted(
|
|
58
|
+
new Description(`[${ this.constructor.name }] Aborted taking screenshot of '${ nameSuffix }' because of a modal dialog obstructing the view`),
|
|
59
|
+
id,
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return stage.announce(new AsyncOperationFailed(error, id));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const [ screenshot, capabilities ] = await Promise.all([
|
|
68
|
+
browseTheWeb.takeScreenshot(),
|
|
69
|
+
browseTheWeb.browserCapabilities(),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const
|
|
73
|
+
context = [ capabilities.platformName, capabilities.browserName, capabilities.browserVersion ],
|
|
74
|
+
photoName = this.combinedNameFrom(...context, nameSuffix);
|
|
75
|
+
|
|
76
|
+
stage.announce(new ActivityRelatedArtifactGenerated(
|
|
77
|
+
event.sceneId,
|
|
78
|
+
event.activityId,
|
|
79
|
+
photoName,
|
|
80
|
+
Photo.fromBase64(screenshot),
|
|
81
|
+
));
|
|
82
|
+
|
|
83
|
+
return stage.announce(new AsyncOperationCompleted(
|
|
84
|
+
new Description(`[${ this.constructor.name }] Took screenshot of '${ nameSuffix }' on ${ context }`),
|
|
85
|
+
id,
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (this.shouldIgnore(error)) {
|
|
90
|
+
stage.announce(new AsyncOperationCompleted(
|
|
91
|
+
new Description(`[${ this.constructor.name }] Aborted taking screenshot of '${ nameSuffix }' because of ${ error.constructor && error.constructor.name }`),
|
|
92
|
+
id,
|
|
93
|
+
));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
stage.announce(new AsyncOperationFailed(error, id));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected abstract shouldTakeAPhotoOf(event: DomainEvent): boolean;
|
|
102
|
+
|
|
103
|
+
protected abstract photoNameFor(event: DomainEvent): string;
|
|
104
|
+
|
|
105
|
+
private combinedNameFrom(...parts: string[]): Name {
|
|
106
|
+
return new Name(parts.filter(v => !! v).join('-'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private shouldIgnore(error: Error) {
|
|
110
|
+
return error.name
|
|
111
|
+
&& (error.name === 'NoSuchSessionError');
|
|
112
|
+
// todo: add SauceLabs
|
|
113
|
+
// [0-0] 2021-12-02T01:32:36.402Z ERROR webdriver: Request failed with status 404 due to no such window: no such window: target window already closed
|
|
114
|
+
// [0-0] from unknown error: web view not found
|
|
115
|
+
}
|
|
116
|
+
}
|