Skip to content

[Enhancement] Step History and Multi-Step Outcome Access via a Unified History Object #51

Description

@rickcedwhat-ai

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:

  1. Retain a lastOutcome reference for convenient adjacent access.
  2. 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' }.

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedBlocked by another issueplaybook-frameworkBelongs to the future RBAC/playbook library

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions