Skip to content

Commit 7cb9681

Browse files
DavertMikclaude
andcommitted
feat: refactor within to Within with begin/end pattern
Add Within() function with three signatures: - Within(locator) to begin scoped context - Within() to end current context - Within(locator, fn) callback pattern (existing behavior) Lowercase within() kept as deprecated alias with one-time warning. switchTo() in helpers auto-ends any active Within context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 551ab94 commit 7cb9681

File tree

9 files changed

+398
-28
lines changed

9 files changed

+398
-28
lines changed

docs/effects.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Effects are functions that can modify scenario flow. They provide ways to handle
77
Effects can be imported directly from CodeceptJS:
88

99
```js
10-
const { tryTo, retryTo, within } = require('codeceptjs/effects')
10+
import { tryTo, retryTo, Within } from 'codeceptjs/effects'
1111
```
1212

1313
> 📝 Note: Prior to v3.7, `tryTo` and `retryTo` were available globally via plugins. This behavior is deprecated and will be removed in v4.0.
@@ -71,31 +71,38 @@ await retryTo(tries => {
7171
}, 3)
7272
```
7373

74-
## within
74+
## Within
7575

76-
The `within` effect allows you to perform multiple steps within a specific context (like an iframe or modal):
76+
The `Within` effect scopes actions to a specific element or iframe. It supports both a begin/end pattern and a callback pattern:
7777

7878
```js
79-
const { within } = require('codeceptjs/effects')
79+
import { Within } from 'codeceptjs/effects'
8080

81-
// inside a test...
81+
// Begin/end pattern
82+
Within('.modal')
83+
I.see('Modal title')
84+
I.click('Close')
85+
Within()
8286

