variable
STORY_ROLE
export const STORY_ROLE: "story" = "story";The role segment a story file carries.
API reference
Named, rendered states of a component that a person and a test can both reach, part of the Unified Toolchain for Flow.
Written from the source by uf doc when this site was built: the signature and the comment above each export, grouped by the specifier a program imports it from.
@uniflowed/story/collectvariable
STORY_ROLEexport const STORY_ROLE: "story" = "story";The role segment a story file carries.
variable
STORY_FILEexport const STORY_FILE: "$story.js" = "$story.js";The name of a story file with no variant: the one the runner renders.
type
StoryVariantexport type StoryVariant = "default" | "native" | "ios" | "android" | "web" | "test";Which build a story file applies to.
The router's vocabulary, exactly. "default" has no segment in the name.
function
classifyStoryFileexport function classifyStoryFile(fileName: string): StoryVariant | null { ... }The variant of fileName as a story file, or null if it is not one.
Takes a file name, not a path, for the reason the Rust classifier does: a caller with a path can normalise it two ways and get two answers.
function
isStoryEntryexport function isStoryEntry(fileName: string): boolean { ... }Whether fileName is the story file a renderer mounts.
The platform variants are companions to a story, never stories of their own — the same distinction ReservedVariant::is_route_entry draws.
type
FindOptionsexport type FindOptions = {|
/** Directory names to skip, replacing the defaults. */
readonly ignore?: $ReadOnlyArray<string>,
/**
* How deep to descend below `root`. Defaults to 32.
*
* A bound rather than a promise not to recurse: a directory tree is
* untrusted input the moment a generated or vendored directory is in it,
* and an unbounded walk is an unbounded stack.
*/
readonly maxDepth?: number,
|};What to leave out of a walk.
function
findStoryFilesexport function findStoryFiles(root: string, options?: FindOptions): Promise<Array<string>> { ... }Every story file under root, as absolute paths, in a stable order.
Sorted, so a catalogue, a report and a set of baselines come out in the same order on every machine.
Symbolic links are not followed. That falls out of asking readdir for directory entries rather than stating it as a rule: an entry describes the link itself, so a link is neither isDirectory() nor isFile() and the walk passes it by. It has to be that way round — a link to a parent is a walk that does not terminate, and a link into node_modules is somebody else's story file.
function
loadStoryFileexport function loadStoryFile(file: string): Promise<Array<StorySet>> { ... }Import file and return every story set it exports.
Throws when it exports none. See the module docs: the name is a claim.
The import is by file: URL rather than by path, because a bare path is resolved against the *importer* on every host uf supports, and this module is not where the story file lives.
type
StoryIndexexport type StoryIndex = {|
/** The files the sets came from, in walk order. */
readonly files: $ReadOnlyArray<string>,
/** Every set, in the order its file was found. */
readonly sets: $ReadOnlyArray<StorySet>,
/** Every story in every set, flattened, in declaration order. */
readonly stories: $ReadOnlyArray<Story>,
/** The story with this id, or `undefined`. */
readonly get: (id: string) => Story | void,
|};Every story a project declares, and how to reach one.
function
indexStoriesexport function indexStories(files: $ReadOnlyArray<string>): Promise<StoryIndex> { ... }Build an index from files that have already been found.
Separate from [collectStories] so that a host which discovered the files some other way — natively, from a watcher, from a changed-files list — can still build the same index.
Two stories with one id is an error, and it names both files. An id is what a failing job prints and what a visual baseline is filed under, so a collision does not produce a confusing result: it produces the *wrong* one, silently, for whichever of the two was written second.
function
collectStoriesexport function collectStories(root: string, options?: FindOptions): Promise<StoryIndex> { ... }Find every story file under root and index what they declare.
@uniflowed/story/playtype
Stepexport type Step = (name: string, body: () => mixed) => Promise<void>;A named stretch of a play function.
type
PlayStageexport type PlayStage = {|
/** The element this story was mounted into. */
readonly container: Element,
/** Queries scoped to `container`. */
readonly canvas: Queries,
/** What a person did: click, type, tab, hover. */
readonly user: typeof userEvent,
/** Name a stretch of the play, so a failure says where it was. */
readonly step: Step,
|};What every play function is handed, minus the props.
Split out from [PlayContext] because it is the part that does not depend on the story's type parameter — render.js builds one of these knowing only a DOM node, and story.js closes over the typed props to complete it.
type
PlayContextexport type PlayContext<Props> = {|
...PlayStage,
/** The props this story was rendered with. */
readonly props: Props,
|};What a play function declared on a typed story set is handed.
type
PlayFunctionexport type PlayFunction = (stage: PlayStage) => mixed;A play function with its story's props already inside it.
This is the shape a resolved [Story](./story.js) holds: story.js wraps the caller's typed function so that everything downstream can call it with a stage and nothing else.
class
StoryPlayErrorexport class StoryPlayError extends Error { ... }A play function's failure, told where it happened.
The original message is kept in full and the original error in cause, because the useful half of an assertion failure is the assertion's own account of what it expected. What this adds is the story id and the step path, which the assertion cannot know.
function
createStageexport function createStage(container: Element): {|
readonly stage: PlayStage,
readonly stepPath: () => string | null,
|} { ... }Build the stage for a story mounted into container.
The step this returns writes into path, which [runPlay] reads when something throws. A step that fails leaves its name in place on purpose: the failure is reported from the innermost step that was running, not from wherever the stack happened to unwind to.
function
runPlayexport function runPlay(
play: PlayFunction,
story: string,
stage: PlayStage,
stepPath: () => string | null,
): Promise<void> { ... }Run play for the story called story, on stage.
Always awaits, even for a synchronous play function, so that a play that grows an await later does not change when its failure surfaces — a synchronous throw and a rejected promise both arrive here.
@uniflowed/story/rendertype
MountedStoryexport type MountedStory = {|
/** The story that was mounted. */
readonly story: Story,
/** The element it was mounted into. */
readonly container: Element,
/** Queries scoped to `container`. */
readonly canvas: Queries,
/**
* Requests this story made, in request order.
*
* The registry's live log, not a copy, so a caller that holds on to it
* across an interaction sees what the interaction asked for. Empty and
* permanently so when the story declares no handlers — nothing is watching.
*/
readonly requests: $ReadOnlyArray<RecordedRequest>,
/**
* Run the story's play function, if it has one.
*
* Resolves immediately when it has none, so a caller never has to ask.
* Throws a [`StoryPlayError`](./play.js) naming the story and the step.
*/
readonly play: () => Promise<void>,
/** The story's markup, as it stands. */
readonly html: () => string,
/** Take it down and stop intercepting. Safe to call twice. */
readonly unmount: () => void,
|};A story on screen, and everything a caller can do with it.
function
renderStoryToHtmlexport function renderStoryToHtml(
story: Story,
options?: {| readonly play?: boolean |},
): Promise<string> { ... }The story's markup, for something that is not a test.
A static story page, a review artefact, a diff in a pull request: all of them need the same string, and none of them should mount React themselves.
play is off by default. The markup of a story *as declared* is what a catalogue shows; the markup after it has been driven is a different and equally useful picture, and the caller is the one who knows which they meant.
@uniflowed/story/runnerfunction
storyTestexport function storyTest(story: Story): () => Promise<void> { ... }The body of one story's test: mount it, play it, take it down.
The unmount is in a finally because a failed assertion inside a play function would otherwise leave the story mounted and its handlers installed — and @uniflowed/mock refuses to nest, so the *next* story would fail with a message about interception rather than about itself.
function
describeStoriesexport function describeStories(set: StorySet): void { ... }Register one test per story in set, under a suite named after it.
Read the module docs before relying on this as a file's only content: the names come from the set rather than from source text, so uf test will not discover the file on its own.
@uniflowed/story/storytype
StoryPropsexport type StoryProps = { readonly [string]: mixed };A story's props once the type parameter is gone.
mixed, not any: a consumer showing a story's props in a panel has to narrow each one, which is correct — it genuinely does not know what they are.
type
Decoratorexport type Decorator = (children: React.Node) => React.Node;Something wrapped around a story before it is mounted.
A theme provider, a router context, a fixed-width frame. It takes the node rather than a render function because there is nothing else useful to give it: React already defers the work, and a decorator that could choose *not* to call its child would be a decorator that can silently render nothing.
type
StoryDeclarationexport type StoryDeclaration<Props extends { ... }> = {|
/**
* What a person calls this state. Defaults to the key it was declared
* under, which is usually already the right words.
*/
readonly name?: string,
/** What differs from the set's props. */
readonly props?: Partial<Props>,
/** Wrapped inside the set's decorators. */
readonly decorators?: $ReadOnlyArray<Decorator>,
/** Offered before the set's, so a story can override one of them. */
readonly mocks?: $ReadOnlyArray<MockHandler>,
/** Drives this state and asserts on it. Replaces the set's `play`. */
readonly play?: (context: PlayContext<Props>) => mixed,
|};One named state, as the caller writes it.
type
StorySetConfigexport type StorySetConfig<Props extends { ... }> = {|
/** What the component is called. `"Forms/Button"` groups it. */
readonly title: string,
/**
* The component every story in the set renders.
*
* `component(...Props)` rather than `component(...Props) renders X`: a
* story renders whatever its component renders, and constraining that here
* would be this package having an opinion about a component it was handed.
*/
readonly component: component(...Props),
/** Complete props, so every story below is renderable. */
readonly props: Props,
/** Wrapped around every story, outside the story's own decorators. */
readonly decorators?: $ReadOnlyArray<Decorator>,
/** In force for every story in the set, behind the story's own. */
readonly mocks?: $ReadOnlyArray<MockHandler>,
/** Run for every story that does not declare its own. */
readonly play?: (context: PlayContext<Props>) => mixed,
/**
* The states, in declaration order.
*
* An object rather than an array because the key is the story's identity —
* it is what a test names and what an id is built from — and an array of
* `{ name, … }` records makes that a field somebody can forget.
*/
readonly stories: { readonly [key: string]: StoryDeclaration<Props> },
|};A component's stories, as the caller writes them.
type
Storyexport type Story = {|
/** `"button--pending"`. Stable, path-safe, and derived from what was written. */
readonly id: string,
/** The key it was declared under. */
readonly key: string,
/** What a person calls it. */
readonly name: string,
/** The set's title, repeated here so a story is self-describing. */
readonly title: string,
/** The set's props with this story's overrides applied, erased to `mixed`. */
readonly props: StoryProps,
/** The set's decorators, then this story's. First is outermost. */
readonly decorators: $ReadOnlyArray<Decorator>,
/**
* This story's handlers, then the set's.
*
* That way round because `@uniflowed/mock` offers a request to handlers in
* order and the first that matches answers it: a story that declares
* `GET /users/:id` overrides the set's handler for the same route, which is
* what a story called `Missing` is for.
*/
readonly mocks: $ReadOnlyArray<MockHandler>,
/** What drives it, or `null`. */
readonly play: PlayFunction | null,
/**
* The element, built on demand.
*
* A function rather than a node so that declaring a thousand stories costs
* a thousand closures rather than a thousand React elements, and so a story
* rendered twice gets two elements rather than one shared one.
*/
readonly element: () => React.Node,
|};One story, resolved: everything it needs to be rendered, and nothing else.
variable
STORY_SETexport const STORY_SET: "uniflowed/story-set" = "uniflowed/story-set";The brand [isStorySet] looks for.
A string rather than a class or a Symbol(), because the check has to hold across two copies of this package in one process — a linked workspace beside a nested install is the ordinary way that happens — and instanceof does not.
type
StorySetexport type StorySet = {|
readonly kind: typeof STORY_SET,
readonly title: string,
readonly stories: $ReadOnlyArray<Story>,
|};A component's stories, resolved.
function
defineStoriesexport function defineStories<Props extends { ... }>(config: StorySetConfig<Props>): StorySet { ... }Resolve a component's stories.
Everything is computed here: inheritance, names, ids and the element closures. A [StorySet] is therefore inert data — the reason a tool can import a story file to list what is in it without a DOM, a runner or a network.
Throws when the set is empty. A story file that declares no stories is a file somebody meant to finish, and reporting it as zero stories hides that at exactly the moment it is cheap to notice.
function
storyIdexport function storyId(title: string, name: string): string { ... }The id title and name produce.
Exported because a visual-regression baseline, a URL and a report all have to agree on it, and each computing its own would agree until the day one of them handled a slash differently.
function
isStorySetexport function isStorySet(value: mixed): boolean { ... }Whether value is a [StorySet].
function
findStoryexport function findStory(set: StorySet, key: string): Story { ... }The story in set under key, or named name.
Throws rather than returning undefined, and says what the set does hold. The caller is a test naming a story it believes exists; handing it void turns a renamed story into a TypeError three lines later, in the runner rather than in the test.