testomatio-editor-blocks 0.4.40 → 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
  }
@@ -795,9 +805,11 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
795
805
  let expectedResult = "";
796
806
  let next = index + 1;
797
807
  let inExpectedResult = false;
808
+ const stepIndent = current.length - current.trimStart().length;
798
809
  while (next < lines.length) {
799
810
  const line = lines[next];
800
- const hasIndent = /^\s{2,}/.test(line);
811
+ const lineIndent = line.length - line.trimStart().length;
812
+ const hasIndent = lineIndent > stepIndent;
801
813
  const rawTrimmed = line.trim();
802
814
  if (!rawTrimmed) {
803
815
  if (stepDataLines.length > 0 || inExpectedResult) {
@@ -826,11 +838,18 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
826
838
  // Check for expected result labels with different formatting
827
839
  const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
828
840
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
829
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
841
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
842
+ rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
830
843
  if (expectedMatch || expectedStarMatch) {
831
844
  inExpectedResult = true;
832
- const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
833
- 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
+ }
834
853
  // Add the content (if any) from this line
835
854
  if (content) {
836
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.40",
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
  {
@@ -1018,12 +1085,21 @@ describe("markdownToBlocks", () => {
1018
1085
  "",
1019
1086
  " * Open login page",
1020
1087
  " *Expected*: The main page is opened",
1088
+ " * Enter username {{username}} and password ${password}",
1089
+ " * Click login button",
1090
+ " * Verify user is redirected to the dashboard",
1021
1091
  ].join("\n");
1022
1092
 
1023
1093
  const blocks = markdownToBlocks(markdown);
1024
1094
  expect(blocks.length).toBeGreaterThan(0);
1025
- const bullets = blocks.filter((b) => b.type === "bulletListItem");
1026
- expect(bullets.length).toBeGreaterThanOrEqual(1);
1095
+
1096
+ const steps = blocks.filter((b) => b.type === "testStep");
1097
+ expect(steps).toHaveLength(4);
1098
+ expect((steps[0].props as any).stepTitle).toBe("Open login page");
1099
+ expect((steps[0].props as any).expectedResult).toBe("The main page is opened");
1100
+ expect((steps[1].props as any).stepTitle).toBe("Enter username {{username}} and password ${password}");
1101
+ expect((steps[2].props as any).stepTitle).toBe("Click login button");
1102
+ expect((steps[3].props as any).stepTitle).toBe("Verify user is redirected to the dashboard");
1027
1103
  });
1028
1104
 
1029
1105
  it("parses expected result prefixes with emphasis", () => {
@@ -1039,7 +1115,7 @@ describe("markdownToBlocks", () => {
1039
1115
  props: {
1040
1116
  stepTitle: "Open the form.",
1041
1117
  stepData: "",
1042
- expectedResult: "** The form opens.\nFields are empty.",
1118
+ expectedResult: "The form opens.\nFields are empty.",
1043
1119
  listStyle: "bullet",
1044
1120
  },
1045
1121
  children: [],
@@ -1203,7 +1279,7 @@ describe("markdownToBlocks", () => {
1203
1279
  stepTitle:
1204
1280
  "Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
1205
1281
  stepData: "",
1206
- expectedResult: "* Upsell SS screen is displayed",
1282
+ expectedResult: "Upsell SS screen is displayed",
1207
1283
  listStyle: "bullet",
1208
1284
  },
1209
1285
  children: [],
@@ -1213,7 +1289,7 @@ describe("markdownToBlocks", () => {
1213
1289
  props: {
1214
1290
  stepTitle: "Close SS",
1215
1291
  stepData: "",
1216
- expectedResult: "* My Course and More tab are displayed",
1292
+ expectedResult: "My Course and More tab are displayed",
1217
1293
  listStyle: "bullet",
1218
1294
  },
1219
1295
  children: [],
@@ -1733,7 +1809,7 @@ describe("markdownToBlocks", () => {
1733
1809
  props: {
1734
1810
  stepTitle: "Check UI of Sleep score info screen",
1735
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.",
1736
- expectedResult: "* - 1st block:\n* - 2nd block:\n* - 3d block:",
1812
+ expectedResult: "- 1st block:\n- 2nd block:\n- 3d block:",
1737
1813
  listStyle: "bullet",
1738
1814
  },
1739
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");
@@ -964,10 +971,12 @@ function parseTestStep(
964
971
  let expectedResult = "";
965
972
  let next = index + 1;
966
973
  let inExpectedResult = false;
974
+ const stepIndent = current.length - current.trimStart().length;
967
975
 
968
976
  while (next < lines.length) {
969
977
  const line = lines[next];
970
- const hasIndent = /^\s{2,}/.test(line);
978
+ const lineIndent = line.length - line.trimStart().length;
979
+ const hasIndent = lineIndent > stepIndent;
971
980
  const rawTrimmed = line.trim();
972
981
 
973
982
  if (!rawTrimmed) {
@@ -1002,12 +1011,18 @@ function parseTestStep(
1002
1011
  // Check for expected result labels with different formatting
1003
1012
  const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
1004
1013
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
1005
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
1014
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
1015
+ rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
1006
1016
 
1007
1017
  if (expectedMatch || expectedStarMatch) {
1008
1018
  inExpectedResult = true;
1009
- const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
1010
- 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
+ }
1011
1026
 
1012
1027
  // Add the content (if any) from this line
1013
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);