@harnessed-ts/storybook 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # @harnessed-ts/storybook
2
+
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3f53eb6: Ember stories. `@harnessed-ts/storybook/ember` renders stories as Ember components under `@storybook/html-vite`, through Ember's public `renderComponent` (Ember 6.8 or later, Storybook 10): spread `emberRenderer` and its `harnessed()` into `.storybook/preview.ts`, and a story renders its meta's `component` with the story's args, or whatever its `render` returns, `<template>` included. Its `harnessed()` resolves every harness interaction once Ember has settled (rendering, run loop, timers and `@ember/test-waiters`), as `@harnessed-ts/ember` does; `settled()` is exported too. `parameters.ember.owner` sets the owner a story renders with. `composeStories`, `composeStory` and `setProjectAnnotations` from the same entry run Ember stories as portable stories, and `Meta`, `StoryObj` and `Preview` type them. `emberIndexer` from `@harnessed-ts/storybook/ember/node` indexes `.stories.gjs` and `.stories.gts` files, whose `<template>` tags Storybook's own indexer cannot parse; it needs `content-tag`. The conformance catalog runs against Ember stories all three ways React's do: as portable stories, through `StoryPage` under Playwright, and through the Vitest addon.
8
+
9
+ `storiesModule()` from `@harnessed-ts/storybook/page` stands in for a stories module a Node runner cannot import (one that imports Ember components), typed as that module: `StoryPage.goto` only reads its meta's `title` or `id`.
10
+
11
+ `harnessed()` takes an `afterAction` option, awaited after every interaction a harness makes, and `dom()` takes the same option for any framework that renders asynchronously.
12
+
13
+ The ESLint plugin treats `.stories.gjs` and `.stories.gts` files as stories files, so `no-raw-locator-in-test` flags raw canvas queries in Ember play functions.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [ec06bdf]
18
+ - Updated dependencies [3f53eb6]
19
+ - @harnessed-ts/core@0.7.0
20
+ - @harnessed-ts/dom@0.7.0
21
+ - @harnessed-ts/page@0.7.0
22
+
23
+ ## 0.6.0
24
+
25
+ ### Minor Changes
26
+
27
+ - e324e94: ember-test-selectors style test ids: `configure({ testIdAttribute: 'data-test-*' })` makes `testId('x')` match any element that has the attribute `data-test-x`, whatever its value, with the same scoping, strictness, `{ global: true }`, frame and absence rules as before under every driver. A plain attribute name keeps matching by value. A presence id is held to letters, digits, `-`, `_`, `.` and `:`, and anything else (or a `RegExp`) is refused rather than spliced into a selector.
28
+
29
+ `Query.boundingBox()` resolves to `{ x, y, width, height }`: the target's border box in CSS pixels, relative to the top-level viewport, offset by every frame crossed. Strict, and on an absent target it rejects after the timeout, as `text()` does. A node that draws no box reads as all zeros, and so does every node under the dom driver: jsdom computes no layout.
30
+
31
+ `Query.upload(files)` sets the files of an `<input type="file">`, or of the input a `<label>` is for, and fires `input` and `change`, replacing any earlier selection. It refuses any other element, and several files for an input that is not `multiple`. Puppeteer, WebdriverIO and TestCafe take only paths, so there each `File` is written to a temporary directory first: its name and bytes arrive, and the browser infers its type from the extension.
32
+
33
+ `Query` gains two abstract members, so a driver outside this repo implements both to keep compiling; the conformance catalog pins all three changes.
34
+
35
+ `@harnessed-ts/eslint-plugin` supports ESLint 8.57 and legacy `.eslintrc`: the peer range is now `>=8.57`, `legacy-recommended` and `legacy-strict` name the rules under `@harnessed-ts/`, and the CJS build exposes `rules` and `configs` at the top level, where eslintrc reads them.
36
+
37
+ `npx @harnessed-ts/claude install` detects ember-test-selectors attributes (in `.gts`, `.gjs` and `.hbs` files too) and writes `testIdAttribute: 'data-test-*'`.
38
+
39
+ `@harnessed-ts/storybook` writes and finds its story-ready marker under a `data-test-*` config, where `StoryPage` waits for it.
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [e324e94]
44
+ - @harnessed-ts/core@0.6.0
45
+ - @harnessed-ts/dom@0.6.0
46
+ - @harnessed-ts/page@0.6.0
47
+
48
+ ## 0.5.0
49
+
50
+ ### Minor Changes
51
+
52
+ - ad20e8c: Add `@harnessed-ts/storybook`. Spread `harnessed()` into `.storybook/preview.ts`, and a story that names its harness in `parameters.harness` gets it in `play` as `context.harness`. The harness drives the canvas through Storybook's instrumented `userEvent`, each public method is a step in the Interactions panel (secret-looking arguments are masked by parameter name in development, and by object key in a minified build too), and the harness matchers are registered on `storybook/test`'s `expect`. `StoryPage` from `@harnessed-ts/storybook/page` opens a story by URL from any runner that can navigate, with typed `args` and `globals`, `play: false` to skip the play function, and a play function's own error when it throws. React only: `@storybook/ember` builds with webpack, and Storybook's Vitest addon needs Vite.
53
+
54
+ `no-raw-locator-in-test` now treats `*.stories.*` files as tests, flagging `canvas.getBy*`, `within` from `storybook/test`, and `canvasElement.querySelector`. `npx @harnessed-ts/claude install` detects Storybook and documents the preview setup, `play` with a harness, and `StoryPage`.
55
+
56
+ ### Patch Changes
57
+
58
+ - @harnessed-ts/core@0.5.0
59
+ - @harnessed-ts/dom@0.5.0
60
+ - @harnessed-ts/page@0.5.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Gaudet, Jay Seo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # @harnessed-ts/storybook
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Harnesses in Storybook. A harness in every play function, with each of its
4
+ methods a step in the Interactions panel, and stories opened as fixtures from
5
+ any runner that can navigate.
6
+
7
+ ```ts
8
+ // .storybook/preview.ts
9
+ import { harnessed } from '@harnessed-ts/storybook'
10
+ export default { ...harnessed() } satisfies Preview
11
+ ```
12
+
13
+ ```tsx
14
+ const meta = {
15
+ title: 'Forms/Login Form',
16
+ component: LoginForm,
17
+ parameters: { harness: LoginFormHarness },
18
+ } satisfies Meta<typeof LoginForm>
19
+ export default meta
20
+
21
+ type Story = WithHarness<StoryObj<typeof meta>, LoginFormHarness>
22
+
23
+ export const WrongPassword: Story = {
24
+ play: async ({ harness }) => {
25
+ await harness.signInAs('ada@example.com', 'guess')
26
+ await expect(await harness.error()).toBe('Wrong password')
27
+ },
28
+ }
29
+ ```
30
+
31
+ ```ts
32
+ // Playwright, or any runner whose driver can navigate
33
+ import { StoryPage } from '@harnessed-ts/storybook/page'
34
+ import * as LoginStories from './LoginForm.stories'
35
+
36
+ const story = await StoryPage.goto(pw(page), LoginStories, 'WrongPassword', { play: false })
37
+ await story.harness(LoginFormHarness).signInAs('ada@example.com', 'hunter2')
38
+ ```
39
+
40
+ **Ember.** `@harnessed-ts/storybook/ember` renders stories as Ember components
41
+ under `@storybook/html-vite`, through Ember's `renderComponent`, and its
42
+ `harnessed()` settles Ember after every interaction. `emberIndexer` from
43
+ `/ember/node` indexes `.gts` stories.
44
+
45
+ ```ts
46
+ // .storybook/preview.ts
47
+ import { emberRenderer, harnessed } from '@harnessed-ts/storybook/ember'
48
+ export default { ...emberRenderer, ...harnessed() } satisfies Preview
49
+ ```
50
+
51
+ Storybook 9 or later; 10 or later, with Ember 6.8 or later, for Ember. See
52
+ [Using with Storybook](https://github.com/joegaudet/harnessed#using-with-storybook)
53
+ and [Ember stories](https://github.com/joegaudet/harnessed#ember-stories).
@@ -0,0 +1,51 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
10
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
11
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
12
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
13
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
14
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
15
+ var __runInitializers = (array, flags, self, value) => {
16
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
17
+ return value;
18
+ };
19
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
20
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
21
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
22
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
23
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
24
+ return __privateGet(this, extra);
25
+ }, set [name](x) {
26
+ return __privateSet(this, extra, x);
27
+ } }, name));
28
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
29
+ for (var i = decorators.length - 1; i >= 0; i--) {
30
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
31
+ if (k) {
32
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
33
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
34
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
35
+ }
36
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
37
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
38
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
39
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
40
+ }
41
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
42
+ };
43
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
44
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
45
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
46
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
47
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
48
+
49
+ export { __decorateElement, __decoratorStart, __runInitializers };
50
+ //# sourceMappingURL=chunk-CGZBV6BD.js.map
51
+ //# sourceMappingURL=chunk-CGZBV6BD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-CGZBV6BD.js"}
@@ -0,0 +1,7 @@
1
+ // src/ready.ts
2
+ var STORY_READY_TEST_ID = "harnessed-story-ready";
3
+ var PLAY_ERROR_ATTRIBUTE = "data-harnessed-play-error";
4
+
5
+ export { PLAY_ERROR_ATTRIBUTE, STORY_READY_TEST_ID };
6
+ //# sourceMappingURL=chunk-EBSULTWQ.js.map
7
+ //# sourceMappingURL=chunk-EBSULTWQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ready.ts"],"names":[],"mappings":";AAMO,IAAM,mBAAA,GAAsB;AAG5B,IAAM,oBAAA,GAAuB","file":"chunk-EBSULTWQ.js","sourcesContent":["/**\n * The test id of the node `harnessed()` adds once a story has rendered and its\n * play function has finished. `StoryPage` waits for it: Storybook's own\n * \"rendered\" signal is a channel event, which a runner outside the iframe's\n * realm cannot listen for, while a node is something every driver can count.\n */\nexport const STORY_READY_TEST_ID = 'harnessed-story-ready'\n\n/** Set on that node when the play function threw, holding the error's message. */\nexport const PLAY_ERROR_ATTRIBUTE = 'data-harnessed-play-error'\n"]}
@@ -0,0 +1,134 @@
1
+ import { STORY_READY_TEST_ID, PLAY_ERROR_ATTRIBUTE } from './chunk-EBSULTWQ.js';
2
+ import { ScopedHarness, harnessMatchers, getConfig, testIdPresenceSelector } from '@harnessed-ts/core';
3
+ import { dom } from '@harnessed-ts/dom';
4
+ import { PLAY_FUNCTION_THREW_EXCEPTION } from 'storybook/internal/core-events';
5
+ import { addons } from 'storybook/preview-api';
6
+ import { expect, userEvent } from 'storybook/test';
7
+
8
+ var SECRET = /pass|secret|token|credential/i;
9
+ var SECRET_WORD = /^(key|pin|otp|auth)$/i;
10
+ var WORD_BREAK = /[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/;
11
+ var REDACTED = "\u2022\u2022\u2022";
12
+ var MAX_ARG = 40;
13
+ var AsyncFunction = (async () => {
14
+ }).constructor;
15
+ var proxies = /* @__PURE__ */ new WeakMap();
16
+ function withSteps(harness, step) {
17
+ const cached = proxies.get(harness);
18
+ if (cached !== void 0) return cached;
19
+ const className = harness.constructor.name;
20
+ const wrap = (value) => {
21
+ if (value instanceof ScopedHarness) return withSteps(value, step);
22
+ if (Array.isArray(value) && value.some((item) => item instanceof ScopedHarness)) {
23
+ return value.map(wrap);
24
+ }
25
+ return value;
26
+ };
27
+ const proxy = new Proxy(harness, {
28
+ get(target, prop) {
29
+ const value = Reflect.get(target, prop, target);
30
+ if (typeof value !== "function") return wrap(value);
31
+ if (typeof prop !== "string" || prop === "constructor" || prop.startsWith("_")) {
32
+ return value.bind(target);
33
+ }
34
+ const method = value;
35
+ if (method instanceof AsyncFunction) {
36
+ return async (...args) => {
37
+ let result;
38
+ await step(stepLabel(className, method, args, prop), async () => {
39
+ result = await method.apply(target, args);
40
+ });
41
+ return wrap(result);
42
+ };
43
+ }
44
+ return (...args) => wrap(method.apply(target, args));
45
+ },
46
+ // An accessor's setter writes a private slot only the real instance has.
47
+ set: (target, prop, value) => Reflect.set(target, prop, value, target)
48
+ });
49
+ proxies.set(harness, proxy);
50
+ return proxy;
51
+ }
52
+ function stepLabel(className, method, args, name = method.name) {
53
+ const params = parameterNames(method);
54
+ const shown = args.map(
55
+ (arg, index) => isSecret(params[index] ?? "") ? JSON.stringify(REDACTED) : formatArg(arg)
56
+ );
57
+ return `${className}.${name}(${shown.join(", ")})`;
58
+ }
59
+ function isSecret(name) {
60
+ return SECRET.test(name) || name.split(WORD_BREAK).some((word) => SECRET_WORD.test(word));
61
+ }
62
+ function formatArg(arg) {
63
+ if (typeof arg === "function") return "\u0192";
64
+ if (arg instanceof ScopedHarness) return arg.constructor.name;
65
+ if (typeof arg === "string") {
66
+ return JSON.stringify(arg.length > MAX_ARG ? `${arg.slice(0, MAX_ARG)}\u2026` : arg);
67
+ }
68
+ const json = JSON.stringify(arg, (key, value) => isSecret(key) ? REDACTED : value) ?? String(arg);
69
+ return json.length > MAX_ARG ? `${json.slice(0, MAX_ARG)}\u2026` : json;
70
+ }
71
+ function parameterNames(method) {
72
+ const list = /\(([^)]*)\)/.exec(Function.prototype.toString.call(method))?.[1] ?? "";
73
+ if (/[{[]/.test(list)) return [];
74
+ return list.split(",").map((param) => param.replace(/[=:].*$/s, "").trim());
75
+ }
76
+
77
+ // src/index.ts
78
+ var matchersRegistered = false;
79
+ var listening = false;
80
+ var playError;
81
+ function listenForPlayErrors() {
82
+ if (listening || !addons.hasChannel()) return;
83
+ addons.getChannel().on(PLAY_FUNCTION_THREW_EXCEPTION, (error) => {
84
+ playError = error?.message ?? "the play function threw";
85
+ });
86
+ listening = true;
87
+ }
88
+ function harnessed(options = {}) {
89
+ if (!matchersRegistered) {
90
+ expect.extend(harnessMatchers);
91
+ matchersRegistered = true;
92
+ }
93
+ return {
94
+ async beforeEach(context) {
95
+ readyMarker(context.canvasElement.ownerDocument)?.remove();
96
+ listenForPlayErrors();
97
+ playError = void 0;
98
+ const HarnessClass = context.parameters.harness;
99
+ if (HarnessClass === void 0) return;
100
+ const env = dom({
101
+ // Storybook's own instance, so each keystroke shows up in the panel. It
102
+ // is only set where a clipboard exists; fall back to a plain one.
103
+ user: hasClick(context.userEvent) ? context.userEvent : userEvent.setup(),
104
+ container: context.canvasElement,
105
+ ...options.afterAction === void 0 ? {} : { afterAction: options.afterAction }
106
+ });
107
+ context.harness = withSteps(new HarnessClass(env), context.step);
108
+ },
109
+ async afterEach(context) {
110
+ const document = context.canvasElement.ownerDocument;
111
+ if (readyMarker(document) !== null) return;
112
+ const marker = document.createElement("div");
113
+ const attribute = getConfig().testIdAttribute;
114
+ if (attribute.endsWith("*"))
115
+ marker.setAttribute(attribute.slice(0, -1) + STORY_READY_TEST_ID, "");
116
+ else marker.setAttribute(attribute, STORY_READY_TEST_ID);
117
+ marker.hidden = true;
118
+ if (playError !== void 0) marker.setAttribute(PLAY_ERROR_ATTRIBUTE, playError);
119
+ document.body.append(marker);
120
+ }
121
+ };
122
+ }
123
+ function readyMarker(document) {
124
+ const attribute = getConfig().testIdAttribute;
125
+ const presence = testIdPresenceSelector(attribute, STORY_READY_TEST_ID);
126
+ return document.querySelector(presence ?? `[${attribute}="${STORY_READY_TEST_ID}"]`);
127
+ }
128
+ function hasClick(value) {
129
+ return typeof value?.click === "function";
130
+ }
131
+
132
+ export { harnessed, stepLabel, withSteps };
133
+ //# sourceMappingURL=chunk-XYTGUTWH.js.map
134
+ //# sourceMappingURL=chunk-XYTGUTWH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/steps.ts","../src/index.ts"],"names":[],"mappings":"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","file":"chunk-XYTGUTWH.js","sourcesContent":["import { ScopedHarness } from '@harnessed-ts/core'\n\n/** Storybook's `context.step`, narrowed to what the proxy needs. */\nexport type StepRunner = (label: string, play: () => Promise<void>) => Promise<unknown> | unknown\n\nconst SECRET = /pass|secret|token|credential/i\n// Short names match whole words only, so `author`, `keyboard` and `typing` stay visible.\nconst SECRET_WORD = /^(key|pin|otp|auth)$/i\n// camelCase, PascalCase acronyms (`APIKey`) and snake/kebab-case word breaks.\nconst WORD_BREAK = /[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/\nconst REDACTED = '•••'\nconst MAX_ARG = 40\n\nconst AsyncFunction = (async () => {}).constructor\n\nconst proxies = new WeakMap<object, unknown>()\n\n/**\n * The harness, with every public async method run inside `step()` — so the\n * Interactions panel reads `LoginFormHarness.signInAs(\"ada@…\", \"•••\")` with the\n * raw clicks and keystrokes nested under it, rather than a flat list of events.\n *\n * Methods run against the real instance, not the proxy: a harness calling its\n * own methods does not log a step per call, and decorated `accessor` fields\n * (backed by private slots a proxy cannot reach) keep working. Sync methods stay\n * sync; a harness they return — `nth()`, a child — comes back wrapped too.\n *\n * shortcut: async is detected by `AsyncFunction`, so a plain method that returns\n * a promise, or one down-levelled to a generator, logs no step. Upgrade if a\n * harness or a build ever needs that.\n */\nexport function withSteps<T extends ScopedHarness>(harness: T, step: StepRunner): T {\n const cached = proxies.get(harness)\n if (cached !== undefined) return cached as T\n const className = harness.constructor.name\n\n const wrap = (value: unknown): unknown => {\n if (value instanceof ScopedHarness) return withSteps(value, step)\n if (Array.isArray(value) && value.some(item => item instanceof ScopedHarness)) {\n return value.map(wrap)\n }\n return value\n }\n\n const proxy = new Proxy(harness, {\n get(target, prop) {\n const value: unknown = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return wrap(value)\n if (typeof prop !== 'string' || prop === 'constructor' || prop.startsWith('_')) {\n return (value as (...args: unknown[]) => unknown).bind(target)\n }\n const method = value as (...args: unknown[]) => unknown\n if (method instanceof AsyncFunction) {\n return async (...args: unknown[]) => {\n let result: unknown\n await step(stepLabel(className, method, args, prop), async () => {\n result = await method.apply(target, args)\n })\n return wrap(result)\n }\n }\n return (...args: unknown[]) => wrap(method.apply(target, args))\n },\n // An accessor's setter writes a private slot only the real instance has.\n set: (target, prop, value) => Reflect.set(target, prop, value, target),\n })\n proxies.set(harness, proxy)\n return proxy\n}\n\n/**\n * `Harness.method(args…)`, each argument shortened, and any argument whose\n * parameter name — or object key — looks like a secret replaced with `•••`.\n *\n * Names are read from the source, so a minified `storybook build` shows mangled\n * class names and masks only object keys, not positional arguments.\n */\nexport function stepLabel(\n className: string,\n method: (...args: never[]) => unknown,\n args: readonly unknown[],\n name: string = method.name,\n): string {\n const params = parameterNames(method)\n const shown = args.map((arg, index) =>\n isSecret(params[index] ?? '') ? JSON.stringify(REDACTED) : formatArg(arg),\n )\n return `${className}.${name}(${shown.join(', ')})`\n}\n\nfunction isSecret(name: string): boolean {\n return SECRET.test(name) || name.split(WORD_BREAK).some(word => SECRET_WORD.test(word))\n}\n\nfunction formatArg(arg: unknown): string {\n if (typeof arg === 'function') return 'ƒ'\n if (arg instanceof ScopedHarness) return arg.constructor.name\n if (typeof arg === 'string') {\n return JSON.stringify(arg.length > MAX_ARG ? `${arg.slice(0, MAX_ARG)}…` : arg)\n }\n const json =\n JSON.stringify(arg, (key, value: unknown) => (isSecret(key) ? REDACTED : value)) ?? String(arg)\n return json.length > MAX_ARG ? `${json.slice(0, MAX_ARG)}…` : json\n}\n\n/** Plain parameter names from the source; none when they are destructured. */\nfunction parameterNames(method: (...args: never[]) => unknown): string[] {\n const list = /\\(([^)]*)\\)/.exec(Function.prototype.toString.call(method))?.[1] ?? ''\n if (/[{[]/.test(list)) return []\n return list.split(',').map(param => param.replace(/[=:].*$/s, '').trim())\n}\n","import { getConfig, harnessMatchers, testIdPresenceSelector } from '@harnessed-ts/core'\nimport type { Assertable, ScopedHarness, ScopedHarnessConstructor } from '@harnessed-ts/core'\nimport { dom } from '@harnessed-ts/dom'\nimport type { UserEvent } from '@testing-library/user-event'\nimport { PLAY_FUNCTION_THREW_EXCEPTION } from 'storybook/internal/core-events'\nimport { addons } from 'storybook/preview-api'\nimport { expect, userEvent } from 'storybook/test'\nimport { PLAY_ERROR_ATTRIBUTE, STORY_READY_TEST_ID } from './ready'\nimport { withSteps } from './steps'\nimport type { StepRunner } from './steps'\n\nexport { PLAY_ERROR_ATTRIBUTE, STORY_READY_TEST_ID } from './ready'\nexport { stepLabel, withSteps } from './steps'\nexport type { StepRunner } from './steps'\n\n/** What `harnessed()` reads from, and adds to, Storybook's story context. */\nexport interface HarnessedContext {\n canvasElement: HTMLElement\n parameters: { harness?: ScopedHarnessConstructor }\n userEvent?: unknown\n step: StepRunner\n harness?: ScopedHarness\n}\n\nexport interface HarnessedOptions {\n /**\n * Awaited after every interaction a harness makes, before the next step reads\n * the canvas. A renderer that updates the DOM asynchronously settles here;\n * `@harnessed-ts/storybook/ember` passes Ember's.\n */\n afterAction?: () => Promise<void>\n}\n\nlet matchersRegistered = false\nlet listening = false\n/** The message of the current story's play error, if it threw. */\nlet playError: string | undefined\n\n/**\n * Storybook catches a play function's error, reports it on its channel, and\n * then finishes the render (running `afterEach`) as if it had passed, so the\n * error has to be caught here to reach the ready mark.\n */\nfunction listenForPlayErrors(): void {\n if (listening || !addons.hasChannel()) return\n addons.getChannel().on(PLAY_FUNCTION_THREW_EXCEPTION, (error?: { message?: string }) => {\n playError = error?.message ?? 'the play function threw'\n })\n listening = true\n}\n\n/**\n * Preview annotations that put a harness in every play function. Spread them\n * into `.storybook/preview.ts`:\n *\n * ```ts\n * import { harnessed } from '@harnessed-ts/storybook'\n * export default { ...harnessed() } satisfies Preview\n * ```\n *\n * A story (or its meta) names its harness in `parameters.harness`, and `play`\n * receives it as `context.harness`, driving the canvas through Storybook's\n * instrumented `userEvent` with each method logged as a step. The harness\n * matchers are added to `storybook/test`'s `expect`.\n */\nexport function harnessed(options: HarnessedOptions = {}): {\n beforeEach: (context: HarnessedContext) => Promise<void>\n afterEach: (context: HarnessedContext) => Promise<void>\n} {\n if (!matchersRegistered) {\n expect.extend(harnessMatchers)\n matchersRegistered = true\n }\n return {\n async beforeEach(context: HarnessedContext) {\n readyMarker(context.canvasElement.ownerDocument)?.remove()\n listenForPlayErrors()\n playError = undefined\n const HarnessClass = context.parameters.harness\n if (HarnessClass === undefined) return\n const env = dom({\n // Storybook's own instance, so each keystroke shows up in the panel. It\n // is only set where a clipboard exists; fall back to a plain one.\n user: (hasClick(context.userEvent) ? context.userEvent : userEvent.setup()) as UserEvent,\n container: context.canvasElement,\n ...(options.afterAction === undefined ? {} : { afterAction: options.afterAction }),\n })\n context.harness = withSteps(new HarnessClass(env), context.step)\n },\n async afterEach(context: HarnessedContext) {\n const document = context.canvasElement.ownerDocument\n if (readyMarker(document) !== null) return\n const marker = document.createElement('div')\n const attribute = getConfig().testIdAttribute\n // Under a presence pattern the id is part of the name: `data-test-<id>`.\n if (attribute.endsWith('*'))\n marker.setAttribute(attribute.slice(0, -1) + STORY_READY_TEST_ID, '')\n else marker.setAttribute(attribute, STORY_READY_TEST_ID)\n marker.hidden = true\n if (playError !== undefined) marker.setAttribute(PLAY_ERROR_ATTRIBUTE, playError)\n document.body.append(marker)\n },\n }\n}\n\n/** Found the way `testId(STORY_READY_TEST_ID)` finds it, which is what `StoryPage` waits on. */\nfunction readyMarker(document: Document): Element | null {\n const attribute = getConfig().testIdAttribute\n const presence = testIdPresenceSelector(attribute, STORY_READY_TEST_ID)\n return document.querySelector(presence ?? `[${attribute}=\"${STORY_READY_TEST_ID}\"]`)\n}\n\nfunction hasClick(value: unknown): boolean {\n return typeof (value as { click?: unknown } | undefined)?.click === 'function'\n}\n\n/**\n * A story type whose `play` receives `harness` typed as `H`.\n *\n * ```ts\n * type Story = WithHarness<StoryObj<typeof meta>, LoginFormHarness>\n * ```\n */\nexport type WithHarness<S, H extends ScopedHarness> = Omit<S, 'play'> & {\n play?: S extends { play?: infer P }\n ? P extends (context: infer C) => infer R\n ? (context: C & { harness: H }) => R\n : never\n : never\n}\n\ndeclare module 'storybook/test' {\n // The type parameter must keep Storybook's own name. Gating on `T` makes calling\n // these on something that is not a target or a harness a type error.\n interface Assertion<T> {\n toBeSelected(): T extends Assertable ? Promise<void> : never\n toBeAbsent(): T extends Assertable ? Promise<void> : never\n toReadAs(expected: string | RegExp): T extends Assertable ? Promise<void> : never\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ var promises = require('fs/promises');
4
+ var contentTag = require('content-tag');
5
+ var csfTools = require('storybook/internal/csf-tools');
6
+
7
+ // src/ember-node.ts
8
+ var emberIndexer = {
9
+ test: /\.stories\.g[jt]s$/,
10
+ async createIndex(fileName, options) {
11
+ const source = await promises.readFile(fileName, "utf-8");
12
+ const { code } = new contentTag.Preprocessor().process(source, { filename: fileName });
13
+ return csfTools.loadCsf(code, { ...options, fileName }).parse().indexInputs;
14
+ }
15
+ };
16
+
17
+ exports.emberIndexer = emberIndexer;
18
+ //# sourceMappingURL=ember-node.cjs.map
19
+ //# sourceMappingURL=ember-node.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ember-node.ts"],"names":["readFile","Preprocessor","loadCsf"],"mappings":";;;;;;;AAoBO,IAAM,YAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,oBAAA;AAAA,EACN,MAAM,WAAA,CAAY,QAAA,EAAU,OAAA,EAAS;AACnC,IAAA,MAAM,MAAA,GAAS,MAAMA,iBAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAC/C,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,IAAIC,uBAAA,EAAa,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,QAAA,EAAU,QAAA,EAAU,CAAA;AAC1E,IAAA,OAAOC,gBAAA,CAAQ,MAAM,EAAE,GAAG,SAAS,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM,CAAE,WAAA;AAAA,EACzD;AACF","file":"ember-node.cjs","sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { Preprocessor } from 'content-tag'\nimport { loadCsf } from 'storybook/internal/csf-tools'\nimport type { Indexer } from 'storybook/internal/types'\n\n/**\n * Indexes `.stories.gjs` and `.stories.gts` files, which Storybook's own CSF\n * indexer cannot parse: their `<template>` tags are not JavaScript. Each file is\n * read with the tags compiled away, as `content-tag` does for Embroider, and\n * indexed as ordinary CSF.\n *\n * ```ts\n * // .storybook/main.ts\n * import { emberIndexer } from '@harnessed-ts/storybook/ember/node'\n * export default {\n * stories: ['../stories/*.stories.@(ts|gts)'],\n * experimental_indexers: existing => [emberIndexer, ...(existing ?? [])],\n * }\n * ```\n */\nexport const emberIndexer: Indexer = {\n test: /\\.stories\\.g[jt]s$/,\n async createIndex(fileName, options) {\n const source = await readFile(fileName, 'utf-8')\n const { code } = new Preprocessor().process(source, { filename: fileName })\n return loadCsf(code, { ...options, fileName }).parse().indexInputs\n },\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { Indexer } from 'storybook/internal/types';
2
+ /**
3
+ * Indexes `.stories.gjs` and `.stories.gts` files, which Storybook's own CSF
4
+ * indexer cannot parse: their `<template>` tags are not JavaScript. Each file is
5
+ * read with the tags compiled away, as `content-tag` does for Embroider, and
6
+ * indexed as ordinary CSF.
7
+ *
8
+ * ```ts
9
+ * // .storybook/main.ts
10
+ * import { emberIndexer } from '@harnessed-ts/storybook/ember/node'
11
+ * export default {
12
+ * stories: ['../stories/*.stories.@(ts|gts)'],
13
+ * experimental_indexers: existing => [emberIndexer, ...(existing ?? [])],
14
+ * }
15
+ * ```
16
+ */
17
+ export declare const emberIndexer: Indexer;
18
+ //# sourceMappingURL=ember-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ember-node.d.ts","sourceRoot":"","sources":["../src/ember-node.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAEvD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,EAAE,OAO1B,CAAA"}
@@ -0,0 +1,17 @@
1
+ import './chunk-CGZBV6BD.js';
2
+ import { readFile } from 'fs/promises';
3
+ import { Preprocessor } from 'content-tag';
4
+ import { loadCsf } from 'storybook/internal/csf-tools';
5
+
6
+ var emberIndexer = {
7
+ test: /\.stories\.g[jt]s$/,
8
+ async createIndex(fileName, options) {
9
+ const source = await readFile(fileName, "utf-8");
10
+ const { code } = new Preprocessor().process(source, { filename: fileName });
11
+ return loadCsf(code, { ...options, fileName }).parse().indexInputs;
12
+ }
13
+ };
14
+
15
+ export { emberIndexer };
16
+ //# sourceMappingURL=ember-node.js.map
17
+ //# sourceMappingURL=ember-node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ember-node.ts"],"names":[],"mappings":";;;;;AAoBO,IAAM,YAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,oBAAA;AAAA,EACN,MAAM,WAAA,CAAY,QAAA,EAAU,OAAA,EAAS;AACnC,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAC/C,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,IAAI,YAAA,EAAa,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,QAAA,EAAU,QAAA,EAAU,CAAA;AAC1E,IAAA,OAAO,OAAA,CAAQ,MAAM,EAAE,GAAG,SAAS,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM,CAAE,WAAA;AAAA,EACzD;AACF","file":"ember-node.js","sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { Preprocessor } from 'content-tag'\nimport { loadCsf } from 'storybook/internal/csf-tools'\nimport type { Indexer } from 'storybook/internal/types'\n\n/**\n * Indexes `.stories.gjs` and `.stories.gts` files, which Storybook's own CSF\n * indexer cannot parse: their `<template>` tags are not JavaScript. Each file is\n * read with the tags compiled away, as `content-tag` does for Embroider, and\n * indexed as ordinary CSF.\n *\n * ```ts\n * // .storybook/main.ts\n * import { emberIndexer } from '@harnessed-ts/storybook/ember/node'\n * export default {\n * stories: ['../stories/*.stories.@(ts|gts)'],\n * experimental_indexers: existing => [emberIndexer, ...(existing ?? [])],\n * }\n * ```\n */\nexport const emberIndexer: Indexer = {\n test: /\\.stories\\.g[jt]s$/,\n async createIndex(fileName, options) {\n const source = await readFile(fileName, 'utf-8')\n const { code } = new Preprocessor().process(source, { filename: fileName })\n return loadCsf(code, { ...options, fileName }).parse().indexInputs\n },\n}\n"]}