UNPKG

storybook-addon-test-codegen

Version:

Addon for Storybook that generates test code for your stories.

1,158 lines (1,143 loc) 46.7 kB
import React2, { useEffect, useMemo, useRef, useState, useLayoutEffect, useCallback } from 'react'; import { addons, types, useAddonState, useChannel, useStorybookApi, useGlobals, experimental_requestResponse } from 'storybook/manager-api'; import { IconButton, Button, Form, AddonPanel, Spaced, Badge, Bar, EmptyTabContent, WithTooltip, TooltipNote, SyntaxHighlighter } from 'storybook/internal/components'; import { AlertIcon, CheckIcon, SyncIcon, CrossIcon, AddIcon, DeleteIcon, CopyIcon } from '@storybook/icons'; import { styled } from 'storybook/theming'; // src/manager.tsx function c(e2, u2, c2) { var i2 = this, a2 = useRef(null), o = useRef(0), f = useRef(null), l = useRef([]), v = useRef(), m = useRef(), d = useRef(e2), g = useRef(true); d.current = e2; var p = "undefined" != typeof window, w = !u2 && 0 !== u2 && p; if ("function" != typeof e2) throw new TypeError("Expected a function"); u2 = +u2 || 0; var s = !!(c2 = c2 || {}).leading, x = !("trailing" in c2) || !!c2.trailing, h = "maxWait" in c2, y = "debounceOnServer" in c2 && !!c2.debounceOnServer, F = h ? Math.max(+c2.maxWait || 0, u2) : null; useEffect(function() { return g.current = true, function() { g.current = false; }; }, []); var A = useMemo(function() { var r2 = function(r3) { var n3 = l.current, t3 = v.current; return l.current = v.current = null, o.current = r3, m.current = d.current.apply(t3, n3); }, n2 = function(r3, n3) { w && cancelAnimationFrame(f.current), f.current = w ? requestAnimationFrame(r3) : setTimeout(r3, n3); }, t2 = function(r3) { if (!g.current) return false; var n3 = r3 - a2.current; return !a2.current || n3 >= u2 || n3 < 0 || h && r3 - o.current >= F; }, e3 = function(n3) { return f.current = null, x && l.current ? r2(n3) : (l.current = v.current = null, m.current); }, c3 = function r3() { var c4 = Date.now(); if (t2(c4)) return e3(c4); if (g.current) { var i3 = u2 - (c4 - a2.current), f2 = h ? Math.min(i3, F - (c4 - o.current)) : i3; n2(r3, f2); } }, A2 = function() { if (p || y) { var e4 = Date.now(), d2 = t2(e4); if (l.current = [].slice.call(arguments), v.current = i2, a2.current = e4, d2) { if (!f.current && g.current) return o.current = a2.current, n2(c3, u2), s ? r2(a2.current) : m.current; if (h) return n2(c3, u2), r2(a2.current); } return f.current || n2(c3, u2), m.current; } }; return A2.cancel = function() { f.current && (w ? cancelAnimationFrame(f.current) : clearTimeout(f.current)), o.current = 0, l.current = a2.current = v.current = f.current = null; }, A2.isPending = function() { return !!f.current; }, A2.flush = function() { return f.current ? e3(Date.now()) : m.current; }, A2; }, [s, h, u2, F, x, w, p, y]); return A; } function i(r2, n2) { return r2 === n2; } function a(n2, t2, a2) { var o = i, f = useRef(n2), l = useState({})[1], v = c(useCallback(function(r2) { f.current = r2, l({}); }, [l]), t2, a2), m = useRef(n2); return o(m.current, n2) || (v(n2), m.current = n2), [f.current, v]; } // node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/esm/typeof.js function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o2) { return typeof o2; } : function(o2) { return o2 && "function" == typeof Symbol && o2.constructor === Symbol && o2 !== Symbol.prototype ? "symbol" : typeof o2; }, _typeof(o); } // node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/esm/toPrimitive.js function toPrimitive(t2, r2) { if ("object" != _typeof(t2) || !t2) return t2; var e2 = t2[Symbol.toPrimitive]; if (void 0 !== e2) { var i2 = e2.call(t2, r2); if ("object" != _typeof(i2)) return i2; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r2 ? String : Number)(t2); } // node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/esm/toPropertyKey.js function toPropertyKey(t2) { var i2 = toPrimitive(t2, "string"); return "symbol" == _typeof(i2) ? i2 : i2 + ""; } // node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/esm/defineProperty.js function _defineProperty(e2, r2, t2) { return (r2 = toPropertyKey(r2)) in e2 ? Object.defineProperty(e2, r2, { value: t2, enumerable: true, configurable: true, writable: true }) : e2[r2] = t2, e2; } // node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/esm/extends.js function _extends() { return _extends = Object.assign ? Object.assign.bind() : function(n2) { for (var e2 = 1; e2 < arguments.length; e2++) { var t2 = arguments[e2]; for (var r2 in t2) ({}).hasOwnProperty.call(t2, r2) && (n2[r2] = t2[r2]); } return n2; }, _extends.apply(null, arguments); } function ownKeys(e2, r2) { var t2 = Object.keys(e2); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e2); r2 && (o = o.filter(function(r3) { return Object.getOwnPropertyDescriptor(e2, r3).enumerable; })), t2.push.apply(t2, o); } return t2; } function _objectSpread(e2) { for (var r2 = 1; r2 < arguments.length; r2++) { var t2 = null != arguments[r2] ? arguments[r2] : {}; r2 % 2 ? ownKeys(Object(t2), true).forEach(function(r3) { _defineProperty(e2, r3, t2[r3]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e2, Object.getOwnPropertyDescriptors(t2)) : ownKeys(Object(t2)).forEach(function(r3) { Object.defineProperty(e2, r3, Object.getOwnPropertyDescriptor(t2, r3)); }); } return e2; } function powerSetPermutations(arr) { var arrLength = arr.length; if (arrLength === 0 || arrLength === 1) return arr; if (arrLength === 2) { return [arr[0], arr[1], "".concat(arr[0], ".").concat(arr[1]), "".concat(arr[1], ".").concat(arr[0])]; } if (arrLength === 3) { return [arr[0], arr[1], arr[2], "".concat(arr[0], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[0]), "".concat(arr[1], ".").concat(arr[2]), "".concat(arr[2], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[1], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[2], ".").concat(arr[1]), "".concat(arr[1], ".").concat(arr[0], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[2], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[0], ".").concat(arr[1]), "".concat(arr[2], ".").concat(arr[1], ".").concat(arr[0])]; } if (arrLength >= 4) { return [arr[0], arr[1], arr[2], arr[3], "".concat(arr[0], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[3]), "".concat(arr[1], ".").concat(arr[0]), "".concat(arr[1], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[3]), "".concat(arr[2], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[1]), "".concat(arr[2], ".").concat(arr[3]), "".concat(arr[3], ".").concat(arr[0]), "".concat(arr[3], ".").concat(arr[1]), "".concat(arr[3], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[1], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[1], ".").concat(arr[3]), "".concat(arr[0], ".").concat(arr[2], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[2], ".").concat(arr[3]), "".concat(arr[0], ".").concat(arr[3], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[3], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[0], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[0], ".").concat(arr[3]), "".concat(arr[1], ".").concat(arr[2], ".").concat(arr[0]), "".concat(arr[1], ".").concat(arr[2], ".").concat(arr[3]), "".concat(arr[1], ".").concat(arr[3], ".").concat(arr[0]), "".concat(arr[1], ".").concat(arr[3], ".").concat(arr[2]), "".concat(arr[2], ".").concat(arr[0], ".").concat(arr[1]), "".concat(arr[2], ".").concat(arr[0], ".").concat(arr[3]), "".concat(arr[2], ".").concat(arr[1], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[1], ".").concat(arr[3]), "".concat(arr[2], ".").concat(arr[3], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[3], ".").concat(arr[1]), "".concat(arr[3], ".").concat(arr[0], ".").concat(arr[1]), "".concat(arr[3], ".").concat(arr[0], ".").concat(arr[2]), "".concat(arr[3], ".").concat(arr[1], ".").concat(arr[0]), "".concat(arr[3], ".").concat(arr[1], ".").concat(arr[2]), "".concat(arr[3], ".").concat(arr[2], ".").concat(arr[0]), "".concat(arr[3], ".").concat(arr[2], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[1], ".").concat(arr[2], ".").concat(arr[3]), "".concat(arr[0], ".").concat(arr[1], ".").concat(arr[3], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[2], ".").concat(arr[1], ".").concat(arr[3]), "".concat(arr[0], ".").concat(arr[2], ".").concat(arr[3], ".").concat(arr[1]), "".concat(arr[0], ".").concat(arr[3], ".").concat(arr[1], ".").concat(arr[2]), "".concat(arr[0], ".").concat(arr[3], ".").concat(arr[2], ".").concat(arr[1]), "".concat(arr[1], ".").concat(arr[0], ".").concat(arr[2], ".").concat(arr[3]), "".concat(arr[1], ".").concat(arr[0], ".").concat(arr[3], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[2], ".").concat(arr[0], ".").concat(arr[3]), "".concat(arr[1], ".").concat(arr[2], ".").concat(arr[3], ".").concat(arr[0]), "".concat(arr[1], ".").concat(arr[3], ".").concat(arr[0], ".").concat(arr[2]), "".concat(arr[1], ".").concat(arr[3], ".").concat(arr[2], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[0], ".").concat(arr[1], ".").concat(arr[3]), "".concat(arr[2], ".").concat(arr[0], ".").concat(arr[3], ".").concat(arr[1]), "".concat(arr[2], ".").concat(arr[1], ".").concat(arr[0], ".").concat(arr[3]), "".concat(arr[2], ".").concat(arr[1], ".").concat(arr[3], ".").concat(arr[0]), "".concat(arr[2], ".").concat(arr[3], ".").concat(arr[0], ".").concat(arr[1]), "".concat(arr[2], ".").concat(arr[3], ".").concat(arr[1], ".").concat(arr[0]), "".concat(arr[3], ".").concat(arr[0], ".").concat(arr[1], ".").concat(arr[2]), "".concat(arr[3], ".").concat(arr[0], ".").concat(arr[2], ".").concat(arr[1]), "".concat(arr[3], ".").concat(arr[1], ".").concat(arr[0], ".").concat(arr[2]), "".concat(arr[3], ".").concat(arr[1], ".").concat(arr[2], ".").concat(arr[0]), "".concat(arr[3], ".").concat(arr[2], ".").concat(arr[0], ".").concat(arr[1]), "".concat(arr[3], ".").concat(arr[2], ".").concat(arr[1], ".").concat(arr[0])]; } } var classNameCombinations = {}; function getClassNameCombinations(classNames) { if (classNames.length === 0 || classNames.length === 1) return classNames; var key = classNames.join("."); if (!classNameCombinations[key]) { classNameCombinations[key] = powerSetPermutations(classNames); } return classNameCombinations[key]; } function createStyleObject(classNames) { var elementStyle = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; var stylesheet = arguments.length > 2 ? arguments[2] : void 0; var nonTokenClassNames = classNames.filter(function(className) { return className !== "token"; }); var classNamesCombinations = getClassNameCombinations(nonTokenClassNames); return classNamesCombinations.reduce(function(styleObject, className) { return _objectSpread(_objectSpread({}, styleObject), stylesheet[className]); }, elementStyle); } function createClassNameString(classNames) { return classNames.join(" "); } function createChildren(stylesheet, useInlineStyles) { var childrenCount = 0; return function(children) { childrenCount += 1; return children.map(function(child, i2) { return createElement({ node: child, stylesheet, useInlineStyles, key: "code-segment-".concat(childrenCount, "-").concat(i2) }); }); }; } function createElement(_ref) { var node = _ref.node, stylesheet = _ref.stylesheet, _ref$style = _ref.style, style = _ref$style === void 0 ? {} : _ref$style, useInlineStyles = _ref.useInlineStyles, key = _ref.key; var properties = node.properties, type = node.type, TagName = node.tagName, value = node.value; if (type === "text") { return value; } else if (TagName) { var childrenCreator = createChildren(stylesheet, useInlineStyles); var props; if (!useInlineStyles) { props = _objectSpread(_objectSpread({}, properties), {}, { className: createClassNameString(properties.className) }); } else { var allStylesheetSelectors = Object.keys(stylesheet).reduce(function(classes, selector) { selector.split(".").forEach(function(className2) { if (!classes.includes(className2)) classes.push(className2); }); return classes; }, []); var startingClassName = properties.className && properties.className.includes("token") ? ["token"] : []; var className = properties.className && startingClassName.concat(properties.className.filter(function(className2) { return !allStylesheetSelectors.includes(className2); })); props = _objectSpread(_objectSpread({}, properties), {}, { className: createClassNameString(className) || void 0, style: createStyleObject(properties.className, Object.assign({}, properties.style, style), stylesheet) }); } var children = childrenCreator(node.children); return /* @__PURE__ */ React2.createElement(TagName, _extends({ key }, props), children); } } var Container = styled.div(({ theme }) => ({ display: "flex", flexDirection: "column", border: `1px solid ${theme.appBorderColor}` })); var Label = styled.div(({ theme, isSticky }) => ({ fontSize: theme.typography.size.s1, fontWeight: "bold", marginBottom: 4, padding: "6px 7px", background: theme.barBg, borderBottom: `1px solid ${theme.appBorderColor}`, position: "relative", ...isSticky ? { position: "sticky", top: 0, zIndex: 1 } : {} })); var Code = styled.div({ padding: "6px 7px 10px", height: "100%", tabSize: "8px", pre: { margin: 0, borderRadius: 0 } }); var CopyIconContainer = styled.div({ position: "absolute", top: "50%", transform: "translateY(-50%)", right: 0, zIndex: 1 }); var WarningButton = styled(IconButton)(({ theme }) => ({ padding: 0, width: 20, height: 20, marginRight: 4 })); var WarningIcon = styled(AlertIcon)(({ theme }) => ({ color: theme.color.warning, width: 12, height: 12 })); function CodeBlock({ name, codeLines, isSticky }) { const [isCopied, setIsCopied] = useState(false); const [copiedTimeoutId, setCopiedTimeoutId] = useState(null); const splitCodeLines = codeLines.flatMap( (line) => line.text.split("\n").map((text, index) => ({ text, warning: line.warning && index === 0 ? line.warning : void 0 })) ); const code = splitCodeLines.map(({ text }) => text).join("\n"); return /* @__PURE__ */ React2.createElement(Container, null, /* @__PURE__ */ React2.createElement(Label, { isSticky }, name, /* @__PURE__ */ React2.createElement(CopyIconContainer, null, /* @__PURE__ */ React2.createElement( WithTooltip, { placement: "top", as: "div", hasChrome: false, trigger: "hover", tooltip: /* @__PURE__ */ React2.createElement(TooltipNote, { note: "Copy" }) }, /* @__PURE__ */ React2.createElement( IconButton, { onClick: () => navigator.clipboard.writeText(code).then(() => { setIsCopied(true); if (copiedTimeoutId) { clearTimeout(copiedTimeoutId); } setCopiedTimeoutId( setTimeout(() => { setIsCopied(false); setCopiedTimeoutId(null); }, 2e3) ); }) }, isCopied ? /* @__PURE__ */ React2.createElement(CheckIcon, { size: 16 }) : /* @__PURE__ */ React2.createElement(CopyIcon, { size: 16 }) ) ))), /* @__PURE__ */ React2.createElement(Code, null, /* @__PURE__ */ React2.createElement( SyntaxHighlighter, { language: "typescript", renderer: ({ rows, useInlineStyles }) => rows.map((row, i2) => { const warning = splitCodeLines[i2]?.warning; if (warning) { const warningIcon = { type: "element", tagName: WarningIndicator, properties: { className: [], warning }, children: [] }; row.children.splice(0, 1, warningIcon); } else if (i2 > 0 && i2 < rows.length - 1) { if (row.children[0].properties) { row.children[0].properties.style = { marginRight: "16px" }; } } return createElement({ node: row, stylesheet: {}, useInlineStyles, key: `code-segement${i2}` }); }) }, code ))); } var Title = styled.div(({ theme }) => ({ fontWeight: theme.typography.weight.bold, fontSize: theme.typography.size.s2, marginBottom: 12 })); var Desc = styled.div(({ theme }) => ({ color: theme.color.defaultText })); var Message = styled.div(({ theme }) => ({ color: theme.color.defaultText, lineHeight: "18px", padding: 15, width: 350 })); var warningToRender = { ROLE_WITHOUT_NAME: { title: "Role without name", description: /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("strong", null, "getByRole"), " without name can match any button, making tests unreliable."), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, "To fix, add ", /* @__PURE__ */ React2.createElement("strong", null, "visible text"), " or ", /* @__PURE__ */ React2.createElement("strong", null, "aria-label"), " to your element:"), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, /* @__PURE__ */ React2.createElement("div", null, "\u274C ", /* @__PURE__ */ React2.createElement("code", null, "<button />")), /* @__PURE__ */ React2.createElement("div", null, "\u2705 ", /* @__PURE__ */ React2.createElement("code", null, "<button>Submit</button>")), /* @__PURE__ */ React2.createElement("div", null, "\u2705 ", /* @__PURE__ */ React2.createElement("code", null, '<button aria-label="Submit">Icon</button>')))) }, QUERY_SELECTOR: { title: "Query selector usage", description: /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("strong", null, "querySelector"), " is fragile and breaks when HTML structure changes."), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, "To fix, use ", /* @__PURE__ */ React2.createElement("strong", null, "semantic HTML"), " or ", /* @__PURE__ */ React2.createElement("strong", null, "aria-label"), " ", "to make the element accessible:"), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, /* @__PURE__ */ React2.createElement("div", null, "\u274C ", /* @__PURE__ */ React2.createElement("code", null, '<div class="submit-18372">Submit</div>')), /* @__PURE__ */ React2.createElement("div", null, "\u2705 ", /* @__PURE__ */ React2.createElement("code", null, "<button>Submit</button>")), /* @__PURE__ */ React2.createElement("div", null, "\u2705 ", /* @__PURE__ */ React2.createElement("code", null, '<div aria-label="Submit">Icon</div>')))) }, TEST_ID: { title: "Test ID usage", description: /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("strong", null, "data-testid"), " elements are not accessible and do not resemble how your software is used."), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, "To improve, use ", /* @__PURE__ */ React2.createElement("strong", null, "semantic HTML"), " or", " ", /* @__PURE__ */ React2.createElement("strong", null, "aria-label"), " to make the element accessible:"), /* @__PURE__ */ React2.createElement("div", { style: { margin: "8px 0" } }, /* @__PURE__ */ React2.createElement("div", null, "\u274C ", /* @__PURE__ */ React2.createElement("code", null, '<div data-testid="submit">Submit</div>')), /* @__PURE__ */ React2.createElement("div", null, "\u2705 ", /* @__PURE__ */ React2.createElement("code", null, "<button>Submit</button>")))) } }; function WarningIndicator({ warning }) { const { title, description } = warningToRender[warning]; return /* @__PURE__ */ React2.createElement( WithTooltip, { trigger: "hover", tooltip: /* @__PURE__ */ React2.createElement(Message, null, /* @__PURE__ */ React2.createElement(Title, null, title), /* @__PURE__ */ React2.createElement(Desc, null, description)) }, /* @__PURE__ */ React2.createElement(WarningButton, null, /* @__PURE__ */ React2.createElement(WarningIcon, null)) ); } // node_modules/.pnpm/dequal@2.0.3/node_modules/dequal/dist/index.mjs var has = Object.prototype.hasOwnProperty; function find(iter, tar, key) { for (key of iter.keys()) { if (dequal(key, tar)) return key; } } function dequal(foo, bar) { var ctor, len, tmp; if (foo === bar) return true; if (foo && bar && (ctor = foo.constructor) === bar.constructor) { if (ctor === Date) return foo.getTime() === bar.getTime(); if (ctor === RegExp) return foo.toString() === bar.toString(); if (ctor === Array) { if ((len = foo.length) === bar.length) { while (len-- && dequal(foo[len], bar[len])) ; } return len === -1; } if (ctor === Set) { if (foo.size !== bar.size) { return false; } for (len of foo) { tmp = len; if (tmp && typeof tmp === "object") { tmp = find(bar, tmp); if (!tmp) return false; } if (!bar.has(tmp)) return false; } return true; } if (ctor === Map) { if (foo.size !== bar.size) { return false; } for (len of foo) { tmp = len[0]; if (tmp && typeof tmp === "object") { tmp = find(bar, tmp); if (!tmp) return false; } if (!dequal(len[1], bar.get(tmp))) { return false; } } return true; } if (ctor === ArrayBuffer) { foo = new Uint8Array(foo); bar = new Uint8Array(bar); } else if (ctor === DataView) { if ((len = foo.byteLength) === bar.byteLength) { while (len-- && foo.getInt8(len) === bar.getInt8(len)) ; } return len === -1; } if (ArrayBuffer.isView(foo)) { if ((len = foo.byteLength) === bar.byteLength) { while (len-- && foo[len] === bar[len]) ; } return len === -1; } if (!ctor || typeof foo === "object") { len = 0; for (ctor in foo) { if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false; if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; } return Object.keys(bar).length === len; } } return foo !== foo && bar !== bar; } // src/constants.ts var ADDON_ID = "storybook/test-codegen"; var PANEL_ID = `${ADDON_ID}/panel`; var IS_RECORDING_KEY = `${ADDON_ID}/is-recording`; var IS_ASSERTING_KEY = `${ADDON_ID}/is-asserting`; var EVENTS = { INTERACTION: `${ADDON_ID}/interaction`, SAVE_NEW_STORY_REQUEST: `${ADDON_ID}/save-new-story-request`, SAVE_NEW_STORY_RESPONSE: `${ADDON_ID}/save-new-story-response` }; var Container2 = styled.div(({ theme }) => ({ height: "100%", background: theme.background.content, display: "flex", flexDirection: "column" })); var ContentWrapper = styled.div({ flex: 1, overflow: "auto" }); var SubnavWrapper = styled.div(({ theme }) => ({ background: theme.background.app, borderBottom: `1px solid ${theme.appBorderColor}`, position: "sticky", top: 0, zIndex: 2 })); var StyledSubnav = styled.nav(({ theme }) => ({ height: 40, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 15px" })); var StyledButton = styled(Button)(({ theme }) => ({ position: "relative", borderRadius: 4, padding: "6px 8px", color: theme.textMutedColor, "&:not(:disabled)": { "&:hover,&:focus-visible": { color: theme.color.secondary } }, marginBottom: 1, lineHeight: "12px" })); var StyledButtonBigContent = styled.div( ({ isHidden }) => ({ display: "flex", alignItems: "center", gap: 6, opacity: isHidden ? 0 : 1 }) ); var StyledButtonSmallContent = styled(StyledButtonBigContent)({ left: 8, top: "50%", transform: "translateY(-50%)", position: "absolute" }); var AssertionButton = styled(StyledButton)( ({ isAsserting }) => ({ backgroundColor: isAsserting ? "rgba(0, 128, 0, 0.1)" : void 0, color: isAsserting ? "rgb(0, 128, 0)" : void 0 }) ); var DisabledButton = styled(StyledButton)({ cursor: "not-allowed" }); var SavedButton = styled(Button)({ color: "white" }); var Group = styled.div({ display: "flex", alignItems: "center", gap: "9px" }); var EmptyStateDescription = styled.div(({ theme }) => ({ display: "flex", alignItems: "center", gap: "8px" })); var RecordIcon = styled.span( ({ theme, isRecording }) => ({ width: 14, height: 14, borderRadius: "50%", backgroundColor: isRecording ? theme.color.negative : theme.color.secondary, display: "inline-block" }) ); var CodeBlocksWrapper = styled.div({ padding: "16px", display: "flex", flexDirection: "column", gap: "12px" }); var SaveContainer = styled.form({ display: "flex", alignItems: "center", gap: 8 }); var SaveIconColorful = styled(CheckIcon)(({ theme }) => ({ width: 12, color: theme.color.secondary })); var SaveInput = styled(Form.Input)(({ theme }) => ({ paddingLeft: 10, paddingRight: 10, fontSize: theme.typography.size.s1, height: 28, minHeight: "unset", ...theme.base === "light" && { color: theme.color.darkest }, "::placeholder": { color: theme.color.mediumdark }, "&:invalid:not(:placeholder-shown)": { boxShadow: `${theme.color.negative} 0 0 0 1px inset` }, "&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration": { display: "none" } })); var RotatingIcon = styled(SyncIcon)(({ theme }) => ({ animation: `${theme.animation.rotate360} 1s linear infinite;` })); var StyledCheckIcon = styled(CheckIcon)({ width: 12 }); var ErrorButton = styled(Button)(({ theme }) => ({ color: theme.color.negative })); var ErrorIcon = styled(CrossIcon)(({ theme }) => ({ color: theme.color.negative })); var AssertIcon = styled(AddIcon)({ width: 14 }); // src/SaveStory.tsx var stringifyArgs = (args) => JSON.stringify(args, (_, value) => { if (typeof value === "function") { return "__sb_empty_function_arg__"; } return value; }); var SaveStoryButton = ({ code, turnOffRecording }) => { const api = useStorybookApi(); const [name, setName] = useState(""); const [state, setState] = useState("button"); const storyData = api.getCurrentStoryData(); useEffect(() => { setName(storyData?.name); setState("button"); }, [storyData?.name]); const saveStory = async () => { setState("creating"); const payload = { code, csfId: storyData.id, importPath: storyData.importPath, args: stringifyArgs( "args" in storyData ? Object.entries(storyData.args || {}).reduce( (acc, [key, value]) => { if (!dequal(value, storyData.initialArgs?.[key])) { acc[key] = value; } return acc; }, {} ) : {} ), name }; const channel = api.getChannel(); if (!channel) { return; } try { const { newStoryId } = await experimental_requestResponse( // biome-ignore lint/suspicious/noExplicitAny: Should be fixed with new package version channel, EVENTS.SAVE_NEW_STORY_REQUEST, EVENTS.SAVE_NEW_STORY_RESPONSE, payload ); if (newStoryId === storyData.id) { setState("success"); setTimeout(() => { setState("button"); }, 2e3); } else { setState("button"); api.addNotification({ id: "create-new-story-file-success", content: { headline: "Story file created", subHeadline: `${name} was created` }, duration: 8e3, icon: /* @__PURE__ */ React2.createElement(CheckIcon, null) }); } await trySelectNewStory(api.selectStory, newStoryId); } catch (ex) { console.error(ex); api.addNotification({ id: "create-new-story-file-error", content: { headline: "Failed to save story", subHeadline: "Please try again" }, duration: 8e3, icon: /* @__PURE__ */ React2.createElement(CheckIcon, null) }); setState("error"); setTimeout(() => { setState("button"); }, 2e3); } finally { turnOffRecording(); } }; const isDevelopment = ( // biome-ignore lint/suspicious/noExplicitAny: global.CONFIG_TYPE === "DEVELOPMENT" ); return /* @__PURE__ */ React2.createElement(SaveContainer, null, state === "button" && isDevelopment && /* @__PURE__ */ React2.createElement(StyledButton, { onClick: () => setState("input"), variant: "outline" }, /* @__PURE__ */ React2.createElement(SaveIconColorful, { size: 16 }), " Save to story"), state === "button" && !isDevelopment && /* @__PURE__ */ React2.createElement( WithTooltip, { as: "div", hasChrome: false, trigger: "hover", tooltip: /* @__PURE__ */ React2.createElement(TooltipNote, { note: "Only available in development mode" }) }, /* @__PURE__ */ React2.createElement(DisabledButton, { variant: "outline", type: "button" }, /* @__PURE__ */ React2.createElement(SaveIconColorful, { size: 16 }), " Save to story") ), state === "input" && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement( SaveInput, { placeholder: "Type story name", required: true, autoFocus: true, value: name, onChange: (e2) => setName(e2.target.value) } ), /* @__PURE__ */ React2.createElement(StyledButton, { onClick: saveStory, type: "submit", variant: "outline" }, "Save")), state === "creating" && /* @__PURE__ */ React2.createElement(StyledButton, { onClick: () => setState("input"), variant: "outline" }, /* @__PURE__ */ React2.createElement(RotatingIcon, null), " Saving"), state === "success" && /* @__PURE__ */ React2.createElement(SavedButton, { variant: "solid", type: "button" }, /* @__PURE__ */ React2.createElement(StyledCheckIcon, null), " Saved"), state === "error" && /* @__PURE__ */ React2.createElement(ErrorButton, { variant: "ghost", type: "button" }, /* @__PURE__ */ React2.createElement(ErrorIcon, null), " Failed to save")); }; async function trySelectNewStory(selectStory, storyId, attempt = 1) { if (attempt > 10) { throw new Error("We could not select the new story. Please try again."); } try { await selectStory(storyId); } catch (e2) { await new Promise((resolve) => setTimeout(resolve, 500)); return trySelectNewStory(selectStory, storyId, attempt + 1); } } // src/codegen/args-to-string.ts var argsToString = (args) => args.map((arg) => { if (typeof arg === "string") { return wrapInQuotes(arg); } if (typeof arg === "object" && arg !== null) { if (Array.isArray(arg)) { return `[${arg.map((item) => typeof item === "string" ? wrapInQuotes(item) : item).join(", ")}]`; } return `{ ${Object.entries(arg).reduce((acc, [key, value]) => { if (typeof value === "object" && value !== null) { acc.push(`${key}: ${argsToString([value])}`); } else { acc.push( `${key}: ${typeof value === "string" ? wrapInQuotes(value) : value}` ); } return acc; }, []).join(", ")} }`; } return String(arg); }).join(", "); var wrapInQuotes = (str) => { const result = str.replace(/\\/g, "\\\\"); if (result.includes("\n")) { return `\`${result.replace(/`/g, "\\`")}\``; } return `'${result.replace(/'/g, "\\'")}'`; }; // src/codegen/combine-interactions.ts var combineInteractions = (interaction, existingInteractions) => { const result = [...existingInteractions]; if (interaction.event.type === "dblclick") { let clicksRemoved = 0; for (let i2 = result.length - 1; i2 >= 0 && clicksRemoved < 2; i2--) { if (result[i2].event.type === "click") { result.splice(i2, 1); clicksRemoved++; } } result.push(interaction); return result; } if (interaction.event.type === "type") { for (let i2 = result.length - 1; i2 >= 0; i2--) { const prevInteraction = result[i2]; const isNotType = prevInteraction.event.type !== "type"; const isShiftKey = prevInteraction.event.type === "keydown" && prevInteraction.event.key === "shift"; if (isNotType && !isShiftKey) { result.push(interaction); return result; } if (prevInteraction?.event.type === "type" && isSameElementQuery(prevInteraction, interaction)) { prevInteraction.event.value = interaction.event.value; return result; } } result.push(interaction); return result; } if (interaction.event.type === "keyup") { for (let i2 = result.length - 1; i2 >= 0; i2--) { const prevInteraction = result[i2]; if (prevInteraction.event.type === "keydown" && prevInteraction.event.key === "shift") { result.splice(i2, 1); return result; } } return result; } if (interaction.event.type === "focus") { let hasShift = false; let tabKeydownIndex = null; for (let i2 = result.length - 1; i2 >= 0; i2--) { const prevInteraction = result[i2]; if (prevInteraction?.event.type === "keydown" && prevInteraction?.event.key === "tab") { tabKeydownIndex = i2; } if (prevInteraction?.event.type === "keydown" && prevInteraction?.event.key === "shift") { hasShift = true; break; } } if (tabKeydownIndex !== null) { result.splice(tabKeydownIndex, 1); result.push({ elementQuery: interaction.elementQuery, event: { type: "focus", shift: hasShift } }); return result; } return result; } result.push(interaction); return result; }; var isSameElementQuery = (a2, b) => { return a2.elementQuery.object === b.elementQuery.object && a2.elementQuery.method === b.elementQuery.method && argsToString(a2.elementQuery.args) === argsToString(b.elementQuery.args) && a2.elementQuery.nth === b.elementQuery.nth; }; // src/codegen/interactions-to-code.ts var EVENT_TO_USER_EVENT = { click: "click", dblclick: "dblClick", keydown: "keyboard", type: "type", select: "selectOptions", upload: "upload", focus: "focus" }; var convertInteractionsToCode = (interactions, hasTypescript) => { const codeLines = []; let usesBody = false; let usesCanvas = false; let needsExpect = false; for (const interaction of interactions) { const { event } = interaction; if (event.type === "keyup" || event.type === "keydown" && ["shift", "tab"].includes(event.key)) { continue; } if (event.type === "assertion") { needsExpect = true; const { queryString: queryString2, asElementPostfix, warning: warning2 } = getQueryString( interaction.elementQuery, hasTypescript ); let assertCode = `expect(${queryString2.replace(asElementPostfix, "")})`.replace("await ", "").replace("canvas.find", "canvas.query"); if (event.args && event.args.length > 0) { assertCode += `.${event.assertionType}(${argsToString(event.args)})`; } else { assertCode += `.${event.assertionType}()`; } if (interaction.elementQuery.object === "body") { usesBody = true; } if (interaction.elementQuery.object === "canvas") { usesCanvas = true; } codeLines.push({ text: `await waitFor(() => ${assertCode})`, warning: warning2 }); continue; } let beginning = `await userEvent.${EVENT_TO_USER_EVENT[event.type]}`; let { queryString, assertion, warning } = getQueryString( interaction.elementQuery, hasTypescript ); let valueStr = ""; if (event.type === "type") { if (event.value === "") { beginning = beginning.replace(EVENT_TO_USER_EVENT[event.type], "clear"); valueStr = ""; } else { valueStr = `, ${wrapInQuotes(event.value)}`; } } else if (event.type === "keydown") { queryString = ""; assertion = null; warning = void 0; valueStr = `'${event.key}'`; } else if (event.type === "select") { valueStr = `, [${event.options.map((option) => `'${option}'`).join(", ")}]`; } else if (event.type === "upload") { valueStr = `, [${event.files.map((file) => `new File(['${file}'], '${file}')`).join(", ")}]`; } else if (event.type === "focus") { beginning = beginning.replace("focus", "tab"); queryString = ""; assertion = null; warning = void 0; valueStr = event.shift ? "{ shift: true }" : ""; } if (queryString) { if (interaction.elementQuery.object === "body") { usesBody = true; } else { usesCanvas = true; } } if (assertion) { codeLines.push({ text: assertion, warning }); needsExpect = true; } codeLines.push({ text: `${beginning}(${queryString}${valueStr});`, warning }); } if (!codeLines.length) { return { imports: [], play: [] }; } const importNames = ["userEvent"]; if (usesCanvas) { importNames.push("within"); } if (needsExpect || usesBody) { importNames.push("waitFor", "expect"); } const play = [ { text: "play: async ({ canvasElement }) => {" } ]; if (usesBody) { play.push({ text: tab("const body = canvasElement.ownerDocument.body;") }); } if (usesCanvas) { if (usesBody) { play.push({ text: tab("const canvas = within(body);") }); } else { play.push({ text: tab("const canvas = within(canvasElement.ownerDocument.body);") }); } } play.push( ...codeLines.map((codeLine) => ({ text: tab(codeLine.text), warning: codeLine.warning })), { text: "}" } ); return { imports: [ { text: `import { ${importNames.join(", ")} } from 'storybook/test';` } ], play }; }; var tab = (str) => ` ${str}`; var getQueryString = (query, hasTypescript) => { const asElementPostfix = " as HTMLElement"; const beginning = `${query.object === "canvas" ? "await " : ""}${query.object}.${query.method}`; const args = argsToString(query.args); let warning; if (query.method === "querySelector") { warning = "QUERY_SELECTOR"; } else if (query.method.includes("ByRole") && (!query.args[1] || !("name" in query.args[1]))) { warning = "ROLE_WITHOUT_NAME"; } else if (query.method.includes("ByTestId")) { warning = "TEST_ID"; } const asElement = query.object === "body" && hasTypescript ? asElementPostfix : ""; const queryString = `${beginning}(${args})${asElement}`; const result = query.nth === null ? queryString : `(${queryString})[${query.nth}]`; const queryStringWithoutAsElement = result.replace(asElement, ""); const assertion = query.object === "body" ? `await waitFor(() => expect(${queryStringWithoutAsElement}).toBeInTheDocument());` : null; return { assertion, queryString: result, asElementPostfix, warning }; }; var useInteractions = () => useAddonState(ADDON_ID, JSON.stringify([])); var useIsRecording = () => { const [globals, setGlobals] = useGlobals(); return [ globals[IS_RECORDING_KEY] === true, (isRecording) => { setGlobals({ [IS_RECORDING_KEY]: isRecording }); } ]; }; var useIsAsserting = () => { const [globals, setGlobals] = useGlobals(); return [ globals[IS_ASSERTING_KEY] === true, (isAssertionMode) => { setGlobals({ [IS_ASSERTING_KEY]: isAssertionMode }); } ]; }; // src/InteractionRecorder.tsx var InteractionRecorder = () => { const [interactions, setInteractions] = useInteractions(); const [isRecording, setIsRecording] = useIsRecording(); const [isAsserting, setIsAsserting] = useIsAsserting(); useChannel({ [EVENTS.INTERACTION]: (interaction) => { setInteractions( (prevInteractions) => JSON.stringify( combineInteractions(interaction, JSON.parse(prevInteractions)) ) ); } }); const toggleRecording = () => { if (isRecording && isAsserting) { setIsAsserting(false); } setIsRecording(!isRecording); }; const toggleAsserting = () => setIsAsserting(!isAsserting); const resetInteractions = () => setInteractions(() => JSON.stringify([])); const api = useStorybookApi(); const storyData = api.getCurrentStoryData(); const turnOffRecording = () => { setIsRecording(false); setIsAsserting(false); }; useEffect(() => { resetInteractions(); turnOffRecording(); }, [storyData?.id]); const hasTypescript = [".ts", ".tsx"].some( (ext) => storyData?.importPath.endsWith(ext) ); const [debouncedInteractions] = a(interactions, 100); const code = useMemo( () => convertInteractionsToCode(JSON.parse(debouncedInteractions), hasTypescript), [debouncedInteractions, hasTypescript] ); const containerRef = useRef(null); useEffect(() => { if (containerRef.current?.parentElement?.style) { containerRef.current.parentElement.style.height = "100%"; } }, []); const codeBlocksRef = useRef(null); const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); useLayoutEffect(() => { const element = codeBlocksRef.current; if (!element || !isScrolledToBottom) { return; } const observer = new MutationObserver(() => { if (isScrolledToBottom) { setTimeout(() => { element.scrollTop = element.scrollHeight; }); } }); observer.observe(element, { childList: true, subtree: true }); return () => observer.disconnect(); }, [code, isScrolledToBottom]); return /* @__PURE__ */ React2.createElement(Container2, { ref: containerRef }, /* @__PURE__ */ React2.createElement(SubnavWrapper, null, /* @__PURE__ */ React2.createElement(Bar, null, /* @__PURE__ */ React2.createElement(StyledSubnav, null, /* @__PURE__ */ React2.createElement(Group, null, /* @__PURE__ */ React2.createElement(StyledButton, { onClick: toggleRecording }, /* @__PURE__ */ React2.createElement(StyledButtonBigContent, { isHidden: isRecording }, /* @__PURE__ */ React2.createElement(RecordIcon, { isRecording: false }), "Start recording"), /* @__PURE__ */ React2.createElement(StyledButtonSmallContent, { isHidden: !isRecording }, /* @__PURE__ */ React2.createElement(RecordIcon, { isRecording: true }), "Stop recording")), /* @__PURE__ */ React2.createElement( AssertionButton, { disabled: !isRecording, onClick: toggleAsserting, isAsserting }, /* @__PURE__ */ React2.createElement(AssertIcon, null), isAsserting ? "Choose element" : "Add assertion" ), /* @__PURE__ */ React2.createElement(StyledButton, { onClick: resetInteractions, disabled: !code.play.length }, /* @__PURE__ */ React2.createElement(DeleteIcon, null), "Reset")), code.play.length > 0 && /* @__PURE__ */ React2.createElement(SaveStoryButton, { code, turnOffRecording })))), /* @__PURE__ */ React2.createElement( ContentWrapper, { ref: codeBlocksRef, onScroll: (e2) => { const { scrollTop, scrollHeight, clientHeight } = e2.target; setIsScrolledToBottom(scrollTop + clientHeight >= scrollHeight); } }, code.play.length === 0 && !isRecording && /* @__PURE__ */ React2.createElement( EmptyTabContent, { title: "No interactions have been recorded.", description: /* @__PURE__ */ React2.createElement(EmptyStateDescription, null, "Click the record button", /* @__PURE__ */ React2.createElement( RecordIcon, { isRecording: false, onClick: toggleRecording, style: { cursor: "pointer" } } ), "to start recording.") } ), code.play.length === 0 && isRecording && /* @__PURE__ */ React2.createElement( EmptyTabContent, { title: "Recording is in progress...", description: isAsserting ? "Click on elements to record assertions." : "Interact with the story to record events." } ), code.play.length > 0 && /* @__PURE__ */ React2.createElement(CodeBlocksWrapper, null, /* @__PURE__ */ React2.createElement(CodeBlock, { name: "Imports", codeLines: code.imports }), /* @__PURE__ */ React2.createElement(CodeBlock, { name: "Play Function", codeLines: code.play, isSticky: true })) )); }; // src/manager.tsx function Title2() { const [interactions] = useInteractions(); const [isRecording] = useIsRecording(); return /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement(Spaced, { col: 1 }, /* @__PURE__ */ React2.createElement("span", { style: { display: "inline-block", verticalAlign: "middle" } }, "Interaction Recorder"), isRecording && /* @__PURE__ */ React2.createElement(Badge, { status: "neutral" }, JSON.parse(interactions).length))); } addons.register(ADDON_ID, (api) => { const channel = api.getChannel(); channel?.on( EVENTS.SAVE_NEW_STORY_RESPONSE, (data) => { if (!data.success) { return; } const story = api.getCurrentStoryData(); if (story.type !== "story") { return; } api.resetStoryArgs(story); if (data.payload.newStoryId) { api.selectStory(data.payload.newStoryId); } } ); addons.add(PANEL_ID, { type: types.PANEL, title: Title2, match: ({ viewMode }) => viewMode === "story", render: ({ active }) => /* @__PURE__ */ React2.createElement(AddonPanel, { active: active ?? false }, /* @__PURE__ */ React2.createElement(InteractionRecorder, null)) }); });