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.
- package/package/editor/blocks/step.js +22 -4
- package/package/editor/blocks/stepHorizontalView.d.ts +2 -2
- package/package/editor/blocks/stepHorizontalView.js +4 -3
- package/package/editor/customMarkdownConverter.js +29 -10
- package/package/styles.css +5 -5
- package/package.json +1 -1
- package/src/editor/blocks/step.tsx +43 -37
- package/src/editor/blocks/stepHorizontalView.tsx +5 -5
- package/src/editor/customMarkdownConverter.test.ts +82 -6
- package/src/editor/customMarkdownConverter.ts +23 -8
- package/src/editor/styles.css +5 -5
|
@@ -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
|
-
|
|
341
|
-
|
|
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" }),
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
833
|
-
let content
|
|
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);
|
package/package/styles.css
CHANGED
|
@@ -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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
1026
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1010
|
-
let content
|
|
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) {
|
package/src/editor/styles.css
CHANGED
|
@@ -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);
|