testomatio-editor-blocks 0.4.44 → 0.4.45

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createReactBlockSpec, useEditorChange } from "@blocknote/react";
3
- import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { StepField } from "./stepField";
5
5
  import { StepHorizontalView } from "./stepHorizontalView";
6
6
  import { useStepImageUpload } from "../stepImageUpload";
@@ -9,6 +9,7 @@ const VIEW_MODE_KEY = "bn-step-view-mode";
9
9
  const STEP_TITLE_PLACEHOLDER = "Enter step title...";
10
10
  const STEP_DATA_PLACEHOLDER = "Enter step data...";
11
11
  const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result...";
12
+ const FORCE_VERTICAL_WIDTH = 550;
12
13
  /* readExpectedCollapsedPreference removed — currently unused */
13
14
  const writeExpectedCollapsedPreference = (collapsed) => {
14
15
  if (typeof window === "undefined") {
@@ -172,6 +173,22 @@ export const stepBlock = createReactBlockSpec({
172
173
  const [documentVersion, setDocumentVersion] = useState(0);
173
174
  const uploadImage = useStepImageUpload();
174
175
  const [viewMode, setViewMode] = useState(() => readStepViewMode());
176
+ const containerRef = useRef(null);
177
+ const [forceVertical, setForceVertical] = useState(false);
178
+ useEffect(() => {
179
+ var _a;
180
+ const el = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
181
+ if (!el)
182
+ return;
183
+ const observer = new ResizeObserver((entries) => {
184
+ for (const entry of entries) {
185
+ setForceVertical(entry.contentRect.width < FORCE_VERTICAL_WIDTH);
186
+ }
187
+ });
188
+ observer.observe(el);
189
+ return () => observer.disconnect();
190
+ }, []);
191
+ const effectiveVertical = forceVertical || viewMode === "vertical";
175
192
  // Calculate step number based on position in document
176
193
  const stepNumber = useMemo(() => {
177
194
  const allBlocks = editor.document;
@@ -337,10 +354,11 @@ export const stepBlock = createReactBlockSpec({
337
354
  }, [expectedHasContent, isExpectedVisible]);
338
355
  const canToggleData = !dataHasContent;
339
356
  const canToggleExpected = !expectedHasContent;
340
- if (viewMode === "horizontal") {
341
- return (_jsx(StepHorizontalView, { blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: _jsx("button", { type: "button", className: "bn-teststep__view-toggle bn-teststep__view-toggle--horizontal", "data-tooltip": "Switch step view", "aria-label": "Switch step view", onClick: handleToggleView, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }) }));
357
+ const viewToggleButton = (_jsx("button", { type: "button", className: `bn-teststep__view-toggle${!effectiveVertical ? " bn-teststep__view-toggle--horizontal" : ""}${forceVertical ? " bn-teststep__view-toggle--disabled" : ""}`, "data-tooltip": "Switch step view", "aria-label": "Switch step view", onClick: forceVertical ? undefined : handleToggleView, "aria-disabled": forceVertical, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }));
358
+ if (!effectiveVertical) {
359
+ return (_jsx(StepHorizontalView, { ref: containerRef, blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: viewToggleButton }));
342
360
  }
343
- return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), _jsx("button", { type: "button", className: "bn-teststep__view-toggle", "data-tooltip": "Switch step view", "aria-label": "Switch step view", onClick: handleToggleView, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) })] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
361
+ return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
344
362
  if (!uploadImage) {
345
363
  return;
346
364
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  type StepHorizontalViewProps = {
3
3
  blockId: string;
4
4
  stepNumber: number;
@@ -10,5 +10,5 @@ type StepHorizontalViewProps = {
10
10
  onFieldFocus: () => void;
11
11
  viewToggle?: ReactNode;
12
12
  };
13
- export declare function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }: StepHorizontalViewProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare const StepHorizontalView: import("react").ForwardRefExoticComponent<StepHorizontalViewProps & import("react").RefAttributes<HTMLDivElement>>;
14
14
  export {};
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
2
3
  import { StepField } from "./stepField";
3
4
  const STEP_PLACEHOLDER = "Enter step name";
4
5
  const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
5
- export function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }) {
6
- return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Step", value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }) }), _jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Expected result", labelAction: viewToggle, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus }) })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
7
- }
6
+ export const StepHorizontalView = forwardRef(function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }, ref) {
7
+ return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, ref: ref, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Step", value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }) }), _jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Expected result", labelAction: viewToggle, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus }) })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
8
+ });
@@ -80,15 +80,20 @@ function unescapeMarkdown(text) {
80
80
  return stripHtmlWrappers(text).replace(/\\([*_`~\[\]()<>\\])/g, "$1").replace(/\\>/g, ">");
81
81
  }
82
82
  function applyTextStyles(text, styles) {
83
+ var _a, _b, _c, _d;
83
84
  if (!styles) {
84
85
  return text;
85
86
  }
86
87
  const hasCode = styles.code === true;
87
88
  let result = text;
88
89
  if (hasCode) {
89
- result = "`" + result.replace(/`/g, "\\`") + "`";
90
+ const leadingWs = (_b = (_a = result.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
91
+ const trailingWs = (_d = (_c = result.match(/(\s*)$/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : "";
92
+ const trimmed = result.slice(leadingWs.length, result.length - trailingWs.length || undefined);
93
+ if (!trimmed)
94
+ return result;
90
95
  // Code style supersedes other styles in Markdown.
91
- return result;
96
+ return leadingWs + "`" + trimmed.replace(/`/g, "\\`") + "`" + trailingWs;
92
97
  }
93
98
  const wrappers = [];
94
99
  if (styles.bold) {
@@ -120,15 +125,20 @@ function applyTextStyles(text, styles) {
120
125
  // trapped inside markers like **bold<br/>text**.
121
126
  const segments = result.split("\n");
122
127
  const wrapped = segments.map((segment) => {
123
- var _a;
128
+ var _a, _b, _c, _d, _e;
124
129
  if (!segment)
125
130
  return segment;
126
- let s = segment;
131
+ const leadingWs = (_b = (_a = segment.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
132
+ const trailingWs = (_d = (_c = segment.match(/(\s*)$/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : "";
133
+ const trimmed = segment.slice(leadingWs.length, segment.length - trailingWs.length || undefined);
134
+ if (!trimmed)
135
+ return segment;
136
+ let s = trimmed;
127
137
  for (const wrapper of wrappers) {
128
- const suffix = (_a = wrapper.suffix) !== null && _a !== void 0 ? _a : wrapper.prefix;
138
+ const suffix = (_e = wrapper.suffix) !== null && _e !== void 0 ? _e : wrapper.prefix;
129
139
  s = `${wrapper.prefix}${s}${suffix}`;
130
140
  }
131
- return s;
141
+ return leadingWs + s + trailingWs;
132
142
  });
133
143
  return wrapped.join("\n");
134
144
  }
@@ -828,11 +838,18 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
828
838
  // Check for expected result labels with different formatting
829
839
  const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
830
840
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
831
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
841
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
842
+ rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
832
843
  if (expectedMatch || expectedStarMatch) {
833
844
  inExpectedResult = true;
834
- const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
835
- let content = rawTrimmed.slice(label.length).trim();
845
+ // Prefer the star match (more specific about formatting) to avoid leaking markers
846
+ let content;
847
+ if (expectedStarMatch) {
848
+ content = (expectedStarMatch[1] || '').trim();
849
+ }
850
+ else {
851
+ content = rawTrimmed.slice(expectedMatch[0].length).trim();
852
+ }
836
853
  // Add the content (if any) from this line
837
854
  if (content) {
838
855
  const expectedContent = unescapeMarkdown(content);
@@ -504,6 +504,11 @@ html.dark .bn-step-image-preview__content {
504
504
  transform: rotate(90deg);
505
505
  }
506
506
 
507
+ .bn-teststep__view-toggle--disabled {
508
+ cursor: not-allowed;
509
+ opacity: 0.3 !important;
510
+ }
511
+
507
512
  .bn-teststep__horizontal-fields {
508
513
  display: flex;
509
514
  gap: 16px;
@@ -521,11 +526,6 @@ html.dark .bn-step-image-preview__content {
521
526
  min-height: 28px;
522
527
  }
523
528
 
524
- @media (max-width: 860px) {
525
- .bn-teststep__horizontal-fields {
526
- flex-direction: column;
527
- }
528
- }
529
529
 
530
530
  .bn-snippet .bn-step-field__input {
531
531
  border-color: var(--snippet-border-light);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.44",
3
+ "version": "0.4.45",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
@@ -1,5 +1,5 @@
1
1
  import { createReactBlockSpec, useEditorChange } from "@blocknote/react";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { StepField } from "./stepField";
4
4
  import { StepHorizontalView } from "./stepHorizontalView";
5
5
  import { useStepImageUpload } from "../stepImageUpload";
@@ -11,6 +11,7 @@ const STEP_TITLE_PLACEHOLDER = "Enter step title...";
11
11
  const STEP_DATA_PLACEHOLDER = "Enter step data...";
12
12
  const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result...";
13
13
  type StepViewMode = "vertical" | "horizontal";
14
+ const FORCE_VERTICAL_WIDTH = 550;
14
15
 
15
16
  /* readExpectedCollapsedPreference removed — currently unused */
16
17
 
@@ -191,6 +192,22 @@ export const stepBlock = createReactBlockSpec(
191
192
  const [documentVersion, setDocumentVersion] = useState(0);
192
193
  const uploadImage = useStepImageUpload();
193
194
  const [viewMode, setViewMode] = useState<StepViewMode>(() => readStepViewMode());
195
+ const containerRef = useRef<HTMLDivElement>(null);
196
+ const [forceVertical, setForceVertical] = useState(false);
197
+
198
+ useEffect(() => {
199
+ const el = containerRef.current?.parentElement;
200
+ if (!el) return;
201
+ const observer = new ResizeObserver((entries) => {
202
+ for (const entry of entries) {
203
+ setForceVertical(entry.contentRect.width < FORCE_VERTICAL_WIDTH);
204
+ }
205
+ });
206
+ observer.observe(el);
207
+ return () => observer.disconnect();
208
+ }, []);
209
+
210
+ const effectiveVertical = forceVertical || viewMode === "vertical";
194
211
 
195
212
  // Calculate step number based on position in document
196
213
  const stepNumber = useMemo(() => {
@@ -391,9 +408,30 @@ export const stepBlock = createReactBlockSpec(
391
408
  const canToggleData = !dataHasContent;
392
409
  const canToggleExpected = !expectedHasContent;
393
410
 
394
- if (viewMode === "horizontal") {
411
+ const viewToggleButton = (
412
+ <button
413
+ type="button"
414
+ className={`bn-teststep__view-toggle${!effectiveVertical ? " bn-teststep__view-toggle--horizontal" : ""}${forceVertical ? " bn-teststep__view-toggle--disabled" : ""}`}
415
+ data-tooltip="Switch step view"
416
+ aria-label="Switch step view"
417
+ onClick={forceVertical ? undefined : handleToggleView}
418
+ aria-disabled={forceVertical}
419
+ >
420
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
421
+ <mask id="mask-toggle" style={{maskType: "alpha"}} maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
422
+ <rect width="16" height="16" fill="#D9D9D9"/>
423
+ </mask>
424
+ <g mask="url(#mask-toggle)">
425
+ <path d="M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z" fill="currentColor"/>
426
+ </g>
427
+ </svg>
428
+ </button>
429
+ );
430
+
431
+ if (!effectiveVertical) {
395
432
  return (
396
433
  <StepHorizontalView
434
+ ref={containerRef}
397
435
  blockId={block.id}
398
436
  stepNumber={stepNumber}
399
437
  stepValue={combinedStepValue}
@@ -402,30 +440,13 @@ export const stepBlock = createReactBlockSpec(
402
440
  onExpectedChange={handleExpectedChange}
403
441
  onInsertNextStep={handleInsertNextStep}
404
442
  onFieldFocus={handleFieldFocus}
405
- viewToggle={
406
- <button
407
- type="button"
408
- className="bn-teststep__view-toggle bn-teststep__view-toggle--horizontal"
409
- data-tooltip="Switch step view"
410
- aria-label="Switch step view"
411
- onClick={handleToggleView}
412
- >
413
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
414
- <mask id="mask-toggle" style={{maskType: "alpha"}} maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
415
- <rect width="16" height="16" fill="#D9D9D9"/>
416
- </mask>
417
- <g mask="url(#mask-toggle)">
418
- <path d="M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z" fill="currentColor"/>
419
- </g>
420
- </svg>
421
- </button>
422
- }
443
+ viewToggle={viewToggleButton}
423
444
  />
424
445
  );
425
446
  }
426
447
 
427
448
  return (
428
- <div className="bn-teststep" data-block-id={block.id}>
449
+ <div className="bn-teststep" data-block-id={block.id} ref={containerRef}>
429
450
  <div className="bn-teststep__timeline">
430
451
  <span className="bn-teststep__number">{stepNumber}</span>
431
452
  <div className="bn-teststep__line" />
@@ -433,22 +454,7 @@ export const stepBlock = createReactBlockSpec(
433
454
  <div className="bn-teststep__content">
434
455
  <div className="bn-teststep__header">
435
456
  <span className="bn-teststep__title">Step</span>
436
- <button
437
- type="button"
438
- className="bn-teststep__view-toggle"
439
- data-tooltip="Switch step view"
440
- aria-label="Switch step view"
441
- onClick={handleToggleView}
442
- >
443
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
444
- <mask id="mask-toggle" style={{maskType: "alpha"}} maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
445
- <rect width="16" height="16" fill="#D9D9D9"/>
446
- </mask>
447
- <g mask="url(#mask-toggle)">
448
- <path d="M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z" fill="currentColor"/>
449
- </g>
450
- </svg>
451
- </button>
457
+ {viewToggleButton}
452
458
  </div>
453
459
  <StepField
454
460
  label="Step"
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { forwardRef, type ReactNode } from "react";
2
2
  import { StepField } from "./stepField";
3
3
  import type { StepSuggestion } from "../stepAutocomplete";
4
4
 
@@ -17,7 +17,7 @@ type StepHorizontalViewProps = {
17
17
  viewToggle?: ReactNode;
18
18
  };
19
19
 
20
- export function StepHorizontalView({
20
+ export const StepHorizontalView = forwardRef<HTMLDivElement, StepHorizontalViewProps>(function StepHorizontalView({
21
21
  blockId,
22
22
  stepNumber,
23
23
  stepValue,
@@ -27,9 +27,9 @@ export function StepHorizontalView({
27
27
  onInsertNextStep,
28
28
  onFieldFocus,
29
29
  viewToggle,
30
- }: StepHorizontalViewProps) {
30
+ }, ref) {
31
31
  return (
32
- <div className="bn-teststep bn-teststep--horizontal" data-block-id={blockId}>
32
+ <div className="bn-teststep bn-teststep--horizontal" data-block-id={blockId} ref={ref}>
33
33
  <div className="bn-teststep__timeline">
34
34
  <span className="bn-teststep__number">{stepNumber}</span>
35
35
  <div className="bn-teststep__line" />
@@ -79,4 +79,4 @@ export function StepHorizontalView({
79
79
  </div>
80
80
  </div>
81
81
  );
82
- }
82
+ });
@@ -46,6 +46,73 @@ describe("blocksToMarkdown", () => {
46
46
  expect(blocksToMarkdown(blocks)).toBe("Hello **world***!*");
47
47
  });
48
48
 
49
+ it("places bold markers outside leading/trailing spaces", () => {
50
+ const blocks: CustomEditorBlock[] = [
51
+ {
52
+ id: "1",
53
+ type: "paragraph",
54
+ props: baseProps,
55
+ content: [
56
+ { type: "text", text: "some ", styles: {} },
57
+ { type: "text", text: " bold ", styles: { bold: true } },
58
+ { type: "text", text: " text", styles: {} },
59
+ ],
60
+ children: [],
61
+ },
62
+ ];
63
+ expect(blocksToMarkdown(blocks)).toBe("some **bold** text");
64
+ });
65
+
66
+ it("places italic markers outside trailing space", () => {
67
+ const blocks: CustomEditorBlock[] = [
68
+ {
69
+ id: "1",
70
+ type: "paragraph",
71
+ props: baseProps,
72
+ content: [
73
+ { type: "text", text: "word ", styles: { italic: true } },
74
+ { type: "text", text: "next", styles: {} },
75
+ ],
76
+ children: [],
77
+ },
78
+ ];
79
+ expect(blocksToMarkdown(blocks)).toBe("*word* next");
80
+ });
81
+
82
+ it("places code backticks outside leading/trailing spaces", () => {
83
+ const blocks: CustomEditorBlock[] = [
84
+ {
85
+ id: "1",
86
+ type: "paragraph",
87
+ props: baseProps,
88
+ content: [
89
+ { type: "text", text: "see ", styles: {} },
90
+ { type: "text", text: " code ", styles: { code: true } },
91
+ { type: "text", text: " here", styles: {} },
92
+ ],
93
+ children: [],
94
+ },
95
+ ];
96
+ expect(blocksToMarkdown(blocks)).toBe("see `code` here");
97
+ });
98
+
99
+ it("returns all-whitespace text unformatted", () => {
100
+ const blocks: CustomEditorBlock[] = [
101
+ {
102
+ id: "1",
103
+ type: "paragraph",
104
+ props: baseProps,
105
+ content: [
106
+ { type: "text", text: "a", styles: {} },
107
+ { type: "text", text: " ", styles: { bold: true } },
108
+ { type: "text", text: "b", styles: {} },
109
+ ],
110
+ children: [],
111
+ },
112
+ ];
113
+ expect(blocksToMarkdown(blocks)).toBe("a b");
114
+ });
115
+
49
116
  it("serializes a numbered list", () => {
50
117
  const blocks: CustomEditorBlock[] = [
51
118
  {
@@ -1048,7 +1115,7 @@ describe("markdownToBlocks", () => {
1048
1115
  props: {
1049
1116
  stepTitle: "Open the form.",
1050
1117
  stepData: "",
1051
- expectedResult: "** The form opens.\nFields are empty.",
1118
+ expectedResult: "The form opens.\nFields are empty.",
1052
1119
  listStyle: "bullet",
1053
1120
  },
1054
1121
  children: [],
@@ -1212,7 +1279,7 @@ describe("markdownToBlocks", () => {
1212
1279
  stepTitle:
1213
1280
  "Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
1214
1281
  stepData: "",
1215
- expectedResult: "* Upsell SS screen is displayed",
1282
+ expectedResult: "Upsell SS screen is displayed",
1216
1283
  listStyle: "bullet",
1217
1284
  },
1218
1285
  children: [],
@@ -1222,7 +1289,7 @@ describe("markdownToBlocks", () => {
1222
1289
  props: {
1223
1290
  stepTitle: "Close SS",
1224
1291
  stepData: "",
1225
- expectedResult: "* My Course and More tab are displayed",
1292
+ expectedResult: "My Course and More tab are displayed",
1226
1293
  listStyle: "bullet",
1227
1294
  },
1228
1295
  children: [],
@@ -1742,7 +1809,7 @@ describe("markdownToBlocks", () => {
1742
1809
  props: {
1743
1810
  stepTitle: "Check UI of Sleep score info screen",
1744
1811
  stepData: "- Back button\nHeader: Sleep Score Info\nText: Ever wonder if 6, 8, or 9 hours of sleep are enough? Sleep score takes the guesswork out of your ZZZ's and shows you how well you slept last night based on duration, efficiency, and consistency.",
1745
- expectedResult: "* - 1st block:\n* - 2nd block:\n* - 3d block:",
1812
+ expectedResult: "- 1st block:\n- 2nd block:\n- 3d block:",
1746
1813
  listStyle: "bullet",
1747
1814
  },
1748
1815
  children: [],
@@ -137,9 +137,12 @@ function applyTextStyles(text: string, styles: EditorStyles | undefined): string
137
137
  let result = text;
138
138
 
139
139
  if (hasCode) {
140
- result = "`" + result.replace(/`/g, "\\`") + "`";
140
+ const leadingWs = result.match(/^(\s*)/)?.[1] ?? "";
141
+ const trailingWs = result.match(/(\s*)$/)?.[1] ?? "";
142
+ const trimmed = result.slice(leadingWs.length, result.length - trailingWs.length || undefined);
143
+ if (!trimmed) return result;
141
144
  // Code style supersedes other styles in Markdown.
142
- return result;
145
+ return leadingWs + "`" + trimmed.replace(/`/g, "\\`") + "`" + trailingWs;
143
146
  }
144
147
 
145
148
  const wrappers: Array<{ prefix: string; suffix?: string }> = [];
@@ -180,12 +183,16 @@ function applyTextStyles(text: string, styles: EditorStyles | undefined): string
180
183
  const segments = result.split("\n");
181
184
  const wrapped = segments.map((segment) => {
182
185
  if (!segment) return segment;
183
- let s = segment;
186
+ const leadingWs = segment.match(/^(\s*)/)?.[1] ?? "";
187
+ const trailingWs = segment.match(/(\s*)$/)?.[1] ?? "";
188
+ const trimmed = segment.slice(leadingWs.length, segment.length - trailingWs.length || undefined);
189
+ if (!trimmed) return segment;
190
+ let s = trimmed;
184
191
  for (const wrapper of wrappers) {
185
192
  const suffix = wrapper.suffix ?? wrapper.prefix;
186
193
  s = `${wrapper.prefix}${s}${suffix}`;
187
194
  }
188
- return s;
195
+ return leadingWs + s + trailingWs;
189
196
  });
190
197
 
191
198
  return wrapped.join("\n");
@@ -1004,12 +1011,18 @@ function parseTestStep(
1004
1011
  // Check for expected result labels with different formatting
1005
1012
  const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
1006
1013
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
1007
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
1014
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
1015
+ rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
1008
1016
 
1009
1017
  if (expectedMatch || expectedStarMatch) {
1010
1018
  inExpectedResult = true;
1011
- const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
1012
- let content = rawTrimmed.slice(label.length).trim();
1019
+ // Prefer the star match (more specific about formatting) to avoid leaking markers
1020
+ let content: string;
1021
+ if (expectedStarMatch) {
1022
+ content = (expectedStarMatch[1] || '').trim();
1023
+ } else {
1024
+ content = rawTrimmed.slice(expectedMatch![0].length).trim();
1025
+ }
1013
1026
 
1014
1027
  // Add the content (if any) from this line
1015
1028
  if (content) {
@@ -504,6 +504,11 @@ html.dark .bn-step-image-preview__content {
504
504
  transform: rotate(90deg);
505
505
  }
506
506
 
507
+ .bn-teststep__view-toggle--disabled {
508
+ cursor: not-allowed;
509
+ opacity: 0.3 !important;
510
+ }
511
+
507
512
  .bn-teststep__horizontal-fields {
508
513
  display: flex;
509
514
  gap: 16px;
@@ -521,11 +526,6 @@ html.dark .bn-step-image-preview__content {
521
526
  min-height: 28px;
522
527
  }
523
528
 
524
- @media (max-width: 860px) {
525
- .bn-teststep__horizontal-fields {
526
- flex-direction: column;
527
- }
528
- }
529
529
 
530
530
  .bn-snippet .bn-step-field__input {
531
531
  border-color: var(--snippet-border-light);