@serenity-js/web 3.0.0-rc.13 → 3.0.0-rc.14

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 (65) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/lib/errors/index.js +5 -1
  3. package/lib/errors/index.js.map +1 -1
  4. package/lib/expectations/index.js +5 -1
  5. package/lib/expectations/index.js.map +1 -1
  6. package/lib/expectations/isVisible.d.ts +4 -1
  7. package/lib/expectations/isVisible.js +4 -1
  8. package/lib/expectations/isVisible.js.map +1 -1
  9. package/lib/index.js +5 -1
  10. package/lib/index.js.map +1 -1
  11. package/lib/input/index.js +5 -1
  12. package/lib/input/index.js.map +1 -1
  13. package/lib/screenplay/abilities/index.js +5 -1
  14. package/lib/screenplay/abilities/index.js.map +1 -1
  15. package/lib/screenplay/index.js +5 -1
  16. package/lib/screenplay/index.js.map +1 -1
  17. package/lib/screenplay/interactions/index.js +5 -1
  18. package/lib/screenplay/interactions/index.js.map +1 -1
  19. package/lib/screenplay/models/PageElement.d.ts +9 -0
  20. package/lib/screenplay/models/PageElement.js +1 -2
  21. package/lib/screenplay/models/PageElement.js.map +1 -1
  22. package/lib/screenplay/models/PageElements.js +2 -2
  23. package/lib/screenplay/models/PageElements.js.map +1 -1
  24. package/lib/screenplay/models/index.js +5 -1
  25. package/lib/screenplay/models/index.js.map +1 -1
  26. package/lib/screenplay/models/selectors/index.js +5 -1
  27. package/lib/screenplay/models/selectors/index.js.map +1 -1
  28. package/lib/screenplay/questions/Attribute.d.ts +62 -25
  29. package/lib/screenplay/questions/Attribute.js +68 -27
  30. package/lib/screenplay/questions/Attribute.js.map +1 -1
  31. package/lib/screenplay/questions/CssClasses.d.ts +70 -26
  32. package/lib/screenplay/questions/CssClasses.js +76 -30
  33. package/lib/screenplay/questions/CssClasses.js.map +1 -1
  34. package/lib/screenplay/questions/Selected.d.ts +5 -5
  35. package/lib/screenplay/questions/Selected.js +2 -2
  36. package/lib/screenplay/questions/Selected.js.map +1 -1
  37. package/lib/screenplay/questions/Text.d.ts +22 -18
  38. package/lib/screenplay/questions/Text.js +61 -19
  39. package/lib/screenplay/questions/Text.js.map +1 -1
  40. package/lib/screenplay/questions/Value.d.ts +55 -16
  41. package/lib/screenplay/questions/Value.js +59 -18
  42. package/lib/screenplay/questions/Value.js.map +1 -1
  43. package/lib/screenplay/questions/index.js +5 -1
  44. package/lib/screenplay/questions/index.js.map +1 -1
  45. package/lib/stage/crew/index.js +5 -1
  46. package/lib/stage/crew/index.js.map +1 -1
  47. package/lib/stage/crew/photographer/index.js +5 -1
  48. package/lib/stage/crew/photographer/index.js.map +1 -1
  49. package/lib/stage/crew/photographer/strategies/index.js +5 -1
  50. package/lib/stage/crew/photographer/strategies/index.js.map +1 -1
  51. package/lib/stage/index.js +5 -1
  52. package/lib/stage/index.js.map +1 -1
  53. package/package.json +6 -6
  54. package/src/expectations/isVisible.ts +4 -1
  55. package/src/screenplay/models/PageElement.ts +11 -2
  56. package/src/screenplay/models/PageElements.ts +3 -3
  57. package/src/screenplay/questions/Attribute.ts +85 -34
  58. package/src/screenplay/questions/CssClasses.ts +82 -30
  59. package/src/screenplay/questions/Selected.ts +7 -7
  60. package/src/screenplay/questions/Text.ts +80 -25
  61. package/src/screenplay/questions/Value.ts +67 -20
  62. package/lib/screenplay/questions/ElementQuestion.d.ts +0 -33
  63. package/lib/screenplay/questions/ElementQuestion.js +0 -53
  64. package/lib/screenplay/questions/ElementQuestion.js.map +0 -1
  65. package/src/screenplay/questions/ElementQuestion.ts +0 -58
@@ -5,7 +5,10 @@ import { ElementExpectation } from './ElementExpectation';
5
5
 
