Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 11 additions & 50 deletions docs/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ Effects are functions that can modify scenario flow. They provide ways to handle
Effects can be imported directly from CodeceptJS:

```js
// ESM
import { tryTo, retryTo, within } from 'codeceptjs/effects'

// CommonJS
const { tryTo, retryTo, within } = require('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.
Expand Down Expand Up @@ -82,58 +78,25 @@ await retryTo(tries => {

## within

The `within` effect scopes all actions inside it to a specific element on the page — useful when working with repeated UI components or narrowing interaction to a specific section.
The `within` effect scopes actions to a specific element or iframe. It supports both a begin/leave pattern and a callback pattern:

```js
import { within } from 'codeceptjs/effects'

// inside a test...
await within('.js-signup-form', () => {
I.fillField('user[login]', 'User')
I.fillField('user[email]', 'user@user.com')
I.fillField('user[password]', 'user@user.com')
I.click('button')
})
I.see('There were problems creating your account.')
```

> ⚠ `within` can cause problems when used incorrectly. If you see unexpected behavior, refactor to use the context parameter on individual actions instead (e.g. `I.click('Login', '.nav')`). Keep `within` for the simplest cases.

> ⚠ Since `within` returns a Promise, always `await` it when you need its return value.

### IFrames

Use a `frame` locator to scope actions inside an iframe:

```js
await within({ frame: '#editor' }, () => {
I.see('Page')
I.fillField('Body', 'Hello world')
})
```

Nested iframes _(WebDriver & Puppeteer only)_:
// Begin/leave pattern
const area = within('.modal')
I.see('Modal title')
I.click('Close')
area.leave()

```js
await within({ frame: ['.content', '#editor'] }, () => {
I.see('Page')
// Callback pattern
within('.modal', () => {
I.see('Modal title')
I.click('Close')
})
```

> ℹ IFrames can also be accessed via `I.switchTo` command.

### Returning Values

`within` can return a value for use in the scenario:

```js
const val = await within('#sidebar', () => {
return I.grabTextFrom({ css: 'h1' })
})
I.fillField('Description', val)
```

When running steps inside a `within` block, they will be shown indented in the output.
See the full [within documentation](/within) for details on iframes, page objects, and `await` usage.

## Usage with TypeScript

Expand All @@ -146,5 +109,3 @@ 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!
213 changes: 187 additions & 26 deletions docs/within.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,216 @@
---
permalink: /within
title: Within
title: within
---

# Within
# within

`within` scopes all actions inside it to a specific element on the page — useful when working with repeated UI components or narrowing interaction to a specific section.
`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
within('.js-signup-form', () => {
I.fillField('user[login]', 'User')
I.fillField('user[email]', 'user@user.com')
I.fillField('user[password]', 'user@user.com')
I.click('button')
import { within } from 'codeceptjs/effects'
```

## Begin / Leave Pattern

The simplest way to use `within` is the begin/leave pattern. Call `within` with a locator to start — it returns a context object. Call `.leave()` on it when done:

```js
const area = within('.signup-form')
I.fillField('Email', 'user@example.com')
I.fillField('Password', 'secret')
I.click('Sign Up')
area.leave()
```

Steps between `within('.signup-form')` and `area.leave()` are scoped to `.signup-form`. After `leave()`, the context resets to the full page.

You can also end a context by calling `within()` with no arguments:

```js
within('.signup-form')
I.fillField('Email', 'user@example.com')
within()
```

### 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 `leave()` or `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('There were problems creating your account.')
I.see('Account created')
```

> ⚠ `within` can cause problems when used incorrectly. If you see unexpected behavior, refactor to use the context parameter on individual actions instead (e.g. `I.click('Login', '.nav')`). Keep `within` for the simplest cases.
> Since `within` returns a Promise, always `await` it when you need its return value.
### Returning values

## IFrames
The callback pattern supports returning values. Use `await` on both the `within` call and the inner action:

Use a `frame` locator to scope actions inside an iframe:
```js
const text = await within('#sidebar', async () => {
return await I.grabTextFrom('h1')
})
I.fillField('Search', text)
```

## When to use `await`

**Begin/leave pattern** does not need `await`:

```js
const area = within('.form')
I.fillField('Name', 'John')
area.leave()
```

**Callback pattern** needs `await` when:

- The callback is `async`
- You need a return value from `within`

```js
within({ frame: '#editor' }, () => {
I.see('Page')
I.fillField('Body', 'Hello world')
// async callback — await required
await within('.form', async () => {
await I.click('Submit')
await I.waitForText('Done')
})
```

Nested iframes _(WebDriver & Puppeteer only)_:
```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
within({ frame: ['.content', '#editor'] }, () => {
I.see('Page')
// Begin/leave
const area = within({ frame: 'iframe' })
I.fillField('Email', 'user@example.com')
I.click('Submit')
area.leave()

// Callback
within({ frame: '#editor-frame' }, () => {
I.see('Page content')
})
```

> ℹ IFrames can also be accessed via `I.switchTo` command.
### Nested IFrames

## Returning Values
Pass an array of selectors to reach nested iframes:

```js
within({ frame: ['.wrapper', '#content-frame'] }, () => {
I.fillField('Name', 'John')
I.see('Sign in!')
})
```

`within` can return a value for use in the scenario:
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
const area = 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
const val = await within('#sidebar', () => {
return I.grabTextFrom({ css: 'h1' })
// pages/Login.js
import { within } from 'codeceptjs/effects'

export default {
loginForm: '.login-form',

fillCredentials(email, password) {
const area = within(this.loginForm)
I.fillField('Email', email)
I.fillField('Password', password)
area.leave()
},

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')
})
I.fillField('Description', val)
```

When running steps inside a `within` block, they will be shown indented in the output.
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')
})
},
}
```

## 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/leave 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.
29 changes: 15 additions & 14 deletions lib/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@ 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'

/**
* @param {CodeceptJS.LocatorOrString} context
* @param {Function} fn
* @return {Promise<*> | undefined}
*/
function within(context, fn) {
if (!context && !fn) {
WithinContext.endCurrent()
return
}

if (context && !fn) {
const ctx = new WithinContext(context)
ctx.start()
return ctx
}

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')

Expand Down Expand Up @@ -74,17 +85,6 @@ 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}`
}
}

/**
* A utility function for CodeceptJS tests that acts as a soft assertion.
* Executes a callback within a recorded session, ensuring errors are handled gracefully without failing the test immediately.
Expand Down Expand Up @@ -297,11 +297,12 @@ async function tryTo(callback) {
)
}

export { hopeThat, retryTo, tryTo, within }
export { hopeThat, retryTo, tryTo, within, within as Within }

export default {
hopeThat,
retryTo,
tryTo,
within,
Within: within,
}
Loading