codeceptjs 4.0.0-rc.17 → 4.0.0-rc.19
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/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +733 -196
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/aria.js +260 -0
- package/lib/command/dryRun.js +23 -3
- package/lib/command/init.js +247 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/config.js +77 -4
- package/lib/container.js +34 -2
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +5 -6
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/locator.js +2 -2
- package/lib/mocha/factory.js +5 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +72 -84
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +51 -48
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -170
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +25 -0
- package/lib/workers.js +1 -15
- package/package.json +12 -10
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/plugin/pauseOn.js +0 -167
- package/lib/plugin/stepByStepReport.js +0 -432
- package/lib/plugin/subtitles.js +0 -89
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /pageobjects
|
|
3
|
+
title: Page Objects
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Page Objects
|
|
7
|
+
|
|
8
|
+
The UI of your web application has interaction areas which can be shared across different tests.
|
|
9
|
+
To avoid code duplication you can put common locators and methods in one place.
|
|
10
|
+
|
|
11
|
+
## Dependency Injection
|
|
12
|
+
|
|
13
|
+
All objects described here are injected via Dependency Injection, in a similar way AngularJS does. If you want an object to be injected in a scenario by its name, you can add it to the configuration:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
include: {
|
|
17
|
+
I: "./custom_steps.js",
|
|
18
|
+
Smth: "./pages/Smth.js",
|
|
19
|
+
loginPage: "./pages/Login.js",
|
|
20
|
+
signinFragment: "./fragments/Signin.js"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
These objects can now be retrieved by the name specified in the configuration.
|
|
25
|
+
|
|
26
|
+
Required objects can be obtained via parameters in tests or via a global `inject()` call.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
// globally inject objects by name
|
|
30
|
+
const { I, myPage, mySteps } = inject();
|
|
31
|
+
|
|
32
|
+
// inject objects for a test by name
|
|
33
|
+
Scenario('sample test', ({ I, myPage, mySteps }) => {
|
|
34
|
+
// ...
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Actor
|
|
39
|
+
|
|
40
|
+
During initialization, you were asked to create a custom steps file. If you accepted this option, you are now able to use the `custom_steps.js` file to extend `I`. See how the `login` method can be added to `I`:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
export default function() {
|
|
44
|
+
return actor({
|
|
45
|
+
login: function(email, password) {
|
|
46
|
+
this.fillField('Email', email);
|
|
47
|
+
this.fillField('Password', password);
|
|
48
|
+
this.click('Submit');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Instead of `I` you should use `this` in the current context.
|
|
55
|
+
|
|
56
|
+
## PageObject
|
|
57
|
+
|
|
58
|
+
If an application has different pages (login, admin, etc) you should use a page object.
|
|
59
|
+
CodeceptJS can generate a template for it with the following command:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx codeceptjs gpo
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This will create a sample template for a page object and include it in the `codecept.json` config file.
|
|
66
|
+
|
|
67
|
+
**Page objects should be classes.** Use `const { I } = inject()` at the top of the file to access `I` and other page objects. Export the class itself — the DI container will auto-instantiate it.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const { I, registerPage } = inject();
|
|
71
|
+
|
|
72
|
+
class LoginPage {
|
|
73
|
+
// setting locators
|
|
74
|
+
fields = {
|
|
75
|
+
email: '#user_basic_email',
|
|
76
|
+
password: '#user_basic_password'
|
|
77
|
+
}
|
|
78
|
+
submitButton = { css: '#new_user_basic input[type=submit]' }
|
|
79
|
+
|
|
80
|
+
// introducing methods
|
|
81
|
+
sendForm(email, password) {
|
|
82
|
+
I.fillField(this.fields.email, email);
|
|
83
|
+
I.fillField(this.fields.password, password);
|
|
84
|
+
I.click(this.submitButton);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
register(email, password) {
|
|
88
|
+
// use another page object inside current one
|
|
89
|
+
registerPage.registerUser({ email, password });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default LoginPage
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> The `inject()` call at the top returns a lazy proxy. `I` and other page objects resolve at call time, so it's safe to destructure before class definition.
|
|
97
|
+
|
|
98
|
+
You can include this pageobject in a test by its name (defined in `codecept.conf.js`). If you created a `loginPage` object,
|
|
99
|
+
it should be added to the list of arguments to be included in the test:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
Scenario('login', ({ I, loginPage }) => {
|
|
103
|
+
loginPage.sendForm('john@doe.com','123456');
|
|
104
|
+
I.see('Hello, John');
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
You can use `async/await` inside a Page Object:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const { I } = inject();
|
|
112
|
+
|
|
113
|
+
class MainPage {
|
|
114
|
+
// setting locators
|
|
115
|
+
container = "//div[@class = 'numbers']"
|
|
116
|
+
mainItem = {
|
|
117
|
+
number: ".//div[contains(@class, 'numbers__main-number')]",
|
|
118
|
+
title: ".//div[contains(@class, 'numbers__main-title-block')]"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// introducing methods
|
|
122
|
+
async openMainArticle() {
|
|
123
|
+
I.waitForVisible(this.container)
|
|
124
|
+
let _this = this
|
|
125
|
+
let title;
|
|
126
|
+
await within(this.container, async () => {
|
|
127
|
+
title = await I.grabTextFrom(_this.mainItem.number);
|
|
128
|
+
let subtitle = await I.grabTextFrom(_this.mainItem.title);
|
|
129
|
+
title = title + " " + subtitle.charAt(0).toLowerCase() + subtitle.slice(1);
|
|
130
|
+
await I.click(_this.mainItem.title)
|
|
131
|
+
})
|
|
132
|
+
return title;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default MainPage
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
and use them in your tests:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
Scenario('open article', async ({ I, mainPage, basePage }) => {
|
|
143
|
+
let title = await mainPage.openMainArticle()
|
|
144
|
+
basePage.pageShouldBeOpened(title)
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Page objects can also be extended via class inheritance:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
const { I } = inject();
|
|
152
|
+
|
|
153
|
+
class AttachFile {
|
|
154
|
+
inputFileField = 'input[name=fileUpload]'
|
|
155
|
+
fileSize = '.file-size'
|
|
156
|
+
fileName = '.file-name'
|
|
157
|
+
|
|
158
|
+
async attachFileFrom(path) {
|
|
159
|
+
await I.waitForVisible(this.inputFileField)
|
|
160
|
+
await I.attachFile(this.inputFileField, path)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async hasFileSize(fileSizeText) {
|
|
164
|
+
await I.waitForElement(this.fileSize)
|
|
165
|
+
const size = await I.grabTextFrom(this.fileSize)
|
|
166
|
+
expect(size).toEqual(fileSizeText)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Export class for auto-instantiation
|
|
171
|
+
export default AttachFile
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> While building complex page objects it is important to keep all `async` functions to be called with `await`. While CodeceptJS allows to run commands synchronously if async function has `I.grab*` or any custom function that returns a promise it must be called with `await`. If you see `UnhandledPromiseRejectionWarning` it might be caused by async page object function that was called without `await`.
|
|
175
|
+
|
|
176
|
+
## Page Object Lifecycle Hooks
|
|
177
|
+
|
|
178
|
+
Page objects support lifecycle hooks that mirror the helper hook system. These methods are called automatically by the framework:
|
|
179
|
+
|
|
180
|
+
| Hook | When it runs |
|
|
181
|
+
|------|-------------|
|
|
182
|
+
| `_before()` | Before the first method call on this page object in a test (lazy, per-test) |
|
|
183
|
+
| `_after()` | After each test, but only if the page object was used in that test |
|
|
184
|
+
| `_beforeSuite()` | Before each Feature/suite (for all page objects that define it) |
|
|
185
|
+
| `_afterSuite()` | After each Feature/suite (for all page objects that define it) |
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const { I } = inject();
|
|
189
|
+
|
|
190
|
+
class DashboardPage {
|
|
191
|
+
_before() {
|
|
192
|
+
I.amOnPage('/dashboard');
|
|
193
|
+
I.waitForElement('.dashboard-loaded');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_after() {
|
|
197
|
+
I.clearCookie();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_afterSuite() {
|
|
201
|
+
I.sendDeleteRequest('/api/test-data/cleanup');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
grabStats() {
|
|
205
|
+
return I.grabTextFrom('.stats');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
seeWelcomeMessage(name) {
|
|
209
|
+
I.see(`Welcome, ${name}`, '.header');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default DashboardPage
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
Scenario('see dashboard stats', async ({ I, dashboardPage }) => {
|
|
218
|
+
// dashboardPage._before() runs automatically before this line
|
|
219
|
+
dashboardPage.seeWelcomeMessage('John');
|
|
220
|
+
const stats = await dashboardPage.grabStats();
|
|
221
|
+
I.say(`Stats: ${stats}`);
|
|
222
|
+
// dashboardPage._after() runs automatically after test ends
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Key behaviors:
|
|
227
|
+
- `_before()` runs **lazily** — only when the page object is first used in a test, not when it's injected
|
|
228
|
+
- `_before()` runs **once per test** — calling multiple methods does not re-trigger it
|
|
229
|
+
- `_after()` is **skipped** for page objects that were never used in the test
|
|
230
|
+
- `_beforeSuite()` and `_afterSuite()` run for **all** page objects that define them, regardless of usage
|
|
231
|
+
- Hook methods are **not** shown as test steps in the output
|
|
232
|
+
|
|
233
|
+
## Page Fragments
|
|
234
|
+
|
|
235
|
+
Similarly, CodeceptJS allows you to generate **PageFragments** and any other abstractions
|
|
236
|
+
by running the `go` command with `--type` (or `-t`) option:
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
npx codeceptjs go --type fragment
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Page Fragments represent autonomous parts of a page, like modal boxes, components, widgets.
|
|
243
|
+
Technically, they are the same as PageObject but conceptually they are a bit different.
|
|
244
|
+
For instance, it is recommended that Page Fragment includes a root locator of a component.
|
|
245
|
+
Methods of page fragments can use `within` block to narrow scope to a root locator:
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
const { I } = inject();
|
|
249
|
+
|
|
250
|
+
class Modal {
|
|
251
|
+
root = '#modal'
|
|
252
|
+
|
|
253
|
+
accept() {
|
|
254
|
+
within(this.root, function() {
|
|
255
|
+
I.click('Accept');
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default Modal
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
To use a Page Fragment within a Test Scenario, just inject it into your Scenario:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
Scenario('failed_login', async ({ I, loginPage, modal }) => {
|
|
267
|
+
loginPage.sendForm('john@doe.com','wrong password');
|
|
268
|
+
I.waitForVisible(modal.root);
|
|
269
|
+
within(modal.root, function () {
|
|
270
|
+
I.see('Login failed');
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
To use a Page Fragment within a Page Object, you can use `inject` method to get it by its name.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
const { I, modal } = inject();
|
|
279
|
+
|
|
280
|
+
class CheckoutPage {
|
|
281
|
+
confirmOrder() {
|
|
282
|
+
I.click('Place Order');
|
|
283
|
+
modal.accept();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default CheckoutPage
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> PageObject and PageFragment names are declared inside `include` section of `codecept.conf.js`. See [Dependency Injection](#dependency-injection)
|
|
291
|
+
|
|
292
|
+
## StepObjects
|
|
293
|
+
|
|
294
|
+
StepObjects represent complex actions which involve the usage of multiple web pages. For instance, creating users in the backend, changing permissions, etc.
|
|
295
|
+
StepObject can be created similarly to PageObjects or PageFragments:
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
npx codeceptjs go --type step
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Technically, they are the same as PageObjects. StepObjects can inject PageObjects and use multiple POs to make a complex scenarios:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
const { I, userPage, permissionPage } = inject();
|
|
305
|
+
|
|
306
|
+
class AdminSteps {
|
|
307
|
+
createUser(name) {
|
|
308
|
+
// action composed from actions of page objects
|
|
309
|
+
userPage.open();
|
|
310
|
+
userPage.create(name);
|
|
311
|
+
permissionPage.activate(name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export default AdminSteps
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Data Objects
|
|
319
|
+
|
|
320
|
+
Page objects can also be used to manage test data via API. **Data Objects** are page object classes that create data using the REST helper and automatically clean it up via the `_after()` hook.
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
Scenario('user sees their profile', async ({ I, userData }) => {
|
|
324
|
+
const user = await userData.createUser({ name: 'John Doe' });
|
|
325
|
+
I.amOnPage(`/users/${user.id}`);
|
|
326
|
+
I.see('John Doe');
|
|
327
|
+
// userData._after() runs automatically — deletes the created user
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This is useful for tests that need API-created data with automatic cleanup, without the overhead of factory configuration.
|
|
332
|
+
|
|
333
|
+
**Learn more:** See [Data Objects](/data#data-objects) for complete documentation, examples, and configuration.
|
|
334
|
+
|
|
335
|
+
## Dynamic Injection
|
|
336
|
+
|
|
337
|
+
Sometimes you need to use a page object in only one or a few tests without adding it to global configuration. Use `injectDependencies()` to inject page objects dynamically per test:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
// pages/searchPage.js
|
|
341
|
+
const { I } = inject();
|
|
342
|
+
|
|
343
|
+
class SearchPage {
|
|
344
|
+
constructor() {
|
|
345
|
+
this.searchField = '#search-input';
|
|
346
|
+
this.searchButton = 'button[type=submit]';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
search(query) {
|
|
350
|
+
I.fillField(this.searchField, query);
|
|
351
|
+
I.click(this.searchButton);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export default new SearchPage();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Inject the page object into a specific scenario:
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
import searchPage from './pages/searchPage.js'
|
|
362
|
+
|
|
363
|
+
Scenario('user searches for products', ({ I, searchPage }) => {
|
|
364
|
+
I.amOnPage('/');
|
|
365
|
+
searchPage.search('laptop');
|
|
366
|
+
I.see('Search Results');
|
|
367
|
+
}).injectDependencies({ searchPage });
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Use cases:**
|
|
371
|
+
- Page objects needed only in a few tests
|
|
372
|
+
- Test-specific configurations or data objects
|
|
373
|
+
- Experimenting with new page objects before adding them globally
|
|
374
|
+
|
|
375
|
+
**Note:** For page objects used across multiple tests, add them to the `include` section in `codecept.conf.js` instead.
|
|
376
|
+
|
|
377
|
+
## Plain Object Page Objects (Legacy)
|
|
378
|
+
|
|
379
|
+
Plain object page objects are still supported for backward compatibility:
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
const { I } = inject();
|
|
383
|
+
|
|
384
|
+
export default {
|
|
385
|
+
fields: {
|
|
386
|
+
email: '#user_basic_email',
|
|
387
|
+
password: '#user_basic_password'
|
|
388
|
+
},
|
|
389
|
+
submitButton: { css: '#new_user_basic input[type=submit]' },
|
|
390
|
+
|
|
391
|
+
sendForm(email, password) {
|
|
392
|
+
I.fillField(this.fields.email, email);
|
|
393
|
+
I.fillField(this.fields.password, password);
|
|
394
|
+
I.click(this.submitButton);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
> Class-based page objects are recommended for new code as they support lifecycle hooks and inheritance.
|