6
6
  /**
7
7
  * @desc
8
- * Expectation that the element is present in the DOM of the page and visible.
8
+ * Expectation that the element is present in the DOM of the page and:
9
+ * - is not hidden, so doesn't have `display: none`, `visibility: hidden` or `opacity: 0`
10
+ * - is within the browser viewport
11
+ * - doesn't have its centre covered by other elements
9
12
  *
10
13
  * @returns {@serenity-js/core/lib/screenplay/questions~Expectation<boolean, Element<'async'>>}
11
14
  *
@@ -16,9 +16,8 @@ export abstract class PageElement<Native_Element_Type = any> implements Optional
16
16
  });
17
17
  }
18
18
 
19
- // todo: review usages and consider removing if not used
20
19
  static of<NET>(childElement: Answerable<PageElement<NET>>, parentElement: Answerable<PageElement<NET>>): QuestionAdapter<PageElement<NET>> {
21
- return Question.about(d`${ childElement } of ${ parentElement })`, async actor => {
20
+ return Question.about(d`${ childElement } of ${ parentElement }`, async actor => {
22
21
  const child = await actor.answer(childElement);
23
22
  const parent = await actor.answer(parentElement);
24
23
 
@@ -83,5 +82,15 @@ export abstract class PageElement<Native_Element_Type = any> implements Optional
83
82
  abstract isPresent(): Promise<boolean>;
84
83
 
85
84
  abstract isSelected(): Promise<boolean>;
85
+
86
+ /**
87
+ * @desc
88
+ * Checks if the PageElement:
89
+ * - is not hidden, so doesn't have CSS style like `display: none`, `visibility: hidden` or `opacity: 0`
90
+ * - is within the browser viewport
91
+ * - doesn't have its centre covered by other elements
92
+ *
93
+ * @returns {Promise<boolean>}
94
+ */
86
95
  abstract isVisible(): Promise<boolean>;
87
96
  }
@@ -1,4 +1,4 @@
1
- import { Answerable, f, List, MetaQuestion, Question } from '@serenity-js/core';
1
+ import { Answerable, List, MetaQuestion, Question } from '@serenity-js/core';
2
2
 
3
3
  import { BrowseTheWeb } from '../abilities';
4
4
  import { Locator } from './Locator';
@@ -22,7 +22,7 @@ export class PageElements<Native_Element_Type = any>
22
22
 
23
23
  of(parent: Answerable<PageElement<Native_Element_Type>>): PageElements<Native_Element_Type> {
24
24
  return new PageElements<Native_Element_Type>(relativeToParent(this.locator, parent))
25
- .describedAs(`<<${this.toString()}>>` + f`.of(${ parent })`);
25
+ .describedAs(`${ this.toString() } of ${ parent }`);
26
26
  }
27
27
  }
28
28
 
