Problem Description
Currently, step outcome details (like the resolved state name or the winning element's locator) are only passed to the immediately subsequent step via the lastOutcome parameter.
If an outcome (such as a checked checkbox or row locator resolved in a prior .detect()) is needed multiple steps later or in .cleanup(), developers must manually store it in the mutable context ctx. However, because ctx is defined and passed by the user (e.g. Playbook.withCtx({ page, userContext })), library-injected properties under ctx run a high risk of clashing with user-defined keys (namespace pollution/collisions).
Additionally, using ctx bypasses TypeScript static analysis, making it easy to introduce runtime bugs due to typoed keys.
Proposed Solution
Instead of writing to the user-owned ctx object, refactor the Play step callback signatures to receive a read-only History Object (e.g. history or outcomes) as their third parameter:
async (page: Page, ctx: PlayCtx, history: PlayHistory) => void
This object will:
- Retain a
lastOutcome reference for convenient adjacent access.
- Store the outcome details of every named
.detect() or .attempt() step in the play, indexed by the step name.
To support this cleanly, .detect() will support an optional first argument name: string (aligning it with the existing .attempt(name, ...) signature).
Example API Usage
new Play()
.detect('findRow', (page) => [
{ name: 'found', locator: page.getByRole('row') },
{ name: 'missing', isTimeoutOutcome: true }
])
.prep('modify row', async (page, ctx, history) => {
// history.lastOutcome is still available and equivalent to history.findRow here
if (history.lastOutcome.name === 'found') {
await history.lastOutcome.locator.click();
}
})
.attempt('saveUpdate', async (page) => {
await page.getByRole('button', { name: 'Save' }).click();
}, [
ToastOutcomes.success('updated'),
ToastOutcomes.failure('failed')
])
.cleanup('revert changes', async (page, ctx, history) => {
// Access outcomes from different steps in the history without namespace collisions or type-casting!
if (history.findRow?.name === 'found' && history.saveUpdate?.name === 'updated') {
const row = history.findRow.locator;
await row.click();
// Perform revert...
}
})
TypeScript Benefits
Each step in the Play builder chain can accumulate type information, giving developers full type-safety and autocomplete on history:
- Typing
history.findRow as { name: 'found' | 'missing', locator?: Locator }.
- Typing
history.saveUpdate as { name: 'updated' | 'failed' }.
Problem Description
Currently, step outcome details (like the resolved state name or the winning element's locator) are only passed to the immediately subsequent step via the
lastOutcomeparameter.If an outcome (such as a checked checkbox or row locator resolved in a prior
.detect()) is needed multiple steps later or in.cleanup(), developers must manually store it in the mutable contextctx. However, becausectxis defined and passed by the user (e.g.Playbook.withCtx({ page, userContext })), library-injected properties underctxrun a high risk of clashing with user-defined keys (namespace pollution/collisions).Additionally, using
ctxbypasses TypeScript static analysis, making it easy to introduce runtime bugs due to typoed keys.Proposed Solution
Instead of writing to the user-owned
ctxobject, refactor the Play step callback signatures to receive a read-only History Object (e.g.historyoroutcomes) as their third parameter:This object will:
lastOutcomereference for convenient adjacent access..detect()or.attempt()step in the play, indexed by the step name.To support this cleanly,
.detect()will support an optional first argumentname: string(aligning it with the existing.attempt(name, ...)signature).Example API Usage
TypeScript Benefits
Each step in the Play builder chain can accumulate type information, giving developers full type-safety and autocomplete on
history:history.findRowas{ name: 'found' | 'missing', locator?: Locator }.history.saveUpdateas{ name: 'updated' | 'failed' }.