pomwright 1.2.0 → 1.4.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 +43 -0
- package/README.md +38 -494
- package/dist/index.d.mts +11 -103
- package/dist/index.d.ts +11 -103
- package/dist/index.js +46 -115
- package/dist/index.mjs +43 -112
- package/docs/BaseApi-explanation.md +63 -0
- package/docs/BasePage-explanation.md +96 -0
- package/docs/LocatorSchema-explanation.md +271 -0
- package/docs/LocatorSchemaPath-explanation.md +165 -0
- package/docs/PlaywrightReportLogger-explanation.md +56 -0
- package/docs/get-locator-methods-explanation.md +250 -0
- package/docs/intro-to-using-pomwright.md +899 -0
- package/docs/sessionStorage-methods-explanation.md +38 -0
- package/docs/tips-folder-structure.md +38 -0
- package/package.json +8 -8
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
# Using POMWright
|
|
2
|
+
|
|
3
|
+
POMWright is a small TypeScript-based framework implementing the Page Object Model Design Pattern for Playwright.
|
|
4
|
+
|
|
5
|
+
POMWright has Playwright/test as a peer dependency and will use whichever supported installed version of Playwright.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install pomwright --save-dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm i -D pomwright
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
To begin using POMWright you don't need much, be it an existing or new Playwright project the approach is the same.
|
|
26
|
+
|
|
27
|
+
> Tip: Implementing POMWright in an existing Playwright project can be done in increments by implementing new Page Object Classes (POC's) with POMWright and/or refactoring existing POC's to POMWright one at a time. POMWright is just TypeScript and Playwright, and can be used along side your existing Playwright code in tests.
|
|
28
|
+
|
|
29
|
+
### Creating a Page Object Class (POC)
|
|
30
|
+
|
|
31
|
+
Lets begin by creating a Page Object Class for a simple login page using POMWright.
|
|
32
|
+
|
|
33
|
+
#### First we'll need a few imports
|
|
34
|
+
|
|
35
|
+
```TS
|
|
36
|
+
// login.page.ts
|
|
37
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
38
|
+
import { BasePage, type PlaywrightReportLogger, GetByMethod } from "pomwright";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From playwright we import:
|
|
42
|
+
|
|
43
|
+
- [Page](https://playwright.dev/docs/api/class-page) - Page provides methods to interact with a single tab in a Browser.
|
|
44
|
+
- [TestInfo](https://playwright.dev/docs/api/class-testinfo) - TestInfo contains information about the currently running test.
|
|
45
|
+
|
|
46
|
+
From POMWright we import:
|
|
47
|
+
|
|
48
|
+
- [BasePage](../src/basePage.ts) - An abstract class which is the foundation of all POC's in POMWright.
|
|
49
|
+
- [PlaywrightReportLogger](../src/helpers/playwrightReportLogger.ts) - A custom logger which records log messages and attaches them to the Playwright HTML report in chronological order (timestamp) per test.
|
|
50
|
+
- [GetByMethod](../src/helpers/locatorSchema.interface.ts#L7) - Dictates which Playwright Locator method POMWright uses for a given LocatorSchema when creating single or nested Locators.
|
|
51
|
+
|
|
52
|
+
#### We can now create a minimal implementation of our POC
|
|
53
|
+
|
|
54
|
+
```TS
|
|
55
|
+
// login.page.ts
|
|
56
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
57
|
+
import { BasePage, GetByMethod, type PlaywrightReportLogger } from "pomwright";
|
|
58
|
+
|
|
59
|
+
type LocatorSchemaPath = "";
|
|
60
|
+
|
|
61
|
+
export default class Login extends BasePage<LocatorSchemaPath> {
|
|
62
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
63
|
+
super(page, testInfo, "https://someDomain.com", "/login", Login.name, pwrl);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected initLocatorSchemas() {}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
As you can see, when we extend our POC with `BasePage` we must provide a type called `LocatorSchemaPath` and our POC must also implement the abstract method `initLocatorSchemas()`. For now you only need to know that they are needed, they will be explained in a moment.
|
|
71
|
+
|
|
72
|
+
The constructor has to invoke super with the following parameters:
|
|
73
|
+
|
|
74
|
+
- page - see [Page](https://playwright.dev/docs/api/class-page)
|
|
75
|
+
- testInfo - see [TestInfo](https://playwright.dev/docs/api/class-testinfo)
|
|
76
|
+
- baseUrl - a string, similar to [BaseURL](https://playwright.dev/docs/api/class-testoptions#test-options-base-url) in playwright.config
|
|
77
|
+
- urlPath - a string, the resource path of the POCs page URL
|
|
78
|
+
- pocName - a string, the POCs human-readable name, used for logging and enriching the playwright HTML report
|
|
79
|
+
- pwrl - see [PlaywrightReportLogger](../src/helpers/playwrightReportLogger.ts)
|
|
80
|
+
|
|
81
|
+
Since each POC we create will be initialized as a custom Playwright fixture, we'll need to provide atleast three of these parameters to the constructor, namely [Page](https://playwright.dev/docs/api/class-page), [TestInfo](https://playwright.dev/docs/api/class-testinfo) and [PlaywrightReportLogger](../src/fixture/base.fixtures.ts), as they will be provided by their respective fixtures. The rest can be defined in the POC as above or in the custom fixture initializing the POC.
|
|
82
|
+
|
|
83
|
+
For an indepth explanation of BasePage see [BasePage-explanation.md](BasePage-explanation.md)
|
|
84
|
+
|
|
85
|
+
#### We can now define the POC's LocatorSchemaPath's and corresponding LocatorSchema
|
|
86
|
+
|
|
87
|
+
Playwright Docs loosely explain the concept of the [Page Object Model Pattern](https://playwright.dev/docs/pom) with some examples. The principle is the same but the structure and how we go about it is a bit different in POMWright.
|
|
88
|
+
|
|
89
|
+
Instead of defining Locators as properties or methods in our POC's, we define them through [LocatorSchema](./LocatorSchema-explanation.md)'s with their own unique [LocatorSchemaPath](./LocatorSchemaPath-explanation.md)'s. In short, each Page Object Class (POC) extending BasePage should define its own LocatorSchemaPath Type, which is a union of strings with the following rules:
|
|
90
|
+
|
|
91
|
+
1. The only character of significance is `.` (dot/period), any other single character or combination of characters are considered words (human readable).
|
|
92
|
+
2. A LocatorSchemaPath string must start and end with a word.
|
|
93
|
+
3. Words are seperated by `.` (dot/period).
|
|
94
|
+
4. Each LocatorSchemaPath string must be unique within its scope.
|
|
95
|
+
|
|
96
|
+
In practice, a LocatorSchemaPath functions as a unique identifier. While the rules are straightforward, there are important nuances. Because LocatorSchemaPath is a core concept of POMWright, I strongly recommend reading the more in-depth explanation [here](./LocatorSchemaPath-explanation.md) before you proceed.
|
|
97
|
+
|
|
98
|
+
Back to our example, lets add some LocatorSchemaPath's to our POC representing our simple login page:
|
|
99
|
+
|
|
100
|
+
```TS
|
|
101
|
+
// login.page.ts
|
|
102
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
103
|
+
import { BasePage, GetByMethod, type PlaywrightReportLogger } from "pomwright";
|
|
104
|
+
|
|
105
|
+
type LocatorSchemaPath =
|
|
106
|
+
| "main"
|
|
107
|
+
| "main.section@login"
|
|
108
|
+
| "main.section@login.heading"
|
|
109
|
+
| "main.section@login.form"
|
|
110
|
+
| "main.section@login.form.input@email"
|
|
111
|
+
| "main.section@login.form.input@password"
|
|
112
|
+
| "main.section@login.form.button@login"
|
|
113
|
+
| "main.section@login.link@createUser";
|
|
114
|
+
|
|
115
|
+
export default class Login extends BasePage<LocatorSchemaPath> {
|
|
116
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
117
|
+
super(page, testInfo, "https://someDomain.com", "/login", Login.name, pwrl);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected initLocatorSchemas() {}
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
We havn't introduced them yet, but as it stands, if we were to invoke any of POMWright's methods with any of these LocatorSchemaPath's we'd get a "not implemented" error, thus we need to add our LocatorSchema's which our LocatorSchemaPath's reference.
|
|
126
|
+
|
|
127
|
+
The LocatorSchema interface lets us define an object for creating a Locator with any of Playwright's locator methods, I advise you to read the more in-depth explanation [here](./LocatorSchema-explanation.md).
|
|
128
|
+
|
|
129
|
+
Now lets add our LocatorSchema, we do this through the `initLocatorSchemas()` method, through the POC's `locators` property which it gets from extending BasePage. The `locators` property is an instance of the GetLocatorBase class, which handles LocatorSchema management and provides the POC with POMWright's locator methods, see [get-locator-methods-explanation.md](./get-locator-methods-explanation.md) for further details.
|
|
130
|
+
|
|
131
|
+
```TS
|
|
132
|
+
// login.page.ts
|
|
133
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
134
|
+
import { BasePage, GetByMethod, type PlaywrightReportLogger } from "pomwright";
|
|
135
|
+
|
|
136
|
+
type LocatorSchemaPath =
|
|
137
|
+
| "main"
|
|
138
|
+
| "main.section@login"
|
|
139
|
+
| "main.section@login.heading"
|
|
140
|
+
| "main.section@login.form"
|
|
141
|
+
| "main.section@login.form.input@email"
|
|
142
|
+
| "main.section@login.form.input@password"
|
|
143
|
+
| "main.section@login.form.button@login"
|
|
144
|
+
| "main.section@login.link@createUser";
|
|
145
|
+
|
|
146
|
+
export default class Login extends BasePage<LocatorSchemaPath> {
|
|
147
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
148
|
+
super(page, testInfo, "https://someDomain.com", "/login", Login.name, pwrl);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected initLocatorSchemas() {
|
|
152
|
+
this.locators.addSchema("main", {
|
|
153
|
+
locator: "main",
|
|
154
|
+
locatorMethod: GetByMethod.locator
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.locators.addSchema("main.section@login", {
|
|
158
|
+
role: "region",
|
|
159
|
+
roleOptions: {
|
|
160
|
+
name: "Login"
|
|
161
|
+
},
|
|
162
|
+
locatorMethod: GetByMethod.role
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.locators.addSchema("main.section@login.heading", {
|
|
166
|
+
role: "heading",
|
|
167
|
+
roleOptions: {
|
|
168
|
+
name: "Welcome",
|
|
169
|
+
level: 1
|
|
170
|
+
},
|
|
171
|
+
locatorMethod: GetByMethod.role
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this.locators.addSchema("main.section@login.form", {
|
|
175
|
+
role: "form",
|
|
176
|
+
locatorMethod: GetByMethod.role
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.locators.addSchema("main.section@login.form.input@email", {
|
|
180
|
+
role: "textbox",
|
|
181
|
+
roleOptions: {
|
|
182
|
+
name: "E-mail"
|
|
183
|
+
},
|
|
184
|
+
locatorMethod: GetByMethod.role
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this.locators.addSchema("main.section@login.form.input@password", {
|
|
188
|
+
role: "textbox",
|
|
189
|
+
roleOptions: {
|
|
190
|
+
name: "Password"
|
|
191
|
+
},
|
|
192
|
+
locatorMethod: GetByMethod.role
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.locators.addSchema("main.section@login.form.button@login", {
|
|
196
|
+
role: "button",
|
|
197
|
+
roleOptions: {
|
|
198
|
+
name: "Login"
|
|
199
|
+
},
|
|
200
|
+
locatorMethod: GetByMethod.role
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.locators.addSchema("main.section@login.link@createUser", {
|
|
204
|
+
role: "link",
|
|
205
|
+
roleOptions: {
|
|
206
|
+
name: "Create an account"
|
|
207
|
+
},
|
|
208
|
+
locatorMethod: GetByMethod.role
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### We can then create helper methods for our POC which uses the LocatorSchema's we defined
|
|
215
|
+
|
|
216
|
+
For an indepth explanation of POMWright's locator methods, see [get-locator-methods-explanation.md](./get-locator-methods-explanation.md)
|
|
217
|
+
|
|
218
|
+
To make sure we can use Playwright test.step in our helper method, we'll import test from Playwright through POMWright.
|
|
219
|
+
|
|
220
|
+
```TS
|
|
221
|
+
// login.page.ts
|
|
222
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
223
|
+
import { BasePage, GetByMethod, type PlaywrightReportLogger, test } from "pomwright";
|
|
224
|
+
import User from "@test-data/user.type";
|
|
225
|
+
|
|
226
|
+
type LocatorSchemaPath =
|
|
227
|
+
| "main"
|
|
228
|
+
| "main.section@login"
|
|
229
|
+
| "main.section@login.heading"
|
|
230
|
+
| "main.section@login.form"
|
|
231
|
+
| "main.section@login.form.input@email"
|
|
232
|
+
| "main.section@login.form.input@password"
|
|
233
|
+
| "main.section@login.form.button@login"
|
|
234
|
+
| "main.section@login.link@createUser";
|
|
235
|
+
|
|
236
|
+
export default class Login extends BasePage<LocatorSchemaPath> {
|
|
237
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
238
|
+
super(page, testInfo, "https://someDomain.com", "/login", Login.name, pwrl);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected initLocatorSchemas() {
|
|
242
|
+
this.locators.addSchema("main", {
|
|
243
|
+
locator: "main",
|
|
244
|
+
locatorMethod: GetByMethod.locator
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.locators.addSchema("main.section@login", {
|
|
248
|
+
role: "region",
|
|
249
|
+
roleOptions: {
|
|
250
|
+
name: "Login"
|
|
251
|
+
},
|
|
252
|
+
locatorMethod: GetByMethod.role
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.locators.addSchema("main.section@login.heading", {
|
|
256
|
+
role: "heading",
|
|
257
|
+
roleOptions: {
|
|
258
|
+
name: "Welcome",
|
|
259
|
+
level: 1
|
|
260
|
+
},
|
|
261
|
+
locatorMethod: GetByMethod.role
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
this.locators.addSchema("main.section@login.form", {
|
|
265
|
+
role: "form",
|
|
266
|
+
locatorMethod: GetByMethod.role
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.locators.addSchema("main.section@login.form.input@email", {
|
|
270
|
+
role: "textbox",
|
|
271
|
+
roleOptions: {
|
|
272
|
+
name: "E-mail"
|
|
273
|
+
},
|
|
274
|
+
locatorMethod: GetByMethod.role
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
this.locators.addSchema("main.section@login.form.input@password", {
|
|
278
|
+
role: "textbox",
|
|
279
|
+
roleOptions: {
|
|
280
|
+
name: "Password"
|
|
281
|
+
},
|
|
282
|
+
locatorMethod: GetByMethod.role
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
this.locators.addSchema("main.section@login.form.button@login", {
|
|
286
|
+
role: "button",
|
|
287
|
+
roleOptions: {
|
|
288
|
+
name: "Login"
|
|
289
|
+
},
|
|
290
|
+
locatorMethod: GetByMethod.role
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.locators.addSchema("main.section@login.link@createUser", {
|
|
294
|
+
role: "link",
|
|
295
|
+
roleOptions: {
|
|
296
|
+
name: "Create an account"
|
|
297
|
+
},
|
|
298
|
+
locatorMethod: GetByMethod.role
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async fillLoginFormAndLoginAs(user: User) {
|
|
303
|
+
await test.step(`${this.pocName}: Fill login form and login as '${user.firstName}'`, async () => {
|
|
304
|
+
const email = await this.getNestedLocator("main.section@login.form.input@email");
|
|
305
|
+
await email.fill(user.email);
|
|
306
|
+
|
|
307
|
+
const password = await this.getNestedLocator("main.section@login.form.input@password");
|
|
308
|
+
await password.fill(user.password);
|
|
309
|
+
|
|
310
|
+
const loginBtn = await this.getNestedLocator("main.section@login.form.button@login");
|
|
311
|
+
await loginBtn.click();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Improved readability, maintainability and re-usability of LocatorSchema
|
|
318
|
+
|
|
319
|
+
This is all well and good, but the Page Object Class quickly becomes large and daunting when we keep our LocatorSchema's inside it. To resolve this lets move our LocatorSchemaPath's and LocatorSchema's into a seperate file.
|
|
320
|
+
|
|
321
|
+
To do this we need to import `GetLocatorBase` from POMWright and implement an initLocatorSchemas function.
|
|
322
|
+
|
|
323
|
+
```TS
|
|
324
|
+
// login.locatorSchema.ts
|
|
325
|
+
import { GetByMethod, type GetLocatorBase } from "pomwright";
|
|
326
|
+
|
|
327
|
+
export type LocatorSchemaPath =
|
|
328
|
+
| "main"
|
|
329
|
+
| "main.section@login"
|
|
330
|
+
| "main.section@login.heading"
|
|
331
|
+
| "main.section@login.form"
|
|
332
|
+
| "main.section@login.form.input@email"
|
|
333
|
+
| "main.section@login.form.input@password"
|
|
334
|
+
| "main.section@login.form.button@login"
|
|
335
|
+
| "main.section@login.link@createUser";
|
|
336
|
+
|
|
337
|
+
export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
|
|
338
|
+
locators.addSchema("main", {
|
|
339
|
+
locator: "main",
|
|
340
|
+
locatorMethod: GetByMethod.locator
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
locators.addSchema("main.section@login", {
|
|
344
|
+
role: "region",
|
|
345
|
+
roleOptions: {
|
|
346
|
+
name: "Login"
|
|
347
|
+
},
|
|
348
|
+
locatorMethod: GetByMethod.role
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
locators.addSchema("main.section@login.heading", {
|
|
352
|
+
role: "heading",
|
|
353
|
+
roleOptions: {
|
|
354
|
+
name: "Welcome",
|
|
355
|
+
level: 1
|
|
356
|
+
},
|
|
357
|
+
locatorMethod: GetByMethod.role
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
locators.addSchema("main.section@login.form", {
|
|
361
|
+
role: "form",
|
|
362
|
+
locatorMethod: GetByMethod.role
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
locators.addSchema("main.section@login.form.input@email", {
|
|
366
|
+
role: "textbox",
|
|
367
|
+
roleOptions: {
|
|
368
|
+
name: "E-mail"
|
|
369
|
+
},
|
|
370
|
+
locatorMethod: GetByMethod.role
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
locators.addSchema("main.section@login.form.input@password", {
|
|
374
|
+
role: "textbox",
|
|
375
|
+
roleOptions: {
|
|
376
|
+
name: "Password"
|
|
377
|
+
},
|
|
378
|
+
locatorMethod: GetByMethod.role
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
locators.addSchema("main.section@login.form.button@login", {
|
|
382
|
+
role: "button",
|
|
383
|
+
roleOptions: {
|
|
384
|
+
name: "Login"
|
|
385
|
+
},
|
|
386
|
+
locatorMethod: GetByMethod.role
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
locators.addSchema("main.section@login.link@createUser", {
|
|
390
|
+
role: "link",
|
|
391
|
+
roleOptions: {
|
|
392
|
+
name: "Create an account"
|
|
393
|
+
},
|
|
394
|
+
locatorMethod: GetByMethod.role
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
We can then update our login POC as follows:
|
|
400
|
+
|
|
401
|
+
```TS
|
|
402
|
+
// login.page.ts
|
|
403
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
404
|
+
import { BasePage, type PlaywrightReportLogger, test } from "pomwright";
|
|
405
|
+
import { type LocatorSchemaPath, initLocatorSchemas } from "./login.locatorSchema";
|
|
406
|
+
import User from "@test-data/user.type";
|
|
407
|
+
|
|
408
|
+
export default class Login extends BasePage<LocatorSchemaPath> {
|
|
409
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
410
|
+
super(page, testInfo, "https://someDomain.com", "/login", Login.name, pwrl);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
protected initLocatorSchemas() {
|
|
414
|
+
initLocatorSchemas(this.locators);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async fillLoginFormAndLoginAs(user: User) {
|
|
418
|
+
await test.step(`${this.pocName}: Fill login form and login as '${user.firstName}'`, async () => {
|
|
419
|
+
const email = await this.getNestedLocator("main.section@login.form.input@email");
|
|
420
|
+
await email.fill(user.email);
|
|
421
|
+
|
|
422
|
+
const password = await this.getNestedLocator("main.section@login.form.input@password");
|
|
423
|
+
await password.fill(user.password);
|
|
424
|
+
|
|
425
|
+
const loginBtn = await this.getNestedLocator("main.section@login.form.button@login");
|
|
426
|
+
await loginBtn.click();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
By moving the POC's LocatorSchemaPath's and LocatorSchema's into a seperate file, our Page Object Class will only contain its helper methods, making both easier to read and maintain. Especially considering that a POC can have well over a hundred LocatorSchema's/LocatorSchemaPath's.
|
|
433
|
+
|
|
434
|
+
#### Now lets use our POC to create a custom Playwright fixture so we can use it in our tests
|
|
435
|
+
|
|
436
|
+
You can read further about fixtures in [Playwrights documentation](https://playwright.dev/docs/test-fixtures).
|
|
437
|
+
|
|
438
|
+
```TS
|
|
439
|
+
// myApp.fixtures.ts
|
|
440
|
+
import { test as base } from "pomwright";
|
|
441
|
+
import Login from "../pom/myApp/pages/login/login.page";
|
|
442
|
+
|
|
443
|
+
type myApp = {
|
|
444
|
+
login: Login;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export const test = base.extend<myApp>({
|
|
448
|
+
login: async ({ page, log }, use, testInfo) => {
|
|
449
|
+
const login = new Login(page, testInfo, log);
|
|
450
|
+
await use(login);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
For us to access and use POMWright's `log` fixture (PlaywrightReportLogger) which our POC needs, we'll need to import test from POMWright. POMWright itself extends and exports playwright test the same way we've done above.
|
|
456
|
+
|
|
457
|
+
We'll then import our POCs and extend test (base) from POMWright/Playwright with our new custom fixtures and export it as test. We're basically just adding additional fixtures to Playwright/test.
|
|
458
|
+
|
|
459
|
+
Say we've created additional POCs for myApp's home page and profile page we'll just add them to the same fixture file like so:
|
|
460
|
+
|
|
461
|
+
```TS
|
|
462
|
+
// myApp.fixtures.ts
|
|
463
|
+
import { test as base } from "pomwright";
|
|
464
|
+
import Home from "../pom/myApp/pages/home.page";
|
|
465
|
+
import Login from "../pom/myApp/pages/login/login.page";
|
|
466
|
+
import Profile from "../pom/myApp/pages/profile/profile.page";
|
|
467
|
+
|
|
468
|
+
type myApp = {
|
|
469
|
+
home: Home;
|
|
470
|
+
login: Login;
|
|
471
|
+
profile: Profile;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export const test = base.extend<myApp>({
|
|
475
|
+
home: async ({ page, log }, use, testInfo) => {
|
|
476
|
+
const home = new Home(page, testInfo, log);
|
|
477
|
+
await use(home);
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
login: async ({ page, log }, use, testInfo) => {
|
|
481
|
+
const login = new Login(page, testInfo, log);
|
|
482
|
+
await use(login);
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
profile: async ({ page, log }, use, testInfo) => {
|
|
486
|
+
const profile = new Profile(page, testInfo, log);
|
|
487
|
+
await use(profile);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Likely we'll want multiple types of fixtures, seperate fixtures for different apps/domains, fixtures for third-party solutions we might need to interact with, automatic fixtures etc. Thus it makes sense to seperate different fixtures into seperate files giving us a more maintainable structure. As all our fixture files extends test with additional fixtures and exports test, we'll want a main fixture file to merge them so we only need one import of test to access all our fixtures when writing our tests.
|
|
493
|
+
|
|
494
|
+
```TS
|
|
495
|
+
// all.fixtures.ts
|
|
496
|
+
import { mergeTests } from "@playwright/test";
|
|
497
|
+
import { test as myAppFixtures } from "./myApp/myApp.fixtures";
|
|
498
|
+
import { test as testUser } from "./testData/testUser.fixtures";
|
|
499
|
+
|
|
500
|
+
export const test = mergeTests(myAppFixtures, testUser);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### Using our POC fixture in a test
|
|
504
|
+
|
|
505
|
+
```TS
|
|
506
|
+
import { test } from "@fixtures/all.fixtures";
|
|
507
|
+
|
|
508
|
+
test("Login to myApp as Bob", async ({ home, login, profile, testUser }) => {
|
|
509
|
+
await test.step(`${home.pocName}: Navigate to '${home.fullUrl}' and initiate login`, async () => {
|
|
510
|
+
await home.page.goto(home.fullUrl);
|
|
511
|
+
|
|
512
|
+
const loginBtn = await home.getNestedLocator("common.navMenu.link@login");
|
|
513
|
+
await loginBtn.click();
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
await login.page.waitForURL(login.fullUrl);
|
|
517
|
+
|
|
518
|
+
await login.fillLoginFormAndLoginAs(testUser.bob);
|
|
519
|
+
|
|
520
|
+
await profile.page.waitForURL(profile.fullUrl);
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
You can extend playwright/test with as many fixtures as you want, Playwright will only load the fixtures we specify and whichever fixtures are needed to build them for a given test.
|
|
525
|
+
|
|
526
|
+
### Creating an abstract BasePage as the foundation of all POC's for a given domain/app
|
|
527
|
+
|
|
528
|
+
So far, we’ve extended POCs directly from POMWright’s BasePage. However, most pages in a domain share common components and functionality. To avoid duplication in every POC, we define an abstract BasePage for the domain and let all POCs extend it.
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// myApp.basePage.ts
|
|
532
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
533
|
+
import { BasePage, type PlaywrightReportLogger } from "pomwright";
|
|
534
|
+
// import common helper methods / classes etc, here...
|
|
535
|
+
|
|
536
|
+
export default abstract class MyAppBase<LocatorSchemaPathType extends string> extends BasePage<LocatorSchemaPathType> {
|
|
537
|
+
// add common properties here
|
|
538
|
+
|
|
539
|
+
constructor(page: Page, testInfo: TestInfo, urlPath: string, pocName: string, pwrl: PlaywrightReportLogger) {
|
|
540
|
+
super(page, testInfo, "https://someDomain.com", urlPath, pocName, pwrl);
|
|
541
|
+
|
|
542
|
+
// initialize common properties here
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// add/implement common helper methods/classes here
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
And we'll update our login.page.ts to extend it instead of BasePage from POMWright.
|
|
550
|
+
|
|
551
|
+
```TS
|
|
552
|
+
// login.page.ts
|
|
553
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
554
|
+
import { type PlaywrightReportLogger, test } from "pomwright";
|
|
555
|
+
import MyAppBase from "../base/myApp.basePage";
|
|
556
|
+
import { type LocatorSchemaPath, initLocatorSchemas } from "./login.locatorSchema";
|
|
557
|
+
import User from "@test-data/user.type";
|
|
558
|
+
|
|
559
|
+
export default class Login extends MyAppBase<LocatorSchemaPath> {
|
|
560
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
561
|
+
super(page, testInfo,"/login", Login.name, pwrl);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
protected initLocatorSchemas() {
|
|
565
|
+
initLocatorSchemas(this.locators);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async fillLoginFormAndLoginAs(user: User) {
|
|
569
|
+
await test.step(`${this.pocName}: Fill login form and login as '${user.firstName}'`, async () => {
|
|
570
|
+
const email = await this.getNestedLocator("main.section@login.form.input@email");
|
|
571
|
+
await email.fill(user.email);
|
|
572
|
+
|
|
573
|
+
const password = await this.getNestedLocator("main.section@login.form.input@password");
|
|
574
|
+
await password.fill(user.password);
|
|
575
|
+
|
|
576
|
+
const loginBtn = await this.getNestedLocator("main.section@login.form.button@login");
|
|
577
|
+
await loginBtn.click();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
We can now add functionality all POC's for the given domain have in common to our abstract BasePage. We might have a collection of utility functions and other helper methods etc. Something along the line of:
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
// myApp.basePage.ts
|
|
587
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
588
|
+
import { BasePage, type PlaywrightReportLogger } from "pomwright";
|
|
589
|
+
import { utils } from "../utils/utils";
|
|
590
|
+
import { Axe } from "./helpers/axe.accessibility";
|
|
591
|
+
import { CookieConsent } from "./helpers/cookieConsent.actions";
|
|
592
|
+
import { Navigation } from "./helpers/navigation.actions";
|
|
593
|
+
import { env } from "@env";
|
|
594
|
+
|
|
595
|
+
export default abstract class MyAppBase<LocatorSchemaPathType extends string> extends BasePage<LocatorSchemaPathType> {
|
|
596
|
+
/** Accessibility testing methods using @axe-core/playwright */
|
|
597
|
+
axe: Axe;
|
|
598
|
+
|
|
599
|
+
/** Common navigation operations, context is dictated by the Fixture it's invoked on */
|
|
600
|
+
navigation: Navigation;
|
|
601
|
+
|
|
602
|
+
/** Common methods for cookie consent dialog and cookie injection/mock and extraction */
|
|
603
|
+
cookieConsent: CookieConsent;
|
|
604
|
+
|
|
605
|
+
/** Collection of different utility functions */
|
|
606
|
+
utils = utils;
|
|
607
|
+
|
|
608
|
+
constructor(page: Page, testInfo: TestInfo, urlPath: string, pocName: string, pwrl: PlaywrightReportLogger) {
|
|
609
|
+
super(page, testInfo, env.BASEURL_MYAPP, urlPath, pocName, pwrl);
|
|
610
|
+
|
|
611
|
+
this.axe = new Axe(this.page, this.pocName, this.log);
|
|
612
|
+
|
|
613
|
+
this.navigation = new Navigation(
|
|
614
|
+
this.page,
|
|
615
|
+
this.baseUrl,
|
|
616
|
+
this.urlPath,
|
|
617
|
+
this.fullUrl,
|
|
618
|
+
this.pocName,
|
|
619
|
+
this.pageActionsToPerformAfterNavigation()
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
this.cookieConsent = new CookieConsent(
|
|
623
|
+
this.page,
|
|
624
|
+
this.baseUrl,
|
|
625
|
+
this.fullUrl
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* A class extending Base must implement pageActionsToPerformAfterNavigation(),
|
|
631
|
+
* if no actions are needed have it return an empty array or null.
|
|
632
|
+
*
|
|
633
|
+
* Intended to be used for actions that should be performed after the page has been navigated to,
|
|
634
|
+
* such as waiting for elements to be visible or not etc. E.g. waiting for a spinner to disappear.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* protected pageActionsToPerformAfterNavigation(): (() => Promise<void>)[] {
|
|
638
|
+
* return [
|
|
639
|
+
* async () => {
|
|
640
|
+
* await expect.soft(await this.getNestedLocator("common.spinner")).toHaveCount(0);
|
|
641
|
+
* }
|
|
642
|
+
* ];
|
|
643
|
+
* }
|
|
644
|
+
*/
|
|
645
|
+
protected abstract pageActionsToPerformAfterNavigation(): (() => Promise<void>)[];
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Allowing all POC's extending MyAppBase to use these these methods:
|
|
650
|
+
|
|
651
|
+
```TS
|
|
652
|
+
import { test } from "@fixtures/all.fixtures";
|
|
653
|
+
|
|
654
|
+
test("Login to myApp as Bob", { tag: ["@login", "@a11y"] }, async ({ home, login, profile, testUser }) => {
|
|
655
|
+
await test.step(`${home.pocName}: Navigate to '${home.fullUrl}' and initiate login`, async () => {
|
|
656
|
+
await home.cookieConsent.set("necessary");
|
|
657
|
+
await home.navigation.gotoThisPage();
|
|
658
|
+
|
|
659
|
+
await home.axe.a11y();
|
|
660
|
+
|
|
661
|
+
const loginBtn = await home.getNestedLocator("common.navMenu.link@login");
|
|
662
|
+
await loginBtn.click();
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
await login.navigation.expectThisPage();
|
|
666
|
+
|
|
667
|
+
await login.axe.a11y();
|
|
668
|
+
|
|
669
|
+
await login.fillLoginFormAndLoginAs(testUser.bob);
|
|
670
|
+
|
|
671
|
+
await profile.navigation.expectThisPage();
|
|
672
|
+
|
|
673
|
+
await profile.axe.a11y();
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Sharing common LocatorSchema's across POC's for a given domain or domains
|
|
678
|
+
|
|
679
|
+
In the example test above we've called:
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
const loginBtn = await home.getNestedLocator("common.navMenu.link@login");
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
As you might have notices, we have no such LocatorSchemaPath or LocatorSchema in the initial login.locatorSchema.ts file. This is because we've defined it in a seperate file "navMenu.locatorSchema.ts" which do not belong to any specific POC as the navMenu is the same component present for all the pages on our imagined app/domain. The same goes for all other components and locators that are shared among all pages.
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
import { GetByMethod, type GetLocatorBase } from "pomwright";
|
|
689
|
+
|
|
690
|
+
export type LocatorSchemaPath =
|
|
691
|
+
| "common.navMenu"
|
|
692
|
+
| "common.navMenu.link@login"
|
|
693
|
+
| "common.navMenu.section.search"
|
|
694
|
+
| "common.navMenu.section.search.input.textfield"
|
|
695
|
+
| "common.navMenu.section.search.button.search";
|
|
696
|
+
|
|
697
|
+
export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
|
|
698
|
+
locators.addSchema("common.navMenu", {
|
|
699
|
+
locator: "nav.main-nav",
|
|
700
|
+
locatorMethod: GetByMethod.locator,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
locators.addSchema("common.navMenu.link@login", {
|
|
704
|
+
role: "link",
|
|
705
|
+
roleOptions: {
|
|
706
|
+
name: "Login",
|
|
707
|
+
},
|
|
708
|
+
locatorMethod: GetByMethod.role,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
locators.addSchema("common.navMenu.section.search", {
|
|
712
|
+
locator: ".inline-search",
|
|
713
|
+
locatorMethod: GetByMethod.locator,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
locators.addSchema("common.navMenu.section.search.input.textfield", {
|
|
717
|
+
role: "combobox",
|
|
718
|
+
roleOptions: {
|
|
719
|
+
name: "Search",
|
|
720
|
+
},
|
|
721
|
+
locatorMethod: GetByMethod.role,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
locators.addSchema("common.navMenu.section.search.button.search", {
|
|
725
|
+
text: "Search",
|
|
726
|
+
textOptions: {
|
|
727
|
+
exact: true,
|
|
728
|
+
},
|
|
729
|
+
locatorMethod: GetByMethod.text,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
We do this for all common components and elements that do not exclusively belong to a given page. We then collect them in a single file e.g. "common.locatorSchema.ts" like so:
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
// common.locatorSchema.ts
|
|
738
|
+
import { GetByMethod, type GetLocatorBase } from "pomwright";
|
|
739
|
+
import { type LocatorSchemaPath as alert, initLocatorSchemas as initAlert } from "...alert.locatorSchema";
|
|
740
|
+
// ...rest of imports here
|
|
741
|
+
|
|
742
|
+
export type LocatorSchemaPath =
|
|
743
|
+
| alert
|
|
744
|
+
| mainHeader
|
|
745
|
+
| cookieConsent
|
|
746
|
+
| freshChat
|
|
747
|
+
| shoppingCart
|
|
748
|
+
| footer
|
|
749
|
+
| navMenu
|
|
750
|
+
| popupFeedback
|
|
751
|
+
| "main";
|
|
752
|
+
|
|
753
|
+
export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
|
|
754
|
+
initAlert(locators);
|
|
755
|
+
initMainHeader(locators);
|
|
756
|
+
initCookieConsent(locators);
|
|
757
|
+
initFreshChat(locators);
|
|
758
|
+
initShoppingCart(locators);
|
|
759
|
+
initFooter(locators);
|
|
760
|
+
initNavMenu(locators);
|
|
761
|
+
initPopupFeedback(locators);
|
|
762
|
+
|
|
763
|
+
locators.addSchema("main", {
|
|
764
|
+
locator: "main",
|
|
765
|
+
locatorMethod: GetByMethod.locator,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Then we can make these available to all POC's in atleast two different ways:
|
|
772
|
+
|
|
773
|
+
#### Sharing common LocatorSchema through the abstract base class
|
|
774
|
+
|
|
775
|
+
Likely the easiest and cleanest solution. Additionally it allows us to create common helper methods in our abstract base class using the shared LocatorSchema's.
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
// myApp.basePage.ts
|
|
779
|
+
import type { Page, TestInfo } from "@playwright/test";
|
|
780
|
+
import { BasePage, type PlaywrightReportLogger } from "pomwright";
|
|
781
|
+
import { utils } from "../utils/utils";
|
|
782
|
+
import { Axe } from "./helpers/axe.accessibility";
|
|
783
|
+
import { CookieConsent } from "./helpers/cookieConsent.actions";
|
|
784
|
+
import { Navigation } from "./helpers/navigation.actions";
|
|
785
|
+
import { env } from "@env";
|
|
786
|
+
// We import the common locatorSchema here:
|
|
787
|
+
import { type LocatorSchemaPath as CommonLocatorSchemaPath, initLocatorSchemas as initCommonLocatorSchemas } from "../page-components/common.locatorSchema";
|
|
788
|
+
|
|
789
|
+
// We then make a union type when extending with BasePage from POMWright
|
|
790
|
+
export default abstract class MyAppBase<LocatorSchemaPathType extends string> extends BasePage<LocatorSchemaPathType | CommonLocatorSchemaPath> {
|
|
791
|
+
axe: Axe;
|
|
792
|
+
navigation: Navigation;
|
|
793
|
+
cookieConsent: CookieConsent;
|
|
794
|
+
utils = utils;
|
|
795
|
+
|
|
796
|
+
constructor(page: Page, testInfo: TestInfo, urlPath: string, pocName: string, pwrl: PlaywrightReportLogger) {
|
|
797
|
+
super(page, testInfo, env.BASEURL_MYAPP, urlPath, pocName, pwrl);
|
|
798
|
+
|
|
799
|
+
this.axe = new Axe(this.page, this.pocName, this.log);
|
|
800
|
+
|
|
801
|
+
this.navigation = new Navigation(
|
|
802
|
+
this.page,
|
|
803
|
+
this.baseUrl,
|
|
804
|
+
this.urlPath,
|
|
805
|
+
this.fullUrl,
|
|
806
|
+
this.pocName,
|
|
807
|
+
this.pageActionsToPerformAfterNavigation()
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
this.cookieConsent = new CookieConsent(
|
|
811
|
+
this.page,
|
|
812
|
+
this.baseUrl,
|
|
813
|
+
this.fullUrl
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// And initialize the common LocatorSchema in the constructor:
|
|
817
|
+
initCommonLocatorSchemas(this.locators);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
protected abstract pageActionsToPerformAfterNavigation(): (() => Promise<void>)[];
|
|
821
|
+
|
|
822
|
+
// This allows us to create helper methods for interacting with common components and elements, usable by all POC's extending this class
|
|
823
|
+
async expectCookieConsentDialogToBeHidden() {
|
|
824
|
+
await test.step("Cookie consent dialog should be hidden", async () => {
|
|
825
|
+
const cookieDialog = await this.getNestedLocator("common.dialog.cookieConsent");
|
|
826
|
+
await expect(cookieDialog).toBeHidden();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
#### Sharing common LocatorSchema through each POC's locatorSchema.ts file
|
|
833
|
+
|
|
834
|
+
I would recommend you use the first solution mentioned, and only use this approach for when a sub-page has the same LocatorSchema as the "parent" with some additional ones.
|
|
835
|
+
|
|
836
|
+
If you only use this approach though, the drawback is that we'll not be able to use common locatorSchema's in our abstract base class for our shared helper methods, but it's easier to reuse common locatorSchema's in custom locator chains for a given page. E.g. You have a LocatorSchema for a common alert message located in the same DOM structure on every single page, except for one or two specific pages, where this alert is shown inside a different DOM structure.
|
|
837
|
+
|
|
838
|
+
Note it is still possible to reuse common locatorSchema in custom locator chains for a given page with the first solution, you'll just need to relying on exporting and importing the `LocatorSchemaWithoutPath` objects you need from one locatorSchema file to the other.
|
|
839
|
+
|
|
840
|
+
Anyway, you can also share the common LocatorSchema's by importing them the same way we did for common.locatorSchema.ts, so for login.locatorSchema.ts we would simply do:
|
|
841
|
+
|
|
842
|
+
```ts
|
|
843
|
+
import { GetByMethod, type GetLocatorBase } from "pomwright";
|
|
844
|
+
import { type LocatorSchemaPath as common, initLocatorSchemas as initCommon } from "../page-components/common.locatorSchema";
|
|
845
|
+
|
|
846
|
+
export type LocatorSchemaPath =
|
|
847
|
+
| common
|
|
848
|
+
| "common.navMenu"
|
|
849
|
+
| "common.navMenu.link@login"
|
|
850
|
+
| "common.navMenu.section.search"
|
|
851
|
+
| "common.navMenu.section.search.input.textfield"
|
|
852
|
+
| "common.navMenu.section.search.button.search";
|
|
853
|
+
|
|
854
|
+
export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
|
|
855
|
+
initCommon(locators);
|
|
856
|
+
|
|
857
|
+
locators.addSchema("common.navMenu", {
|
|
858
|
+
locator: "nav.main-nav",
|
|
859
|
+
locatorMethod: GetByMethod.locator,
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
locators.addSchema("common.navMenu.link@login", {
|
|
863
|
+
role: "link",
|
|
864
|
+
roleOptions: {
|
|
865
|
+
name: "Login",
|
|
866
|
+
},
|
|
867
|
+
locatorMethod: GetByMethod.role,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
locators.addSchema("common.navMenu.section.search", {
|
|
871
|
+
locator: ".inline-search",
|
|
872
|
+
locatorMethod: GetByMethod.locator,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
locators.addSchema("common.navMenu.section.search.input.textfield", {
|
|
876
|
+
role: "combobox",
|
|
877
|
+
roleOptions: {
|
|
878
|
+
name: "Search",
|
|
879
|
+
},
|
|
880
|
+
locatorMethod: GetByMethod.role,
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
locators.addSchema("common.navMenu.section.search.button.search", {
|
|
884
|
+
text: "Search",
|
|
885
|
+
textOptions: {
|
|
886
|
+
exact: true,
|
|
887
|
+
},
|
|
888
|
+
locatorMethod: GetByMethod.text,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Note: You can combine both these solutions as mentioned, but not for the same LocatorSchemaPath's as they must always be unique. If you initialize the common.locatorSchema through the abstract base class and on a POCs locatorSchema file POMWright won't know which one is the correct one and will throw an error stating you're trying the add the same LocatorSchemaPath twice.
|
|
894
|
+
|
|
895
|
+
## More practical examples see ./intTest
|
|
896
|
+
|
|
897
|
+
If the website you're writing tests for only have URL's with static / predefined resource path's, have a look at `intTest/page-object-models/testApp/without-options`
|
|
898
|
+
|
|
899
|
+
If the website you're writing tests for have one or more pages where the URL contains a generated value that always changes or changes depending on some context and you want to create a POC to represent and interact with it, have a look at `intTest/page-object-models/testApp/with-options`
|