MidstreamDocs
Marking scenes

Parameters: one scene, many variants

Declare a value in your test, and let whoever opens the scene choose it.

You want to review the empty cart and the full cart. They are the same moment in the same flow, so they should not be two scenes. Declare the difference as a parameter, and each reviewer picks a value when they open the scene.

Declaring one

midstreamParameter() wraps the value your test would have produced anyway. It takes a factory function and a description of the value, and returns a value your test uses normally:

import { test } from "@playwright/test";
import {
  midstreamScene,
  midstreamParameter,
} from "@midstream/sdk/playwright";

const CATEGORIES = ["electronics", "books", "garden"];

test("a customer can browse a category", async ({ page }) => {
  const category = midstreamParameter(
    () => CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)]!,
    {
      name: "category",
      label: "Category",
      type: "enum",
      values: CATEGORIES,
      description: "Which department to open.",
    },
  );

  await page.goto(`/browse/${category}`);

  await midstreamScene(page, "browse-category");
});

Note what the test does not do: it does not read an environment variable, and it does not branch on whether Midstream is running. It asks for a category and gets one. On your laptop that is the factory's random pick. In CI it is also the factory's pick, and the parameter's shape is registered alongside the capture. In a deploy it is the value the reviewer chose.

Declare parameters before the scene call and in the same test. The scene call is what collects them, so a declaration after it is not registered with that capture, and a declaration in a test with no scene call is not registered at all.

midstreamParameter() is not async — there is nothing to await.

Options

OptionTypeNotes
namestringRequired. The stable key. It is the form field's identity and the key the chosen value comes back under, so renaming it is like renaming a slug: the old choices no longer apply.
type"enum" | "string" | "number" | "boolean"Required. Decides the control on the form and how the chosen value is converted before your test sees it.
valuesarrayRequired for enum, ignored otherwise. The allowed choices, in the order they appear in the dropdown.
labelstringThe name shown on the form. Defaults to name in title case.
defaultscalarWhat the form is prefilled with. Defaults to whatever the factory returned when the capture was registered.
descriptionstringOne line of help shown under the label. Worth writing — the person choosing is often not the person who wrote the test.

Values are single values: a string, a number, or a boolean. Not objects, not arrays. Parameters are a form, and a form collects scalars.

Names are unique within a test. Declaring the same name twice replaces the first definition rather than adding a second field.

What a reviewer sees

Each of the four types gets the control you would expect: enum is a dropdown of its values, string a text box, number a number box, boolean a switch.

Opening a scene that declares parameters takes you to a short form first, instead of dropping you straight into an instance. Choose the values, submit, and you get an instance built with those values. From a scene's page you can also launch another instance with a different set — name it something you will recognize, and the two sit side by side.

That is the point of the feature: one scene, and as many live variants of it as the conversation needs. Each set of chosen values gets its own instance, with its own state, which nobody else's clicking disturbs.

If an instance is created without anyone choosing — the plain "open this scene" path, for a scene that declares nothing — the values used are the declared defaults: the explicit default, or the first values entry for an enum, or an empty value for the rest.

How the value reaches your test

The whole path, end to end:

  1. midstreamParameter() runs in CI and records the parameter's shape.
  2. The scene call attaches those shapes to the capture. They are stored with it, so a scene's form is whatever the test declared at that commit.
  3. Someone opens the scene, and the stored shapes are rendered as the launch form.
  4. The values they choose are stored on the instance they created.
  5. Standing that instance up runs your test again, with those values supplied to the test process through its environment.
  6. midstreamParameter() picks them up and returns the chosen one instead of calling the factory.

Step 6 is the reason your test never touches the environment itself. If a value is missing or unreadable, the factory runs and the test proceeds with its own data.

The conversion in step 6 is worth one line of detail, because a form only ever hands back text. A number is converted with Number(), so a field left as nonsense arrives as NaN rather than an error. A boolean is true only for the exact string true. enum and string arrive as strings — which means a numeric enum comes back as "3", not 3.

Keep the set small

Every parameter multiplies what the person opening the scene has to decide, and a form with six fields is a form that gets submitted at its defaults.

  • One or two parameters per scene. If you want a third, consider whether it is really a second scene.
  • Prefer enum with a handful of named choices over a free-text field. "Which of these three" is a question anyone can answer; "type a plan name" is not.
  • Let the default be the interesting case, not the empty one.
  • Parameters are recorded per capture, so removing a declaration from the test removes the field from the next capture onward. Instances built earlier keep the values they were built with.

Like the scene call, midstreamParameter() never fails your test. If anything inside it goes wrong it warns and returns the factory's value.