@@ -40,7 +40,7 @@ function relativeToDocumentRoot<Native_Element_Type>(selector: Answerable<Select
40
40
  * @package
41
41
  */
42
42
  function relativeToParent<Native_Element_Type>(relativeLocator: Answerable<Locator<Native_Element_Type>>, parent: Answerable<PageElement<Native_Element_Type>>): Question<Promise<Locator<Native_Element_Type>>> {
43
- return Question.about(relativeLocator.toString() + f`.of${ parent }`, async actor => {
43
+ return Question.about(`${ relativeLocator.toString() } of ${ parent }`, async actor => {
44
44
  const locator: Locator<Native_Element_Type> = await actor.answer(relativeLocator);
45
45
  const parentElement: PageElement<Native_Element_Type> = await actor.answer(parent);
46
46
 
@@ -1,12 +1,10 @@
1
- import { Answerable, AnswersQuestions, LogicError, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core';
1
+ import { Answerable, AnswersQuestions, d, LogicError, MetaQuestion, Question, QuestionAdapter, UsesAbilities } from '@serenity-js/core';
2
2
 
3
3
  import { PageElement } from '../models';
4
- import { ElementQuestion } from './ElementQuestion';
5
4
 
6
5
  /**
7
6
  * @desc
8
- * Returns the value of the given HTML attribute of a given {@link WebElement},
9
- * represented by Answerable<{@link @wdio/types~Element}>
7
+ * Returns the value of the specified HTML attribute of a given {@link PageElement}.
10
8
  *
11
9
  * @example <caption>Example widget</caption>
12
10
  * <ul id="shopping-list" data-items-left="2">
@@ -15,37 +13,60 @@ import { ElementQuestion } from './ElementQuestion';
15
13
  * <li data-state="buy">Chocolate<li>
16
14
  * </ul>
17
15
  *
18
- * @example <caption>Retrieve a HTML attribute of a given WebElement</caption>
16
+ * @example <caption>Retrieve an HTML attribute of a given PageElement</caption>
19
17
  * import { actorCalled } from '@serenity-js/core';
20
18
  * import { Ensure, equals } from '@serenity-js/assertions';
21
- * import { Attribute, by, BrowseTheWeb, Target } from '@serenity-js/webdriverio';
19
+ * import { Attribute, By, PageElement } from '@serenity-js/web';
20
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
22
21
  *
23
22
  * const shoppingList = () =>
24
- * Target.the('shopping list').located(by.id('shopping-list'))
23
+ * PageElement.located(By.id('shopping-list')).describedAs('shopping list');
25
24
  *
26
25
  * actorCalled('Lisa')
27
- * .whoCan(BrowseTheWeb.using(browser))
26
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
28
27
  * .attemptsTo(
29
- * Ensure.that(Attribute.called('data-items-left').of(shoppingList()), equals('2')),
28
+ * Ensure.that(
29
+ * Attribute.called('data-items-left').of(shoppingList()),
30
+ * equals('2')
31
+ * ),
32
+ * )
33
+ *
34
+ * @example <caption>Using Attribute as QuestionAdapter</caption>
35
+ * import { actorCalled } from '@serenity-js/core';
36
+ * import { Ensure, equals } from '@serenity-js/assertions';
37
+ * import { Attribute, By, PageElement } from '@serenity-js/web';
38
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
39
+ *
40
+ * const shoppingList = () =>
41
+ * PageElement.located(By.css('#shopping-list')).describedAs('shopping list')
42
+ *
43
+ * actorCalled('Lisa')
44
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
45
+ * .attemptsTo(
46
+ * Ensure.that(
47
+ * Attribute.called('id').of(shoppingList()).toLocaleUpperCase(),
48
+ * equals('SHOPPING-LIST')
49
+ * ),
30
50
  * )
31
51
  *
32
- * @example <caption>Find WebElements with a given attribute</caption>
52
+ * @example <caption>Find PageElements with a given attribute</caption>
33
53
  * import { actorCalled } from '@serenity-js/core';
34
54
  * import { Ensure, includes } from '@serenity-js/assertions';
35
- * import { Attribute, BrowseTheWeb, by, Target } from '@serenity-js/webdriverio';
55
+ * import { Attribute, By, PageElements } from '@serenity-js/web';
56
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
36
57
  *
37
58
  * class ShoppingList {
38
59
  * static items = () =>
39
- * Target.all('items')
40
- * .located(by.css('#shopping-list li'))
60
+ * PageElements.located(By.css('#shopping-list li'))
61
+ * .describedAs('items');
41
62
  *
42
63
  * static outstandingItems = () =>
43
- * ShoppingList.items
44
- * .where(Attribute.called('data-state'), includes('buy'))
64
+ * ShoppingList.items()
65
+ * .where(Attribute.called('data-state'), includes('buy'));
45
66
  * }
46
67
  *
47
68
  * actorCalled('Lisa')
48
- * .whoCan(BrowseTheWeb.using(browser))
69
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
49
70
  * .attemptsTo(
50
71
  * Ensure.that(
51
72
  * Text.ofAll(ShoppingList.outstandingItems()),
@@ -53,13 +74,18 @@ import { ElementQuestion } from './ElementQuestion';
53
74
  * ),
54
75
  * )
55
76
  *
56
- * @extends {ElementQuestion}
77
+ * @extends {@serenity-js/core/lib/screenplay~Question}
57
78
  * @implements {@serenity-js/core/lib/screenplay/questions~MetaQuestion}
58
79
  */
59
80
  export class Attribute
60
- extends ElementQuestion<Promise<string>>
81
+ extends Question<Promise<string>>
61
82
  implements MetaQuestion<Answerable<PageElement>, Promise<string>>
62
83
  {
84
+ /**
85
+ * @private
86
+ */
87
+ private subject: string;
88
+
63
89
  /**
64
90
  * @param {Answerable<string>} name
65
91
  * @returns {Attribute}
@@ -72,41 +98,66 @@ export class Attribute
72
98
  * @param {Answerable<string>} name
73
99
  * @param {@serenity-js/core/lib/screenplay~Answerable<Element>} [element]
74
100
  */
75
- constructor(
101
+ protected constructor(
76
102
  private readonly name: Answerable<string>,
77
103
  private readonly element?: Answerable<PageElement>,
78
104
  ) {
79
- super(`"${ name }" attribute of ${ element }`);
105
+ super();
106
+ this.subject = element
107
+ ? d`${ name } attribute of ${ element }`
108
+ : d`${ name } attribute`
80
109
  }
81
110
 
82
111
  /**
83
112
  * @desc
84
- * Resolves to the value of a HTML attribute of the `target` element,
85
- * located in the context of a `parent` element.
113
+ * Resolves to the value of an HTML attribute of the `target` element,
114
+ * located within the `parent` element.
86
115
  *
87
- * @param {Answerable<PageElement>} parent
88
- * @returns {Question<Promise<string[]>>}
116
+ * @param {@serenity-js/core/lib/screenplay~Answerable<PageElement>} parent
117
+ * @returns {@serenity-js/core/lib/screenplay~QuestionAdapter<string>}
89
118
  *
90
- * @see {@link Target.all}
91
119
  * @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
92
120
  */
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
- );
121
+ of(parent: Answerable<PageElement>): QuestionAdapter<string> & MetaQuestion<Answerable<PageElement>, Promise<string>> {
122
+ return Question.createAdapter(
123
+ new Attribute(
124
+ this.name,
125
+ this.element
126
+ ? PageElement.of(this.element, parent)
127
+ : parent
128
+ )
129
+ ) as QuestionAdapter<string> & MetaQuestion<Answerable<PageElement>, Promise<string>>;
100
130
  }
101
131
 
102
132
  async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string> {
133
+ const name = await actor.answer(this.name);
134
+
103
135
  if (! this.element) {
104
- throw new LogicError(`Target not specified`); // todo: better error message?
136
+ throw new LogicError(d`Couldn't read attribute ${ name } of an unspecified page element.`);
105
137
  }
106
138
 
107
139
  const element = await actor.answer(this.element);
108
- const name = await actor.answer(this.name);
109
140
 
110
141
  return element.attribute(name);
111
142
  }
143
+
144
+ /**
145
+ * @desc
146
+ * Changes the description of this question's subject.
147
+ *
148
+ * @param {string} subject
149
+ * @returns {Question<T>}
150
+ */
151
+ describedAs(subject: string): this {
152
+ this.subject = subject;
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * @returns {string}
158
+ * Returns a human-readable representation of this {@link @serenity-js/core/lib/screenplay~Question}.
159
+ */
160
+ toString(): string {
161
+ return this.subject;
162
+ }
112
163
  }
@@ -1,13 +1,11 @@
1
- import { Answerable, AnswersQuestions, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core';
2
- import { formatted } from '@serenity-js/core/lib/io';
1
+ import { Answerable, AnswersQuestions, d, MetaQuestion, Question, QuestionAdapter, UsesAbilities } from '@serenity-js/core';
3
2
 
4
3
  import { PageElement } from '../models';
5
- import { ElementQuestion } from './ElementQuestion';
6
4
 
7
5
  /**
8
6
  * @desc
9
7
  * 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}>.
8
+ * of a given {@link PageElement}.
11
9
  *
12
10
  * @example <caption>Example widget</caption>
13
11
  * <ul id="shopping-list" class="active favourite">
@@ -16,37 +14,64 @@ import { ElementQuestion } from './ElementQuestion';
16
14
  * <li class="buy">Chocolate<li>
17
15
  * </ul>
18
16
  *
19
- * @example <caption>Retrieve CSS classes of a given WebElement</caption>
17
+ * @example <caption>Retrieve CSS classes of a given PageElement</caption>
20
18
  * import { actorCalled } from '@serenity-js/core';
21
19
  * import { Ensure, equals } from '@serenity-js/assertions';
22
- * import { BrowseTheWeb, by, CssClasses, Target } from '@serenity-js/webdriverio';
20
+ * import { By, CssClasses, PageElement } from '@serenity-js/web';
21
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
23
22
  *
24
23
  * const shoppingList = () =>
25
- * Target.the('shopping list').located(by.id('shopping-list'))
24
+ * PageElement.located(By.css('#shopping-list')).describedAs('shopping list')
26
25
  *
27
26
  * actorCalled('Lisa')
28
- * .whoCan(BrowseTheWeb.using(browser))
27
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
29
28
  * .attemptsTo(
30
- * Ensure.that(CssClasses.of(shoppingList()), equals([ 'active', 'favourite' ])),
29
+ * Ensure.that(
30
+ * CssClasses.of(shoppingList()),
31
+ * equals([ 'active', 'favourite' ])
32
+ * ),
33
+ * )
34
+ *
35
+ * @example <caption>Using CssClasses as QuestionAdapter</caption>
36
+ * import { actorCalled } from '@serenity-js/core';
37
+ * import { Ensure, equals } from '@serenity-js/assertions';
38
+ * import { By, CssClasses, PageElement } from '@serenity-js/web';
39
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
40
+ *
41
+ * const shoppingList = () =>
42
+ * PageElement.located(By.css('#shopping-list')).describedAs('shopping list')
43
+ *
44
+ * actorCalled('Lisa')
45
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
46
+ * .attemptsTo(
47
+ * Ensure.that(
48
+ * CssClasses.of(shoppingList()).length,
49
+ * equals(2)
50
+ * ),
51
+ * Ensure.that(
52
+ * CssClasses.of(shoppingList())[0],
53
+ * equals('active')
54
+ * ),
31
55
  * )
32
56
  *
33
- * @example <caption>Find WebElements with a given class</caption>
57
+ * @example <caption>Find PageElements with a given class</caption>
34
58
  * import { actorCalled } from '@serenity-js/core';
35
59
  * import { Ensure, contain } from '@serenity-js/assertions';
36
- * import { BrowseTheWeb, by, CssClasses, Target } from '@serenity-js/webdriverio';
60
+ * import { By, CssClasses, PageElement } from '@serenity-js/web';
61
+ * import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio';
37
62
  *
38
63
  * class ShoppingList {
39
64
  * static items = () =>
40
- * Target.all('items')
41
- * .located(by.css('#shopping-list li'))
65
+ * PageElements.located(By.css('#shopping-list li'))
66
+ * .describedAs('items')
42
67
  *
43
68
  * static outstandingItems = () =>
44
- * ShoppingList.items
69
+ * ShoppingList.items()
45
70
  * .where(CssClasses, contain('buy'))
46
71
  * }
47
72
  *
48
73
  * actorCalled('Lisa')
49
- * .whoCan(BrowseTheWeb.using(browser))
74
+ * .whoCan(BrowseTheWebWithWebdriverIO.using(browser))
50
75
  * .attemptsTo(
51
76
  * Ensure.that(
52
77
  * Text.ofAll(ShoppingList.outstandingItems()),
@@ -54,41 +79,48 @@ import { ElementQuestion } from './ElementQuestion';
54
79
  * ),
55
80
  * )
56
81
  *
57
- * @extends {ElementQuestion}
82
+ * @extends {@serenity-js/core/lib/screenplay~Question}
58
83
  * @implements {@serenity-js/core/lib/screenplay/questions~MetaQuestion}
59
84
  */
60
85
  export class CssClasses
61
- extends ElementQuestion<Promise<string[]>>
86
+ extends Question<Promise<string[]>>
62
87
  implements MetaQuestion<Answerable<PageElement>, Promise<string[]>>
63
88
  {
64
89
  /**
65
- * @param {Question<PageElement> | PageElement} target
66
- * @returns {CssClasses}
90
+ * @private
67
91
  */
68
- static of(target: Answerable<PageElement>): CssClasses {
69
- return new CssClasses(target);
92
+ private subject: string;
93
+
94
+ /**
95
+ * @param {@serenity-js/core/lib/screenplay~Answerable<PageElement>} pageElement
96
+ * @returns {@serenity-js/core/lib/screenplay~QuestionAdapter<string[]>}
97
+ *
98
+ * @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
99
+ */
100
+ static of(pageElement: Answerable<PageElement>): QuestionAdapter<string[]> & MetaQuestion<Answerable<PageElement>, Promise<string[]>> {
101
+ return Question.createAdapter(new CssClasses(pageElement)) as QuestionAdapter<string[]> & MetaQuestion<Answerable<PageElement>, Promise<string[]>>;
70
102
  }
71
103
 
72
104
  /**
73
- * @param {Question<PageElement> | PageElement} target
105
+ * @param {@serenity-js/core/lib/screenplay~Answerable<PageElement>} pageElement
74
106
  */
75
- constructor(private readonly target: Answerable<PageElement>) {
76
- super(formatted `CSS classes of ${ target}`);
107
+ protected constructor(private readonly pageElement: Answerable<PageElement>) {
108
+ super();
109
+ this.subject = d`CSS classes of ${ pageElement}`;
77
110
  }
78
111
 
79
112
  /**
80
113
  * @desc
81
- * Resolves to an array of CSS classes of the `target` element,
82
- * located in the context of a `parent` element.
114
+ * Resolves to an array of CSS classes of the `pageElement`,
115
+ * located within the `parent` element.
83
116
  *
84
- * @param {@serenity-js/core/lib/screenplay~Answerable<Element>} parent
117
+ * @param {@serenity-js/core/lib/screenplay~Answerable<PageElement>} parent
85
118
  * @returns {Question<Promise<string[]>>}
86
119
  *
87
- * @see {@link Target.all}
88
120
  * @see {@link @serenity-js/core/lib/screenplay/questions~MetaQuestion}
89
121
  */
90
122
  of(parent: Answerable<PageElement>): Question<Promise<string[]>> {
91
- return new CssClasses(PageElement.of(this.target, parent));
123
+ return new CssClasses(PageElement.of(this.pageElement, parent));
92
124
  }
93
125
 
94
126
  /**
@@ -104,7 +136,7 @@ export class CssClasses
104
136
  * @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
105
137
  */
106
138
  async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string[]> {
107
- const element = await this.resolve(actor, this.target);
139
+ const element = await actor.answer(this.pageElement);
108
140
 
109
141
  return element.attribute('class')
110
142
  .then(attribute => attribute ?? '')
@@ -115,4 +147,24 @@ export class CssClasses
115
147
  .filter(cssClass => !! cssClass),
116
148
  );
117
149
  }
150
+
151
+ /**
152
+ * @desc
153
+ * Changes the description of this question's subject.
154
+ *
155
+ * @param {string} subject
156
+ * @returns {Question<T>}
157
+ */
158
+ describedAs(subject: string): this {
159
+ this.subject = subject;
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * @returns {string}
165
+ * Returns a human-readable representation of this {@link @serenity-js/core/lib/screenplay~Question}.
166
+ */
167
+ toString(): string {
168
+ return this.subject;
169
+ }
118
170
  }
@@ -1,4 +1,4 @@
1
- import { Answerable, Question } from '@serenity-js/core';
1
+ import { Answerable, List, Question } from '@serenity-js/core';
2
2
  import { formatted } from '@serenity-js/core/lib/io';
3
3
 
4
4
  import { By, PageElement, PageElements } from '../models';
@@ -99,15 +99,15 @@ export class Selected {
99
99
  * @param {Answerable<PageElement>} pageElement
100
100
  * A {@link Target} identifying the `<select>` element of interest
101
101
  *
102
- * @returns {Question<Promise<string[]>>}
102
+ * @returns {@serenity-js/core/lib/screenplay/questions~List<string>}
103
103
  *
104
104
  * @see {@link Select.values}
105
105
  */
106
- static valuesOf(pageElement: Answerable<PageElement>): Question<Promise<string[]>> {
106
+ static valuesOf(pageElement: Answerable<PageElement>): List<string> {
107
107
  return PageElements.located(By.css('option:checked'))
108
108
  .of(pageElement)
109
109
  .eachMappedTo(Value)
110
- .describedAs(formatted `values selected in ${ pageElement }`) as Question<Promise<string[]>>;
110
+ .describedAs(formatted `values selected in ${ pageElement }`);
111
111
  }
112
112
 
113
113
  /**
@@ -201,14 +201,14 @@ export class Selected {
201
201
  * @param {Answerable<PageElement>} pageElement
202
202
  * A {@link Target} identifying the `<select>` element of interest
203
203
  *
204
- * @returns {Question<Promise<string[]>>}
204
+ * @returns {@serenity-js/core/lib/screenplay/questions~List<string>}
205
205
  *
206
206
  * @see {@link Select.options}
207
207
  */
208
- static optionsIn(pageElement: Answerable<PageElement>): Question<Promise<string[]>> {
208
+ static optionsIn(pageElement: Answerable<PageElement>): List<string> {
209
209
  return PageElements.located(By.css('option:checked'))
210
210
  .of(pageElement)
211
211
  .eachMappedTo(Text)
212
- .describedAs(formatted `options selected in ${ pageElement }`) as Question<Promise<string[]>>;
212
+ .describedAs(formatted `options selected in ${ pageElement }`);
213
213
  }
214
214
  }