diff --git a/docs/effects.md b/docs/effects.md
index bf6d39a2d..2f23195af 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -7,7 +7,7 @@ Effects are functions that can modify scenario flow. They provide ways to handle
Effects can be imported directly from CodeceptJS:
```js
-const { tryTo, retryTo, within } = require('codeceptjs/effects')
+import { tryTo, retryTo, Within } from 'codeceptjs/effects'
```
> 📝 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 => {
}, 3)
```
-## within
+## Within
-The `within` effect allows you to perform multiple steps within a specific context (like an iframe or modal):
+The `Within` effect scopes actions to a specific element or iframe. It supports both a begin/end pattern and a callback pattern:
```js
-const { within } = require('codeceptjs/effects')
+import { Within } from 'codeceptjs/effects'
-// inside a test...
+// Begin/end pattern
+Within('.modal')
+I.see('Modal title')
+I.click('Close')
+Within()
-within('.modal', () => {
+// Callback pattern
+Within('.modal', () => {
I.see('Modal title')
I.click('Close')
})
```
+See the full [Within documentation](/within) for details on iframes, page objects, and `await` usage.
+
+> The lowercase `within()` is deprecated. Use `Within` instead.
+
## Usage with TypeScript
Effects are fully typed and work well with TypeScript:
```ts
-import { tryTo, retryTo, within } from 'codeceptjs/effects'
+import { tryTo, retryTo, Within } from 'codeceptjs/effects'
const success = await tryTo(async () => {
await I.see('Element')
})
```
-
-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!
diff --git a/docs/within.md b/docs/within.md
new file mode 100644
index 000000000..bf33eb937
--- /dev/null
+++ b/docs/within.md
@@ -0,0 +1,222 @@
+# Within
+
+`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.
+
+```js
+import { Within } from 'codeceptjs/effects'
+```
+
+## Begin / End Pattern
+
+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:
+
+```js
+Within('.signup-form')
+I.fillField('Email', 'user@example.com')
+I.fillField('Password', 'secret')
+I.click('Sign Up')
+Within()
+```
+
+Steps between `Within('.signup-form')` and `Within()` are scoped to `.signup-form`. After `Within()`, the context resets to the full page.
+
+### Auto-end previous context
+
+Starting a new `Within` automatically ends the previous one:
+
+```js
+Within('.sidebar')
+I.click('Dashboard')
+
+Within('.main-content') // ends .sidebar, begins .main-content
+I.see('Welcome')
+Within()
+```
+
+### Forgetting to close
+
+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.
+
+## Callback Pattern
+
+The callback pattern wraps actions in a function. The context is automatically closed when the function returns:
+
+```js
+Within('.signup-form', () => {
+ I.fillField('Email', 'user@example.com')
+ I.fillField('Password', 'secret')
+ I.click('Sign Up')
+})
+I.see('Account created')
+```
+
+### Returning values
+
+The callback pattern supports returning values. Use `await` on both the `Within` call and the inner action:
+
+```js
+const text = await Within('#sidebar', async () => {
+ return await I.grabTextFrom('h1')
+})
+I.fillField('Search', text)
+```
+
+## When to use `await`
+
+**Begin/end pattern** does not need `await`:
+
+```js
+Within('.form')
+I.fillField('Name', 'John')
+Within()
+```
+
+**Callback pattern** needs `await` when:
+
+- The callback is `async`
+- You need a return value from `Within`
+
+```js
+// async callback — await required
+await Within('.form', async () => {
+ await I.click('Submit')
+ await I.waitForText('Done')
+})
+```
+
+```js
+// sync callback — no await needed
+Within('.form', () => {
+ I.fillField('Name', 'John')
+ I.click('Submit')
+})
+```
+
+## Working with IFrames
+
+Use the `frame` locator to scope actions inside an iframe:
+
+```js
+// Begin/end
+Within({ frame: 'iframe' })
+I.fillField('Email', 'user@example.com')
+I.click('Submit')
+Within()
+
+// Callback
+Within({ frame: '#editor-frame' }, () => {
+ I.see('Page content')
+})
+```
+
+### Nested IFrames
+
+Pass an array of selectors to reach nested iframes:
+
+```js
+Within({ frame: ['.wrapper', '#content-frame'] }, () => {
+ I.fillField('Name', 'John')
+ I.see('Sign in!')
+})
+```
+
+Each selector in the array navigates one level deeper into the iframe hierarchy.
+
+### switchTo auto-disables Within
+
+If you call `I.switchTo()` while inside a `Within` context, the within context is automatically ended. This prevents conflicts between the two scoping mechanisms:
+
+```js
+Within('.sidebar')
+I.click('Open editor')
+I.switchTo('#editor-frame') // automatically ends Within('.sidebar')
+I.fillField('content', 'Hello')
+I.switchTo() // exits iframe
+```
+
+## Usage in Page Objects
+
+In page objects, import `Within` directly:
+
+```js
+// pages/Login.js
+import { Within } from 'codeceptjs/effects'
+
+export default {
+ loginForm: '.login-form',
+
+ fillCredentials(email, password) {
+ Within(this.loginForm)
+ I.fillField('Email', email)
+ I.fillField('Password', password)
+ Within()
+ },
+
+ submitLogin(email, password) {
+ this.fillCredentials(email, password)
+ I.click('Log In')
+ },
+}
+```
+
+```js
+// tests/login_test.js
+Scenario('user can log in', ({ I, loginPage }) => {
+ I.amOnPage('/login')
+ loginPage.submitLogin('user@example.com', 'password')
+ I.see('Dashboard')
+})
+```
+
+The callback pattern also works in page objects:
+
+```js
+// pages/Checkout.js
+import { Within } from 'codeceptjs/effects'
+
+export default {
+ async getTotal() {
+ return await Within('.order-summary', async () => {
+ return await I.grabTextFrom('.total')
+ })
+ },
+}
+```
+
+## Deprecated: lowercase `within`
+
+The lowercase `within()` is still available as a global function for backward compatibility, but it is deprecated:
+
+```js
+// deprecated — still works, shows a one-time warning
+within('.form', () => {
+ I.fillField('Name', 'John')
+})
+
+// recommended
+import { Within } from 'codeceptjs/effects'
+Within('.form', () => {
+ I.fillField('Name', 'John')
+})
+```
+
+The global `within` only supports the callback pattern. For the begin/end pattern, you must import `Within`.
+
+## Output
+
+When running steps inside a `Within` block, the output shows them indented under the context:
+
+```
+ Within ".signup-form"
+ I fill field "Email", "user@example.com"
+ I fill field "Password", "secret"
+ I click "Sign Up"
+ I see "Account created"
+```
+
+## Tips
+
+- Prefer the begin/end pattern for simple linear flows — it's more readable.
+- Use the callback pattern when you need return values or want guaranteed cleanup.
+- Avoid deeply nesting `Within` blocks. If you find yourself needing nested contexts, consider restructuring your test.
+- `Within` cannot be used inside a `session`. Use `session` at the top level and `Within` inside it, not the other way around.
diff --git a/lib/effects.js b/lib/effects.js
index fc14197aa..65de954ca 100644
--- a/lib/effects.js
+++ b/lib/effects.js
@@ -3,22 +3,28 @@ import output from './output.js'
import store from './store.js'
import event from './event.js'
import container from './container.js'
-import MetaStep from './step/meta.js'
import { isAsyncFunction } from './utils.js'
+import { WithinContext, WithinStep } from './step/within.js'
+
+function Within(context, fn) {
+ if (!context && !fn) {
+ WithinContext.endCurrent()
+ return
+ }
+
+ if (context && !fn) {
+ const ctx = new WithinContext(context)
+ ctx.start()
+ return
+ }
-/**
- * @param {CodeceptJS.LocatorOrString} context
- * @param {Function} fn
- * @return {Promise<*> | undefined}
- */
-function within(context, fn) {
const helpers = store.dryRun ? {} : container.helpers()
const locator = typeof context === 'object' ? JSON.stringify(context) : context
return recorder.add(
'register within wrapper',
() => {
- const metaStep = new WithinStep(locator, fn)
+ const metaStep = new WithinStep(locator)
const defineMetaStep = step => (step.metaStep = metaStep)
recorder.session.start('within')
@@ -74,15 +80,13 @@ function within(context, fn) {
)
}
-class WithinStep extends MetaStep {
- constructor(locator, fn) {
- super('Within')
- this.args = [locator]
- }
-
- toString() {
- return `${this.prefix}Within ${this.humanizeArgs()}${this.suffix}`
+let withinDeprecationWarned = false
+function within(context, fn) {
+ if (!withinDeprecationWarned) {
+ withinDeprecationWarned = true
+ output.print(' [deprecated] within() is deprecated. Use Within() from "codeceptjs/effects" instead.')
}
+ return Within(context, fn)
}
/**
@@ -297,11 +301,12 @@ async function tryTo(callback) {
)
}
-export { hopeThat, retryTo, tryTo, within }
+export { hopeThat, retryTo, tryTo, within, Within }
export default {
hopeThat,
retryTo,
tryTo,
within,
+ Within,
}
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index de2925bda..2cbf8c30d 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -2278,24 +2278,10 @@ class Playwright extends Helper {
}
/**
- * Clears the text input element: ``, `