npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sayer/agent-step

v0.1.5

Published

Playwright fixture for agentic test steps you mix with locators and assertions

Readme

@sayer/agent-step

npm version npm downloads

@sayer/agent-step is a Playwright fixture for adding agentic actions to tests and combining them with other test steps.

You keep writing Playwright as usual. When a bit of the UI is annoying to locate, you hand that part to agentStep in plain English, then assert the result yourself.

await agentStep({
  action: 'Add the red medium shirt to the basket',
  expect: ['The basket badge shows 1'],
});

await expect(page.locator('#badge')).toHaveText('1');

It drives the test's existing page. It does not spin up another browser or go through Playwright MCP.

Installation

npm install @sayer/agent-step

Add -D if you only use it in tests. @playwright/test is a peer dependency (1.63+). Needs an OpenAI-compatible model that can do tool calling. OpenRouter works.

Add it as a fixture

This is the bit you want. Put this in tests/fixtures.ts so it sits next to your other fixtures:

import { test as base, expect } from '@playwright/test';
import {
  agentStepFixture,
  type AgentStepFixtures,
} from '@sayer/agent-step';

export const test = base.extend<AgentStepFixtures>(agentStepFixture);
export { expect };

Then import test from there, not from @playwright/test:

import { test, expect } from './fixtures';

test('checkout', async ({ page, agentStep }) => {
  await page.goto('/shop');

  await agentStep({
    action: 'Add the red medium shirt to the basket',
    expect: [
      'The basket badge shows 1',
      'The basket contains the red medium shirt',
    ],
  });

  await expect(page.locator('#badge')).toHaveText('1');
});

action is what to do. expect is what should be true afterwards. Those are checked separately so the model that clicked around is not also marking its own homework.

Or call it directly

If you do not want a fixture, keep the normal Playwright import and pass page in:

import { test, expect } from '@playwright/test';
import { agentStep } from '@sayer/agent-step';

test('checkout', async ({ page }) => {
  await page.goto('/shop');

  await agentStep(page, {
    action: 'Add the red medium shirt to the basket',
    expect: ['The basket contains the red medium shirt'],
  });
});

Env

Set these where you run Playwright. The package reads process.env and does not load .env for you.

export AGENT_LLM_BASE_URL=https://openrouter.ai/api/v1
export AGENT_LLM_API_KEY=sk-or-...
export AGENT_LLM_MODEL=openai/gpt-4o-mini

AGENT_LLM_BASE_URL is the API root. Do not put /chat/completions on the end.

Same shape works for OpenAI (https://api.openai.com/v1) or anything else that speaks Chat Completions with tools.

The model has to support tool calling. If you get "no choices" back, it is usually the model slug or tools not being supported.

Custom headers

Pass extra LLM headers when you create the step. Values have to be strings. They go out on every model request for that factory, including verification.

import { test as base, expect } from '@playwright/test';
import {
  createAgentStep,
  type AgentStepFixtures,
} from '@sayer/agent-step';

export const test = base.extend<AgentStepFixtures>({
  agentStep: async ({ page }, use, testInfo) => {
    await use(
      createAgentStep({
        page,
        testInfo,
        headers: {
          'X-Title': 'checkout-tests',
        },
      }),
    );
  },
});

export { expect };

A header you set replaces the built-in HTTP-Referer or X-OpenRouter-Title when the name matches. agentStepFixture does not take headers. Use createAgentStep in your fixture when you need them.

Secrets

Do not put passwords in the prompt. Use a placeholder and pass the real value in secrets:

await agentStep({
  action: 'Sign in with the test account email',
  expect: ['Signed in as the test account email'],
  secrets: { EMAIL: process.env.TEST_EMAIL! },
});

In the action, tell it to type %EMAIL%. The value is filled in the browser and redacted from reports.

Timeout

Each step gets 60 seconds for the action plus verification. Pass timeout in milliseconds if you need longer. This still throws when soft is true. It is the agent budget, not Playwright's test timeout, so raise that too if the step can run long.

await agentStep({
  action: 'Add the red medium shirt to the basket',
  expect: ['The basket badge shows 1'],
  timeout: 120_000,
});

Check without an action

Leave out action when the page is already in the state you want to judge. The model does not click or type. It only reads the page and checks expect.

await agentStep({
  expect: ['The order total equals the sum of the line items'],
});

Use this when the numbers or layout are not stable enough for a locator, but the relationship on the page should still hold. Formatting and position can differ. If the snapshot does not contain the values, the check fails.

Soft verification

By default a failed expect throws. Pass soft: true to attach the miss and continue, so later Playwright assertions can still run. Timeouts and tool errors still throw.

await agentStep({
  action: 'Add the red medium shirt to the basket',
  expect: ['The basket badge shows 1'],
  soft: true,
});

await expect(page.locator('#badge')).toHaveText('1');

How it behaves

Each agentStep is a Playwright test.step. It snapshots the page, does a small allowlisted set of actions (browser_click, browser_type, etc), then asks the model again whether the expect lines hold. Transcripts and failures get attached to the report.

It will not run arbitrary JS or leave the current origin. Retries are off so it does not click pay twice.

This is not a replacement for Playwright assertions. Use expect for anything you actually care about. Pin the model in CI. It will cost tokens and it will flake more than a locator.

Links