83-
within('.modal', () => {
87+
// Callback pattern
88+
Within('.modal', () => {
8489
I.see('Modal title')
8590
I.click('Close')
8691
})
8792
```
8893

94+
See the full [Within documentation](/within) for details on iframes, page objects, and `await` usage.
95+
96+
> The lowercase `within()` is deprecated. Use `Within` instead.
97+
8998
## Usage with TypeScript
9099

91100
Effects are fully typed and work well with TypeScript:
92101

93102
```ts
94-
import { tryTo, retryTo, within } from 'codeceptjs/effects'
103+
import { tryTo, retryTo, Within } from 'codeceptjs/effects'
95104

96105
const success = await tryTo(async () => {
97106
await I.see('Element')
98107
})
99108
```
100-
101-
This documentation covers the main effects functionality while providing practical examples and important notes about deprecation and future changes. Let me know if you'd like me to expand any section or add more examples!

docs/within.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Within
2+
3+
`Within` narrows the execution context to a specific element or iframe on the page. All actions called inside a `Within` block are scoped to the matched element.
4+
5+
```js
6+
import { Within } from 'codeceptjs/effects'
7+
```
8+
9+
## Begin / End Pattern
10+
11+
The simplest way to use `Within` is the begin/end pattern. Call `Within` with a locator to start, perform actions, then call `Within()` with no arguments to end:
12+
13+
```js
14+
Within('.signup-form')
15+
I.fillField('Email', 'user@example.com')
16+
I.fillField('Password', 'secret')
17+
I.click('Sign Up')
18+
Within()
19+
```
20+
21+
Steps between `Within('.signup-form')` and `Within()` are scoped to `.signup-form`. After `Within()`, the context resets to the full page.
22+
23+
### Auto-end previous context
24+
25+
Starting a new `Within` automatically ends the previous one:
26+
27+
```js
28+
Within('.sidebar')
29+
I.click('Dashboard')
30+
31+
Within('.main-content') // ends .sidebar, begins .main-content
32+
I.see('Welcome')
33+
Within()
34+
```
35+
36+
### Forgetting to close
37+
38+
If you forget to call `Within()` at the end, the context is automatically cleaned up when the test finishes. However, it is good practice to always close it explicitly.
39+
40+
## Callback Pattern
41+
42+
The callback pattern wraps actions in a function. The context is automatically closed when the function returns:
43+
44+
```js
45+
Within('.signup-form', () => {
46+
I.fillField('Email', 'user@example.com')
47+
I.fillField('Password', 'secret')
48+
I.click('Sign Up')
49+
})
50+
I.see('Account created')
51+
```
52+
53+
### Returning values
54+
55+
The callback pattern supports returning values. Use `await` on both the `Within` call and the inner action:
56+
57+
```js
58+
const text = await Within('#sidebar', async () => {
59+
return await I.grabTextFrom('h1')
60+
})
61+
I.fillField('Search', text)
62+
```
63+
64+
## When to use `await`
65+
66+
**Begin/end pattern** does not need `await`:
67+
68+
```js
69+
Within('.form')
70+
I.fillField('Name', 'John')
71+
Within()
72+
```
73+
74+
**Callback pattern** needs `await` when:
75+
76+
- The callback is `async`
77+
- You need a return value from `Within`
78+
79+
```js
80+
// async callback — await required
81+
await Within('.form', async () => {
82+
await I.click('Submit')
83+
await I.waitForText('Done')
84+
})
85+
```
86+
87+
```js
88+
// sync callback — no await needed
89+
Within('.form', () => {
90+
I.fillField('Name', 'John')
91+
I.click('Submit')
92+
})
93+
```
94+
95+
## Working with IFrames
96+
97+
Use the `frame` locator to scope actions inside an iframe:
98+
99+
```js
100+
// Begin/end
101+
Within({ frame: 'iframe' })
102+
I.fillField('Email', 'user@example.com')
103+
I.click('Submit')
104+
Within()
105+
106+
// Callback
107+
Within({ frame: '#editor-frame' }, () => {
108+
I.see('Page content')
109+
})
110+
```
111+
112+
### Nested IFrames
113+
114+
Pass an array of selectors to reach nested iframes:
115+
116+
```js
117+
Within({ frame: ['.wrapper', '#content-frame'] }, () => {
118+
I.fillField('Name', 'John')
119+
I.see('Sign in!')
120+
})
121+
```
122+
123+
Each selector in the array navigates one level deeper into the iframe hierarchy.
124+
125+
### switchTo auto-disables Within
126+
127+
If you call `I.switchTo()` while inside a `Within` context, the within context is automatically ended. This prevents conflicts between the two scoping mechanisms:
128+
129+
```js
130+
Within('.sidebar')
131+
I.click('Open editor')
132+
I.switchTo('#editor-frame') // automatically ends Within('.sidebar')
133+
I.fillField('content', 'Hello')
134+
I.switchTo() // exits iframe
135+
```
136+
137+
## Usage in Page Objects
138+
139+
In page objects, import `Within` directly:
140+
141+
```js
142+
// pages/Login.js
143+
import { Within } from 'codeceptjs/effects'
144+
145+
export default {
146+
loginForm: '.login-form',
147+
148+
fillCredentials(email, password) {
149+
Within(this.loginForm)
150+
I.fillField('Email', email)
151+
I.fillField('Password', password)
152+
Within()
153+
},
154+
155+
submitLogin(email, password) {
156+
this.fillCredentials(email, password)
157+
I.click('Log In')
158+
},
159+
}
160+
```
161+
162+
```js
163+
// tests/login_test.js
164+
Scenario('user can log in', ({ I, loginPage }) => {
165+
I.amOnPage('/login')
166+
loginPage.submitLogin('user@example.com', 'password')
167+
I.see('Dashboard')
168+
})
169+
```
170+
171+
The callback pattern also works in page objects:
172+
173+
```js
174+
// pages/Checkout.js
175+
import { Within } from 'codeceptjs/effects'
176+
177+
export default {
178+
async getTotal() {
179+
return await Within('.order-summary', async () => {
180+
return await I.grabTextFrom('.total')
181+
})
182+
},
183+
}
184+
```
185+
186+
## Deprecated: lowercase `within`
187+
188+
The lowercase `within()` is still available as a global function for backward compatibility, but it is deprecated:
189+
190+
```js
191+
// deprecated — still works, shows a one-time warning
192+
within('.form', () => {
193+
I.fillField('Name', 'John')
194+
})
195+
196+
// recommended
197+
import { Within } from 'codeceptjs/effects'
198+
Within('.form', () => {
199+
I.fillField('Name', 'John')
200+
})
201+
```
202+
203+
The global `within` only supports the callback pattern. For the begin/end pattern, you must import `Within`.
204+
205+
## Output
206+
207+
When running steps inside a `Within` block, the output shows them indented under the context:
208+
209+
```
210+
Within ".signup-form"
211+
I fill field "Email", "user@example.com"
212+
I fill field "Password", "secret"
213+
I click "Sign Up"
214+
I see "Account created"
215+
```
216+
217+
## Tips
218+
219+
- Prefer the begin/end pattern for simple linear flows — it's more readable.
220+
- Use the callback pattern when you need return values or want guaranteed cleanup.
221+
- Avoid deeply nesting `Within` blocks. If you find yourself needing nested contexts, consider restructuring your test.
222+
- `Within` cannot be used inside a `session`. Use `session` at the top level and `Within` inside it, not the other way around.

lib/effects.js

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@ import output from './output.js'
33
import store from './store.js'
44
import event from './event.js'
55
import container from './container.js'
6-
import MetaStep from './step/meta.js'
76
import { isAsyncFunction } from './utils.js'
7+
import { WithinContext, WithinStep } from './step/within.js'
8+
9+
function Within(context, fn) {
10+
if (!context && !fn) {
11+
WithinContext.endCurrent()
12+
return
13+
}
14+
15+
if (context && !fn) {
16+
const ctx = new WithinContext(context)
17+
ctx.start()
18+
return
19+
}
820

9-
/**
10-
* @param {CodeceptJS.LocatorOrString} context
11-
* @param {Function} fn
12-
* @return {Promise<*> | undefined}
13-
*/
14-
function within(context, fn) {
1521
const helpers = store.dryRun ? {} : container.helpers()
1622
const locator = typeof context === 'object' ? JSON.stringify(context) : context
1723

1824
return recorder.add(
1925
'register within wrapper',
2026
() => {
21-
const metaStep = new WithinStep(locator, fn)
27+
const metaStep = new WithinStep(locator)
2228
const defineMetaStep = step => (step.metaStep = metaStep)
2329
recorder.session.start('within')
2430

@@ -74,15 +80,13 @@ function within(context, fn) {
7480
)
7581
}
7682

77-
class WithinStep extends MetaStep {
78-
constructor(locator, fn) {
79-
super('Within')
80-
this.args = [locator]
81-
}
82-
83-
toString() {
84-
return `${this.prefix}Within ${this.humanizeArgs()}${this.suffix}`
83+
let withinDeprecationWarned = false
84+
function within(context, fn) {
85+
if (!withinDeprecationWarned) {
86+
withinDeprecationWarned = true
87+
output.print(' [deprecated] within() is deprecated. Use Within() from "codeceptjs/effects" instead.')
8588
}
89+
return Within(context, fn)
8690
}
8791

8892
/**
@@ -297,11 +301,12 @@ async function tryTo(callback) {
297301
)
298302
}
299303

300-
export { hopeThat, retryTo, tryTo, within }
304+
export { hopeThat, retryTo, tryTo, within, Within }
301305

302306
export default {
303307
hopeThat,
304308
retryTo,
305309
tryTo,
306310
within,
311+
Within,
307312
}

lib/helper/Playwright.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3576,6 +3576,10 @@ class Playwright extends Helper {
35763576
* {{> switchTo }}
35773577
*/
35783578
async switchTo(locator) {
3579+
if (this.withinLocator) {
3580+
await this._withinEnd()
3581+
}
3582+
35793583
if (Number.isInteger(locator)) {
35803584
// Select by frame index of current context
35813585

lib/helper/Puppeteer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,10 @@ class Puppeteer extends Helper {
26262626
* {{> switchTo }}
26272627
*/
26282628
async switchTo(locator) {
2629+
if (this.withinLocator) {
2630+
await this._withinEnd()
2631+
}
2632+
26292633
if (Number.isInteger(locator)) {
26302634
// Select by frame index of current context
26312635
let frames = []

lib/helper/WebDriver.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2759,6 +2759,10 @@ class WebDriver extends Helper {
27592759
* {{> switchTo }}
27602760
*/
27612761
async switchTo(locator) {
2762+
if (this.withinLocator) {
2763+
await this._withinEnd()
2764+
}
2765+
27622766
this.browser.isInsideFrame = true
27632767
if (!locator) {
27642768
return this.browser.switchFrame(null)

0 commit comments

Comments
 (0)