@serenity-js/playwright 3.0.0-rc.20
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 +30 -0
- package/LICENSE.md +201 -0
- package/NOTICE.md +1 -0
- package/README.md +145 -0
- package/lib/PlaywrightOptions.d.ts +40 -0
- package/lib/PlaywrightOptions.js +3 -0
- package/lib/PlaywrightOptions.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/screenplay/abilities/BrowseTheWebWithPlaywright.d.ts +71 -0
- package/lib/screenplay/abilities/BrowseTheWebWithPlaywright.js +86 -0
- package/lib/screenplay/abilities/BrowseTheWebWithPlaywright.js.map +1 -0
- package/lib/screenplay/abilities/index.d.ts +1 -0
- package/lib/screenplay/abilities/index.js +18 -0
- package/lib/screenplay/abilities/index.js.map +1 -0
- package/lib/screenplay/index.d.ts +2 -0
- package/lib/screenplay/index.js +19 -0
- package/lib/screenplay/index.js.map +1 -0
- package/lib/screenplay/models/PlaywrightBrowsingSession.d.ts +25 -0
- package/lib/screenplay/models/PlaywrightBrowsingSession.js +67 -0
- package/lib/screenplay/models/PlaywrightBrowsingSession.js.map +1 -0
- package/lib/screenplay/models/PlaywrightCookie.d.ts +8 -0
- package/lib/screenplay/models/PlaywrightCookie.js +39 -0
- package/lib/screenplay/models/PlaywrightCookie.js.map +1 -0
- package/lib/screenplay/models/PlaywrightModalDialogHandler.d.ts +13 -0
- package/lib/screenplay/models/PlaywrightModalDialogHandler.js +45 -0
- package/lib/screenplay/models/PlaywrightModalDialogHandler.js.map +1 -0
- package/lib/screenplay/models/PlaywrightPage.d.ts +58 -0
- package/lib/screenplay/models/PlaywrightPage.js +177 -0
- package/lib/screenplay/models/PlaywrightPage.js.map +1 -0
- package/lib/screenplay/models/PlaywrightPageElement.d.ts +24 -0
- package/lib/screenplay/models/PlaywrightPageElement.js +194 -0
- package/lib/screenplay/models/PlaywrightPageElement.js.map +1 -0
- package/lib/screenplay/models/index.d.ts +5 -0
- package/lib/screenplay/models/index.js +22 -0
- package/lib/screenplay/models/index.js.map +1 -0
- package/lib/screenplay/models/locators/PlaywrightLocator.d.ts +13 -0
- package/lib/screenplay/models/locators/PlaywrightLocator.js +77 -0
- package/lib/screenplay/models/locators/PlaywrightLocator.js.map +1 -0
- package/lib/screenplay/models/locators/PlaywrightRootLocator.d.ts +11 -0
- package/lib/screenplay/models/locators/PlaywrightRootLocator.js +26 -0
- package/lib/screenplay/models/locators/PlaywrightRootLocator.js.map +1 -0
- package/lib/screenplay/models/locators/index.d.ts +2 -0
- package/lib/screenplay/models/locators/index.js +19 -0
- package/lib/screenplay/models/locators/index.js.map +1 -0
- package/package.json +68 -0
- package/src/PlaywrightOptions.ts +43 -0
- package/src/index.ts +2 -0
- package/src/screenplay/abilities/BrowseTheWebWithPlaywright.ts +89 -0
- package/src/screenplay/abilities/index.ts +1 -0
- package/src/screenplay/index.ts +2 -0
- package/src/screenplay/models/PlaywrightBrowsingSession.ts +88 -0
- package/src/screenplay/models/PlaywrightCookie.ts +46 -0
- package/src/screenplay/models/PlaywrightModalDialogHandler.ts +53 -0
- package/src/screenplay/models/PlaywrightPage.ts +258 -0
- package/src/screenplay/models/PlaywrightPageElement.ts +235 -0
- package/src/screenplay/models/index.ts +5 -0
- package/src/screenplay/models/locators/PlaywrightLocator.ts +113 -0
- package/src/screenplay/models/locators/PlaywrightRootLocator.ts +29 -0
- package/src/screenplay/models/locators/index.ts +2 -0
- package/tsconfig.eslint.json +10 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CorrelationId } from '@serenity-js/core/lib/model';
|
|
2
|
+
import { BrowsingSession, Cookie, CookieData } from '@serenity-js/web';
|
|
3
|
+
import * as playwright from 'playwright-core';
|
|
4
|
+
|
|
5
|
+
import { PlaywrightOptions } from '../../PlaywrightOptions';
|
|
6
|
+
import { PlaywrightCookie, PlaywrightPage } from '../models';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @desc
|
|
10
|
+
* Playwright-specific implementation of the {@link @serenity-js/web/lib/screenplay/models~BrowsingSession}.
|
|
11
|
+
*
|
|
12
|
+
* @see {@link @serenity-js/web/lib/screenplay/models~BrowsingSession}
|
|
13
|
+
*/
|
|
14
|
+
export class PlaywrightBrowsingSession extends BrowsingSession<PlaywrightPage> {
|
|
15
|
+
|
|
16
|
+
private currentPlaywrightBrowserContext: playwright.BrowserContext;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
protected readonly browser: playwright.Browser,
|
|
20
|
+
protected readonly browserContextOptions: PlaywrightOptions,
|
|
21
|
+
) {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected async registerCurrentPage(): Promise<PlaywrightPage> {
|
|
26
|
+
const context = await this.browserContext();
|
|
27
|
+
|
|
28
|
+
await context.newPage();
|
|
29
|
+
|
|
30
|
+
// calling context.newPage() triggers a callback registered via browserContext(),
|
|
31
|
+
// which wraps playwright.Page in PlaywrightPage and adds it to the list of pages
|
|
32
|
+
// returned by this.allPages()
|
|
33
|
+
|
|
34
|
+
const allPages = await this.allPages()
|
|
35
|
+
|
|
36
|
+
return allPages[allPages.length-1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @override
|
|
41
|
+
*/
|
|
42
|
+
async closeAllPages(): Promise<void> {
|
|
43
|
+
await super.closeAllPages();
|
|
44
|
+
|
|
45
|
+
const context = await this.browserContext();
|
|
46
|
+
await context.close();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async cookie(name: string): Promise<Cookie> {
|
|
50
|
+
const context = await this.browserContext();
|
|
51
|
+
return new PlaywrightCookie(context, name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async setCookie(cookie: CookieData): Promise<void> {
|
|
55
|
+
const context = await this.browserContext();
|
|
56
|
+
|
|
57
|
+
await context.addCookies([ cookie ]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async deleteAllCookies(): Promise<void> {
|
|
61
|
+
const context = await this.browserContext()
|
|
62
|
+
await context.clearCookies();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async browserContext(): Promise<playwright.BrowserContext> {
|
|
66
|
+
if (! this.currentPlaywrightBrowserContext) {
|
|
67
|
+
this.currentPlaywrightBrowserContext = await this.browser.newContext(this.browserContextOptions);
|
|
68
|
+
|
|
69
|
+
this.currentPlaywrightBrowserContext.on('page', async page => {
|
|
70
|
+
await page.waitForLoadState();
|
|
71
|
+
|
|
72
|
+
this.register(
|
|
73
|
+
new PlaywrightPage(this, page, this.browserContextOptions, CorrelationId.create())
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (this.browserContextOptions?.defaultNavigationTimeout) {
|
|
78
|
+
this.currentPlaywrightBrowserContext.setDefaultNavigationTimeout(this.browserContextOptions?.defaultNavigationTimeout);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.browserContextOptions?.defaultTimeout) {
|
|
82
|
+
this.currentPlaywrightBrowserContext.setDefaultTimeout(this.browserContextOptions?.defaultTimeout);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return this.currentPlaywrightBrowserContext;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Timestamp } from '@serenity-js/core';
|
|
2
|
+
import { Cookie, CookieData, CookieMissingError } from '@serenity-js/web';
|
|
3
|
+
import * as playwright from 'playwright-core';
|
|
4
|
+
import { ensure, isDefined } from 'tiny-types';
|
|
5
|
+
|
|
6
|
+
export class PlaywrightCookie extends Cookie {
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly context: playwright.BrowserContext,
|
|
10
|
+
cookieName: string,
|
|
11
|
+
) {
|
|
12
|
+
super(cookieName);
|
|
13
|
+
ensure('context', context, isDefined());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async delete(): Promise<void> {
|
|
17
|
+
// see https://github.com/microsoft/playwright/issues/10143
|
|
18
|
+
const cookies = await this.context.cookies();
|
|
19
|
+
await this.context.clearCookies();
|
|
20
|
+
await this.context.addCookies(
|
|
21
|
+
cookies.filter(cookie => cookie.name !== this.cookieName)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected async read(): Promise<CookieData> {
|
|
26
|
+
const cookies = await this.context.cookies();
|
|
27
|
+
|
|
28
|
+
const found = cookies.find(cookie => cookie.name === this.cookieName);
|
|
29
|
+
|
|
30
|
+
if (! found) {
|
|
31
|
+
throw new CookieMissingError(`Cookie '${ this.cookieName }' not set`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: found.name,
|
|
36
|
+
value: found.value,
|
|
37
|
+
domain: found.domain,
|
|
38
|
+
path: found.path,
|
|
39
|
+
expiry: found.expires !== undefined
|
|
40
|
+
? Timestamp.fromTimestampInSeconds(Math.round(found.expires))
|
|
41
|
+
: undefined,
|
|
42
|
+
httpOnly: found.httpOnly,
|
|
43
|
+
secure: found.secure,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AbsentModalDialog, AcceptedModalDialog, DismissedModalDialog, ModalDialog, ModalDialogHandler } from '@serenity-js/web';
|
|
2
|
+
import * as playwright from 'playwright-core';
|
|
3
|
+
|
|
4
|
+
export class PlaywrightModalDialogHandler extends ModalDialogHandler {
|
|
5
|
+
|
|
6
|
+
private readonly defaultHandler: (dialog: playwright.Dialog) => Promise<ModalDialog> =
|
|
7
|
+
async (dialog: playwright.Dialog) => {
|
|
8
|
+
await dialog.dismiss();
|
|
9
|
+
return new DismissedModalDialog(dialog.message());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private currentHandler: (dialog: playwright.Dialog) => Promise<ModalDialog>;
|
|
13
|
+
|
|
14
|
+
constructor(private readonly page: playwright.Page) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
this.currentHandler = this.defaultHandler;
|
|
18
|
+
|
|
19
|
+
this.page.on('dialog', async (dialog: playwright.Dialog) => {
|
|
20
|
+
this.modalDialog = await this.currentHandler(dialog);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async acceptNext(): Promise<void> {
|
|
25
|
+
this.currentHandler = async (dialog: playwright.Dialog) => {
|
|
26
|
+
await dialog.accept(dialog.defaultValue());
|
|
27
|
+
return new AcceptedModalDialog(dialog.message());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async acceptNextWithValue(text: string | number): Promise<void> {
|
|
32
|
+
this.currentHandler = async (dialog: playwright.Dialog) => {
|
|
33
|
+
await dialog.accept(String(text));
|
|
34
|
+
return new AcceptedModalDialog(dialog.message());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async dismissNext(): Promise<void> {
|
|
39
|
+
this.currentHandler = async (dialog: playwright.Dialog) => {
|
|
40
|
+
await dialog.dismiss();
|
|
41
|
+
return new DismissedModalDialog(dialog.message());
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async reset(): Promise<void> {
|
|
46
|
+
this.modalDialog = new AbsentModalDialog();
|
|
47
|
+
this.currentHandler = this.defaultHandler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async discard(): Promise<void> {
|
|
51
|
+
this.page.off('dialog', this.currentHandler);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { LogicError } from '@serenity-js/core';
|
|
2
|
+
import { CorrelationId } from '@serenity-js/core/lib/model';
|
|
3
|
+
import { Cookie, CookieData, Key, Page, PageElement, PageElements, Selector } from '@serenity-js/web';
|
|
4
|
+
import * as Buffer from 'buffer';
|
|
5
|
+
import * as playwright from 'playwright-core';
|
|
6
|
+
import * as structs from 'playwright-core/types/structs';
|
|
7
|
+
import { URL } from 'url';
|
|
8
|
+
|
|
9
|
+
import { PlaywrightOptions } from '../../PlaywrightOptions';
|
|
10
|
+
import { PlaywrightLocator, PlaywrightRootLocator } from './locators';
|
|
11
|
+
import { PlaywrightBrowsingSession } from './PlaywrightBrowsingSession';
|
|
12
|
+
import { PlaywrightModalDialogHandler } from './PlaywrightModalDialogHandler';
|
|
13
|
+
import { PlaywrightPageElement } from './PlaywrightPageElement';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @desc
|
|
17
|
+
* Playwright-specific implementation of the {@link @serenity-js/web/lib/screenplay/models~Page}.
|
|
18
|
+
*
|
|
19
|
+
* @see {@link @serenity-js/web/lib/screenplay/models~Page}
|
|
20
|
+
*/
|
|
21
|
+
export class PlaywrightPage extends Page<playwright.ElementHandle> {
|
|
22
|
+
/**
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
private lastScriptExecutionSummary: LastScriptExecutionSummary;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
session: PlaywrightBrowsingSession,
|
|
29
|
+
private readonly page: playwright.Page,
|
|
30
|
+
private readonly options: PlaywrightOptions,
|
|
31
|
+
pageId: CorrelationId,
|
|
32
|
+
) {
|
|
33
|
+
super(
|
|
34
|
+
session,
|
|
35
|
+
new PlaywrightRootLocator(page),
|
|
36
|
+
new PlaywrightModalDialogHandler(page),
|
|
37
|
+
pageId
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
locate(selector: Selector): PageElement<playwright.ElementHandle> {
|
|
42
|
+
return new PlaywrightPageElement(
|
|
43
|
+
new PlaywrightLocator(
|
|
44
|
+
this.rootLocator,
|
|
45
|
+
selector,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
locateAll(selector: Selector): PageElements<playwright.ElementHandle> {
|
|
51
|
+
return new PageElements(
|
|
52
|
+
new PlaywrightLocator(
|
|
53
|
+
this.rootLocator,
|
|
54
|
+
selector
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async navigateTo(destination: string): Promise<void> {
|
|
60
|
+
await this.page.goto(destination, { waitUntil: this.options?.defaultNavigationWaitUntil });
|
|
61
|
+
await this.resetState();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async navigateBack(): Promise<void> {
|
|
65
|
+
await this.page.goBack({ waitUntil: this.options?.defaultNavigationWaitUntil });
|
|
66
|
+
await this.resetState();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async navigateForward(): Promise<void> {
|
|
70
|
+
await this.page.goForward({ waitUntil: this.options?.defaultNavigationWaitUntil });
|
|
71
|
+
await this.resetState();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async reload(): Promise<void> {
|
|
75
|
+
await this.page.reload({ waitUntil: this.options?.defaultNavigationWaitUntil });
|
|
76
|
+
await this.resetState();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async sendKeys(keys: (string | Key)[]): Promise<void> {
|
|
80
|
+
const keySequence = keys.map(key => {
|
|
81
|
+
if (! Key.isKey(key)) {
|
|
82
|
+
return key;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return key.devtoolsName;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await this.page.keyboard.press(
|
|
89
|
+
keySequence.join('+'),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async executeScript<Result, InnerArguments extends any[]>(script: string | ((...parameters: InnerArguments) => Result), ...args: InnerArguments): Promise<Result> {
|
|
94
|
+
|
|
95
|
+
const nativeArguments = await Promise.all(
|
|
96
|
+
args.map(arg =>
|
|
97
|
+
arg instanceof PlaywrightPageElement
|
|
98
|
+
? arg.nativeElement()
|
|
99
|
+
: arg
|
|
100
|
+
)
|
|
101
|
+
) as InnerArguments;
|
|
102
|
+
|
|
103
|
+
const serialisedScript = typeof script === 'function'
|
|
104
|
+
? String(script)
|
|
105
|
+
: String(`function script() { ${ script } }`);
|
|
106
|
+
|
|
107
|
+
const result = await this.page.evaluate<Result, typeof nativeArguments>(
|
|
108
|
+
new Function(`
|
|
109
|
+
const parameters = arguments[0];
|
|
110
|
+
return (${ serialisedScript }).apply(null, parameters);
|
|
111
|
+
`) as structs.PageFunction<typeof nativeArguments, Result>,
|
|
112
|
+
nativeArguments
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.lastScriptExecutionSummary = new LastScriptExecutionSummary(
|
|
116
|
+
result,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async executeAsyncScript<Result, Parameters extends any[]>(script: string | ((...args: [...parameters: Parameters, callback: (result: Result) => void]) => void), ...args: Parameters): Promise<Result> {
|
|
123
|
+
|
|
124
|
+
const nativeArguments = await Promise.all(
|
|
125
|
+
args.map(arg =>
|
|
126
|
+
arg instanceof PlaywrightPageElement
|
|
127
|
+
? arg.nativeElement()
|
|
128
|
+
: arg
|
|
129
|
+
)
|
|
130
|
+
) as Parameters;
|
|
131
|
+
|
|
132
|
+
const serialisedScript = typeof script === 'function'
|
|
133
|
+
? String(script)
|
|
134
|
+
: String(`function script() { ${ script } }`);
|
|
135
|
+
|
|
136
|
+
const result = await this.page.evaluate<Result, typeof nativeArguments>(
|
|
137
|
+
new Function(`
|
|
138
|
+
const parameters = arguments[0];
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
try {
|
|
142
|
+
return (${ serialisedScript }).apply(null, parameters.concat(resolve));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return reject(error);
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
`) as structs.PageFunction<typeof nativeArguments, Result>,
|
|
148
|
+
nativeArguments
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
this.lastScriptExecutionSummary = new LastScriptExecutionSummary(
|
|
152
|
+
result,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @desc
|
|
160
|
+
* Returns the last result of calling {@link BrowseTheWebWithPlaywright#executeAsyncScript}
|
|
161
|
+
* or {@link BrowseTheWebWithPlaywright#executeScript}
|
|
162
|
+
*
|
|
163
|
+
* @returns {any}
|
|
164
|
+
*/
|
|
165
|
+
lastScriptExecutionResult<Result = any>(): Result {
|
|
166
|
+
if (! this.lastScriptExecutionSummary) {
|
|
167
|
+
throw new LogicError(`Make sure to execute a script before checking on the result`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return this.lastScriptExecutionSummary.result as Result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async takeScreenshot(): Promise<string> {
|
|
174
|
+
const screenshot: Buffer = await this.page.screenshot();
|
|
175
|
+
|
|
176
|
+
return screenshot.toString('base64');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async cookie(name: string): Promise<Cookie> {
|
|
180
|
+
return (this.session as PlaywrightBrowsingSession).cookie(name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async setCookie(cookieData: CookieData): Promise<void> {
|
|
184
|
+
const url = await this.page.url();
|
|
185
|
+
|
|
186
|
+
const cookie = {
|
|
187
|
+
name: cookieData.name,
|
|
188
|
+
value: cookieData.value,
|
|
189
|
+
domain: cookieData.domain,
|
|
190
|
+
path: cookieData.path,
|
|
191
|
+
url: !(cookieData.domain && cookieData.path)
|
|
192
|
+
? url
|
|
193
|
+
: undefined,
|
|
194
|
+
secure: cookieData.secure,
|
|
195
|
+
httpOnly: cookieData.httpOnly,
|
|
196
|
+
expires: cookieData.expiry
|
|
197
|
+
? cookieData.expiry.toSeconds()
|
|
198
|
+
: undefined,
|
|
199
|
+
sameSite: cookieData.sameSite,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (this.session as PlaywrightBrowsingSession).setCookie(cookie);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async deleteAllCookies(): Promise<void> {
|
|
206
|
+
await (this.session as PlaywrightBrowsingSession).deleteAllCookies();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
title(): Promise<string> {
|
|
210
|
+
return this.page.title();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
name(): Promise<string> {
|
|
214
|
+
return this.page.evaluate(
|
|
215
|
+
/* istanbul ignore next */
|
|
216
|
+
() => window.name
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async url(): Promise<URL> {
|
|
221
|
+
return new URL(this.page.url());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async viewportSize(): Promise<{ width: number, height: number }> {
|
|
225
|
+
return this.page.viewportSize();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setViewportSize(size: { width: number, height: number }): Promise<void> {
|
|
229
|
+
return this.page.setViewportSize(size);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async close(): Promise<void> {
|
|
233
|
+
await this.resetState();
|
|
234
|
+
await (this.modalDialogHandler as PlaywrightModalDialogHandler).discard();
|
|
235
|
+
await this.page.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async closeOthers(): Promise<void> {
|
|
239
|
+
await this.session.closePagesOtherThan(this);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async isPresent(): Promise<boolean> {
|
|
243
|
+
return ! this.page.isClosed();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async resetState() {
|
|
247
|
+
this.lastScriptExecutionSummary = undefined;
|
|
248
|
+
await this.rootLocator.switchToMainFrame()
|
|
249
|
+
await this.modalDialogHandler.reset();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @package
|
|
255
|
+
*/
|
|
256
|
+
class LastScriptExecutionSummary<Result = any> {
|
|
257
|
+
constructor(public readonly result: Result) {}
|
|
258
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { LogicError } from '@serenity-js/core';
|
|
2
|
+
import { PageElement, SelectOption, SwitchableOrigin } from '@serenity-js/web';
|
|
3
|
+
import * as scripts from '@serenity-js/web/lib/scripts';
|
|
4
|
+
import * as playwright from 'playwright-core';
|
|
5
|
+
import { ensure, isDefined } from 'tiny-types';
|
|
6
|
+
|
|
7
|
+
import { PlaywrightLocator } from './locators';
|
|
8
|
+
|
|
9
|
+
export class PlaywrightPageElement extends PageElement<playwright.ElementHandle> {
|
|
10
|
+
of(parent: PageElement<playwright.ElementHandle>): PageElement<playwright.ElementHandle> {
|
|
11
|
+
return new PlaywrightPageElement(this.locator.of(parent.locator));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async enterValue(value: string | number | Array<string | number>): Promise<void> {
|
|
15
|
+
const text = [].concat(value).join('');
|
|
16
|
+
|
|
17
|
+
const element = await this.nativeElement();
|
|
18
|
+
return element.fill(text);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async clearValue(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const element = await this.nativeElement();
|
|
24
|
+
await element.fill('');
|
|
25
|
+
}
|
|
26
|
+
catch(error) {
|
|
27
|
+
throw new LogicError(`The input field doesn't seem to have a 'value' attribute that could be cleared.`, error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async click(): Promise<void> {
|
|
32
|
+
const element = await this.nativeElement();
|
|
33
|
+
return element.click();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async doubleClick(): Promise<void> {
|
|
37
|
+
const element = await this.nativeElement();
|
|
38
|
+
return element.dblclick();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async scrollIntoView(): Promise<void> {
|
|
42
|
+
const element = await this.nativeElement();
|
|
43
|
+
return element.scrollIntoViewIfNeeded();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async hoverOver(): Promise<void> {
|
|
47
|
+
const element = await this.nativeElement();
|
|
48
|
+
return element.hover();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async rightClick(): Promise<void> {
|
|
52
|
+
const element = await this.nativeElement();
|
|
53
|
+
return element.click({ button: 'right' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async selectOptions(...options: Array<SelectOption>): Promise<void> {
|
|
57
|
+
const element = await this.nativeElement();
|
|
58
|
+
|
|
59
|
+
const optionsToSelect = options.map(option =>
|
|
60
|
+
({
|
|
61
|
+
value: option.value,
|
|
62
|
+
label: option.label,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await element.selectOption(optionsToSelect);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async selectedOptions(): Promise<Array<SelectOption>> {
|
|
70
|
+
const element = await this.nativeElement();
|
|
71
|
+
|
|
72
|
+
const options = await element.$$eval(
|
|
73
|
+
'option',
|
|
74
|
+
/* istanbul ignore next */
|
|
75
|
+
(optionNodes: Array<HTMLOptionElement>) =>
|
|
76
|
+
optionNodes.map((optionNode: HTMLOptionElement) => {
|
|
77
|
+
return {
|
|
78
|
+
selected: optionNode.selected,
|
|
79
|
+
disabled: optionNode.disabled,
|
|
80
|
+
label: optionNode.label,
|
|
81
|
+
value: optionNode.value,
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return options.map(option =>
|
|
87
|
+
new SelectOption(option.label, option.value, option.selected, option.disabled)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async attribute(name: string): Promise<string> {
|
|
92
|
+
const element = await this.nativeElement();
|
|
93
|
+
return element.getAttribute(name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async text(): Promise<string> {
|
|
97
|
+
const element = await this.nativeElement();
|
|
98
|
+
return element.innerText(); // eslint-disable-line unicorn/prefer-dom-node-text-content
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async value(): Promise<string> {
|
|
102
|
+
const element = await this.nativeElement();
|
|
103
|
+
return element.inputValue();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async switchTo(): Promise<SwitchableOrigin> {
|
|
107
|
+
try {
|
|
108
|
+
const element = await this.nativeElement();
|
|
109
|
+
|
|
110
|
+
const frame = await element.contentFrame();
|
|
111
|
+
|
|
112
|
+
if (frame) {
|
|
113
|
+
const locator = (this.locator as PlaywrightLocator);
|
|
114
|
+
|
|
115
|
+
await locator.switchToFrame(element);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
switchBack: async (): Promise<void> => {
|
|
119
|
+
await locator.switchToParentFrame();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const previouslyFocusedElement = await element.evaluateHandle(
|
|
125
|
+
/* istanbul ignore next */
|
|
126
|
+
(domNode: HTMLElement) => {
|
|
127
|
+
const currentlyFocusedElement = document.activeElement;
|
|
128
|
+
domNode.focus();
|
|
129
|
+
return currentlyFocusedElement;
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return new PreviouslyFocusedElementSwitcher(previouslyFocusedElement);
|
|
134
|
+
} catch(error) {
|
|
135
|
+
throw new LogicError(`Couldn't switch to page element located ${ this.locator }`, error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async isActive(): Promise<boolean> {
|
|
140
|
+
try {
|
|
141
|
+
const element = await this.nativeElement();
|
|
142
|
+
return element.evaluate(
|
|
143
|
+
/* istanbul ignore next */
|
|
144
|
+
domNode => domNode === document.activeElement
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async isClickable(): Promise<boolean> {
|
|
152
|
+
try {
|
|
153
|
+
const element = await this.nativeElement();
|
|
154
|
+
await element.click({ trial: true });
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async isEnabled(): Promise<boolean> {
|
|
163
|
+
try {
|
|
164
|
+
const element = await this.nativeElement();
|
|
165
|
+
return element.isEnabled();
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async isPresent(): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
const element = await this.nativeElement();
|
|
174
|
+
return element !== null;
|
|
175
|
+
}
|
|
176
|
+
catch(error) {
|
|
177
|
+
if (error.name === 'TimeoutError') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async isSelected(): Promise<boolean> {
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const element: playwright.ElementHandle = await this.nativeElement();
|
|
188
|
+
|
|
189
|
+
// works for <option />
|
|
190
|
+
const selected = await element.getAttribute('selected');
|
|
191
|
+
if (selected !== null) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// works only for checkboxes and radio buttons, throws for other elements
|
|
196
|
+
return await element.isChecked();
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async isVisible(): Promise<boolean> {
|
|
203
|
+
try {
|
|
204
|
+
const element = await this.nativeElement();
|
|
205
|
+
|
|
206
|
+
const isVisible = await element.isVisible();
|
|
207
|
+
if (! isVisible) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return await element.evaluate(scripts.isVisible);
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
class PreviouslyFocusedElementSwitcher implements SwitchableOrigin {
|
|
222
|
+
constructor(private readonly node: playwright.JSHandle) {
|
|
223
|
+
ensure('DOM element', node, isDefined());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async switchBack (): Promise<void> {
|
|
227
|
+
await this.node.evaluate(
|
|
228
|
+
/* istanbul ignore next */
|
|
229
|
+
(domNode: HTMLElement) => {
|
|
230
|
+
domNode.focus();
|
|
231
|
+
},
|
|
232
|
+
this.node
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|