testomatio-editor-blocks 0.4.19 → 0.4.21
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.d.ts +1 -0
- package/package/editor/blocks/step.js +10 -2
- package/package/editor/customMarkdownConverter.js +30 -22
- package/package.json +1 -1
- package/src/editor/blocks/blocks.test.ts +36 -0
- package/src/editor/blocks/step.tsx +11 -2
- package/src/editor/customMarkdownConverter.test.ts +54 -78
- package/src/editor/customMarkdownConverter.ts +25 -19
|
@@ -43,6 +43,10 @@ const writeStepViewMode = (mode) => {
|
|
|
43
43
|
//
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
+
export const isEmptyParagraph = (b) => b.type === "paragraph" &&
|
|
47
|
+
(!Array.isArray(b.content) ||
|
|
48
|
+
b.content.length === 0 ||
|
|
49
|
+
b.content.every((n) => { var _a; return n.type === "text" && !((_a = n.text) === null || _a === void 0 ? void 0 : _a.trim()); }));
|
|
46
50
|
export const stepBlock = createReactBlockSpec({
|
|
47
51
|
type: "testStep",
|
|
48
52
|
content: "none",
|
|
@@ -82,9 +86,13 @@ export const stepBlock = createReactBlockSpec({
|
|
|
82
86
|
return 1;
|
|
83
87
|
let count = 1;
|
|
84
88
|
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
85
|
-
|
|
89
|
+
const b = allBlocks[i];
|
|
90
|
+
if (b.type === "testStep") {
|
|
86
91
|
count++;
|
|
87
92
|
}
|
|
93
|
+
else if (isEmptyParagraph(b)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
88
96
|
else {
|
|
89
97
|
break;
|
|
90
98
|
}
|
|
@@ -99,7 +107,7 @@ export const stepBlock = createReactBlockSpec({
|
|
|
99
107
|
return false;
|
|
100
108
|
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
101
109
|
const b = allBlocks[i];
|
|
102
|
-
if (b.type === "testStep" || b.type === "snippet") {
|
|
110
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
103
111
|
continue;
|
|
104
112
|
}
|
|
105
113
|
if (b.type === "heading") {
|
|
@@ -237,7 +237,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
237
237
|
}
|
|
238
238
|
case "bulletListItem": {
|
|
239
239
|
const text = inlineToMarkdown(block.content);
|
|
240
|
-
lines.push(`${indent}
|
|
240
|
+
lines.push(`${indent}* ${text}`.trimEnd());
|
|
241
241
|
lines.push(...serializeChildren(block, ctx));
|
|
242
242
|
return lines;
|
|
243
243
|
}
|
|
@@ -624,13 +624,11 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
624
624
|
break;
|
|
625
625
|
}
|
|
626
626
|
// Only try to parse as testStep for top-level items (indentLevel === 0)
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
if (indentLevel === 0 && (allowEmptySteps ||
|
|
630
|
-
// For bullet lists, always try to parse as test steps
|
|
631
|
-
// For numbered lists, only try if they have step-like characteristics
|
|
627
|
+
// Under a Steps heading (allowEmptySteps=true): always try for both bullet and numbered
|
|
628
|
+
// Outside Steps heading: only if the item looks like a test step (has Expected markers or indented data)
|
|
629
|
+
if (indentLevel === 0 && (allowEmptySteps || isLikelyStep(lines, index))) {
|
|
632
630
|
const looksLikeTestStep = listType === "bullet" ||
|
|
633
|
-
(listType === "numbered" && (isLikelyStep(lines, index)));
|
|
631
|
+
(listType === "numbered" && (allowEmptySteps || isLikelyStep(lines, index)));
|
|
634
632
|
if (looksLikeTestStep) {
|
|
635
633
|
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
636
634
|
if (nextStep) {
|
|
@@ -678,20 +676,28 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
678
676
|
}
|
|
679
677
|
function isLikelyStep(lines, index) {
|
|
680
678
|
// Look ahead to see if there's indented content or expected result
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
679
|
+
// Look ahead through subsequent lines for expected result markers or indented content
|
|
680
|
+
for (let i = index + 1; i < lines.length; i++) {
|
|
681
|
+
const line = lines[i];
|
|
682
|
+
const trimmed = line.trim();
|
|
683
|
+
// Stop at blank lines
|
|
684
|
+
if (!trimmed)
|
|
685
|
+
break;
|
|
686
|
+
// Check for indented content (step data) first — indented lines indicate a test step
|
|
687
|
+
const hasIndent = /^\s{2,}/.test(line);
|
|
688
|
+
if (hasIndent)
|
|
689
|
+
return true;
|
|
690
|
+
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
691
|
+
if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
692
|
+
break;
|
|
693
|
+
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::"))
|
|
694
|
+
break;
|
|
695
|
+
// Check for expected result markers
|
|
696
|
+
if (EXPECTED_LABEL_REGEX.test(trimmed))
|
|
697
|
+
return true;
|
|
698
|
+
if (trimmed.match(/^\*[^*]*expected/i))
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
695
701
|
return false;
|
|
696
702
|
}
|
|
697
703
|
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
@@ -1145,7 +1151,9 @@ export function markdownToBlocks(markdown) {
|
|
|
1145
1151
|
index = snippetWrapper.nextIndex;
|
|
1146
1152
|
continue;
|
|
1147
1153
|
}
|
|
1148
|
-
const stepLikeBlock =
|
|
1154
|
+
const stepLikeBlock = (stepsHeadingLevel !== null || isLikelyStep(lines, index))
|
|
1155
|
+
? parseTestStep(lines, index, stepsHeadingLevel !== null)
|
|
1156
|
+
: null;
|
|
1149
1157
|
if (stepLikeBlock) {
|
|
1150
1158
|
blocks.push(stepLikeBlock.block);
|
|
1151
1159
|
index = stepLikeBlock.nextIndex;
|
package/package.json
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { customSchema } from "../customSchema";
|
|
3
|
+
import { isEmptyParagraph } from "./step";
|
|
4
|
+
|
|
5
|
+
describe("isEmptyParagraph", () => {
|
|
6
|
+
it("returns true for paragraph with no content", () => {
|
|
7
|
+
expect(isEmptyParagraph({ type: "paragraph", content: [], children: [] })).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns true for paragraph with undefined content", () => {
|
|
11
|
+
expect(isEmptyParagraph({ type: "paragraph", children: [] })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns true for paragraph with only whitespace text", () => {
|
|
15
|
+
expect(
|
|
16
|
+
isEmptyParagraph({
|
|
17
|
+
type: "paragraph",
|
|
18
|
+
content: [{ type: "text", text: " " }],
|
|
19
|
+
children: [],
|
|
20
|
+
}),
|
|
21
|
+
).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns false for paragraph with text content", () => {
|
|
25
|
+
expect(
|
|
26
|
+
isEmptyParagraph({
|
|
27
|
+
type: "paragraph",
|
|
28
|
+
content: [{ type: "text", text: "hello" }],
|
|
29
|
+
children: [],
|
|
30
|
+
}),
|
|
31
|
+
).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns false for non-paragraph blocks", () => {
|
|
35
|
+
expect(isEmptyParagraph({ type: "heading", content: [], children: [] })).toBe(false);
|
|
36
|
+
expect(isEmptyParagraph({ type: "testStep", content: [], children: [] })).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
3
39
|
|
|
4
40
|
describe("custom block specs", () => {
|
|
5
41
|
it("registers the step block", () => {
|
|
@@ -47,6 +47,12 @@ const writeStepViewMode = (mode: StepViewMode) => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
export const isEmptyParagraph = (b: any): boolean =>
|
|
51
|
+
b.type === "paragraph" &&
|
|
52
|
+
(!Array.isArray(b.content) ||
|
|
53
|
+
b.content.length === 0 ||
|
|
54
|
+
b.content.every((n: any) => n.type === "text" && !n.text?.trim()));
|
|
55
|
+
|
|
50
56
|
export const stepBlock = createReactBlockSpec(
|
|
51
57
|
{
|
|
52
58
|
type: "testStep",
|
|
@@ -91,8 +97,11 @@ export const stepBlock = createReactBlockSpec(
|
|
|
91
97
|
|
|
92
98
|
let count = 1;
|
|
93
99
|
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
94
|
-
|
|
100
|
+
const b = allBlocks[i];
|
|
101
|
+
if (b.type === "testStep") {
|
|
95
102
|
count++;
|
|
103
|
+
} else if (isEmptyParagraph(b)) {
|
|
104
|
+
continue;
|
|
96
105
|
} else {
|
|
97
106
|
break;
|
|
98
107
|
}
|
|
@@ -108,7 +117,7 @@ export const stepBlock = createReactBlockSpec(
|
|
|
108
117
|
|
|
109
118
|
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
110
119
|
const b = allBlocks[i];
|
|
111
|
-
if (b.type === "testStep" || b.type === "snippet") {
|
|
120
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
112
121
|
continue;
|
|
113
122
|
}
|
|
114
123
|
if (b.type === "heading") {
|
|
@@ -485,37 +485,9 @@ describe("markdownToBlocks", () => {
|
|
|
485
485
|
const blocks = markdownToBlocks(markdown);
|
|
486
486
|
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
487
487
|
|
|
488
|
+
// Preconditions and Postconditions bullets are now parsed as bulletListItem (not testStep)
|
|
489
|
+
// Only items under the "Steps" heading with Expected markers remain as testSteps
|
|
488
490
|
expect(stepBlocks).toEqual([
|
|
489
|
-
{
|
|
490
|
-
type: "testStep",
|
|
491
|
-
props: {
|
|
492
|
-
stepTitle: "The user is logged into the application.",
|
|
493
|
-
stepData: "",
|
|
494
|
-
expectedResult: "",
|
|
495
|
-
listStyle: "bullet",
|
|
496
|
-
},
|
|
497
|
-
children: [],
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
type: "testStep",
|
|
501
|
-
props: {
|
|
502
|
-
stepTitle: "The user has the necessary permissions to receive notifications.",
|
|
503
|
-
stepData: "",
|
|
504
|
-
expectedResult: "",
|
|
505
|
-
listStyle: "bullet",
|
|
506
|
-
},
|
|
507
|
-
children: [],
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
type: "testStep",
|
|
511
|
-
props: {
|
|
512
|
-
stepTitle: "The application is configured to send real-time notifications.",
|
|
513
|
-
stepData: "",
|
|
514
|
-
expectedResult: "",
|
|
515
|
-
listStyle: "bullet",
|
|
516
|
-
},
|
|
517
|
-
children: [],
|
|
518
|
-
},
|
|
519
491
|
{
|
|
520
492
|
type: "testStep",
|
|
521
493
|
props: {
|
|
@@ -556,26 +528,6 @@ describe("markdownToBlocks", () => {
|
|
|
556
528
|
},
|
|
557
529
|
children: [],
|
|
558
530
|
},
|
|
559
|
-
{
|
|
560
|
-
type: "testStep",
|
|
561
|
-
props: {
|
|
562
|
-
stepTitle: "The user has received and viewed the notifications.",
|
|
563
|
-
stepData: "",
|
|
564
|
-
expectedResult: "",
|
|
565
|
-
listStyle: "bullet",
|
|
566
|
-
},
|
|
567
|
-
children: [],
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
type: "testStep",
|
|
571
|
-
props: {
|
|
572
|
-
stepTitle: "The application continues to function as expected after receiving and processing the notifications.",
|
|
573
|
-
stepData: "",
|
|
574
|
-
expectedResult: "",
|
|
575
|
-
listStyle: "bullet",
|
|
576
|
-
},
|
|
577
|
-
children: [],
|
|
578
|
-
},
|
|
579
531
|
]);
|
|
580
532
|
});
|
|
581
533
|
|
|
@@ -674,33 +626,21 @@ describe("markdownToBlocks", () => {
|
|
|
674
626
|
children: [],
|
|
675
627
|
},
|
|
676
628
|
{
|
|
677
|
-
type: "
|
|
678
|
-
props:
|
|
679
|
-
|
|
680
|
-
stepData: "",
|
|
681
|
-
expectedResult: "",
|
|
682
|
-
listStyle: "bullet",
|
|
683
|
-
},
|
|
629
|
+
type: "bulletListItem",
|
|
630
|
+
props: baseProps,
|
|
631
|
+
content: [{ type: "text", text: "The user is logged into the application.", styles: {} }],
|
|
684
632
|
children: [],
|
|
685
633
|
},
|
|
686
634
|
{
|
|
687
|
-
type: "
|
|
688
|
-
props:
|
|
689
|
-
|
|
690
|
-
stepData: "",
|
|
691
|
-
expectedResult: "",
|
|
692
|
-
listStyle: "bullet",
|
|
693
|
-
},
|
|
635
|
+
type: "bulletListItem",
|
|
636
|
+
props: baseProps,
|
|
637
|
+
content: [{ type: "text", text: "The user has the necessary permissions to receive notifications.", styles: {} }],
|
|
694
638
|
children: [],
|
|
695
639
|
},
|
|
696
640
|
{
|
|
697
|
-
type: "
|
|
698
|
-
props:
|
|
699
|
-
|
|
700
|
-
stepData: "",
|
|
701
|
-
expectedResult: "",
|
|
702
|
-
listStyle: "bullet",
|
|
703
|
-
},
|
|
641
|
+
type: "bulletListItem",
|
|
642
|
+
props: baseProps,
|
|
643
|
+
content: [{ type: "text", text: "The application is configured to send real-time notifications.", styles: {} }],
|
|
704
644
|
children: [],
|
|
705
645
|
},
|
|
706
646
|
]);
|
|
@@ -980,6 +920,46 @@ describe("markdownToBlocks", () => {
|
|
|
980
920
|
);
|
|
981
921
|
});
|
|
982
922
|
|
|
923
|
+
it("parses multiple steps separated by blank lines", () => {
|
|
924
|
+
const markdown = [
|
|
925
|
+
"## Steps",
|
|
926
|
+
"* asjkldnkasndkj",
|
|
927
|
+
" *Expected*: slkednfkjsdnfkjsdnf",
|
|
928
|
+
"",
|
|
929
|
+
"* sdfnsikdfnsikdn",
|
|
930
|
+
" *Expected*: sedfnskdijfns",
|
|
931
|
+
"",
|
|
932
|
+
"* sdfnksdjfnsdknf",
|
|
933
|
+
" *Expected*: sdjfnskdjfnskdfn",
|
|
934
|
+
].join("\n");
|
|
935
|
+
|
|
936
|
+
const blocks = markdownToBlocks(markdown);
|
|
937
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
938
|
+
|
|
939
|
+
expect(stepBlocks).toHaveLength(3);
|
|
940
|
+
|
|
941
|
+
expect(stepBlocks[0].props).toEqual({
|
|
942
|
+
stepTitle: "asjkldnkasndkj",
|
|
943
|
+
stepData: "",
|
|
944
|
+
expectedResult: "slkednfkjsdnfkjsdnf\n",
|
|
945
|
+
listStyle: "bullet",
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
expect(stepBlocks[1].props).toEqual({
|
|
949
|
+
stepTitle: "sdfnsikdfnsikdn",
|
|
950
|
+
stepData: "",
|
|
951
|
+
expectedResult: "sedfnskdijfns\n",
|
|
952
|
+
listStyle: "bullet",
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(stepBlocks[2].props).toEqual({
|
|
956
|
+
stepTitle: "sdfnksdjfnsdknf",
|
|
957
|
+
stepData: "",
|
|
958
|
+
expectedResult: "sdjfnskdjfnskdfn",
|
|
959
|
+
listStyle: "bullet",
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
983
963
|
it("round-trips simple blocks", () => {
|
|
984
964
|
const blocks: CustomEditorBlock[] = [
|
|
985
965
|
{
|
|
@@ -1008,13 +988,9 @@ describe("markdownToBlocks", () => {
|
|
|
1008
988
|
children: [],
|
|
1009
989
|
},
|
|
1010
990
|
{
|
|
1011
|
-
type: "
|
|
1012
|
-
props:
|
|
1013
|
-
|
|
1014
|
-
stepData: "",
|
|
1015
|
-
expectedResult: "",
|
|
1016
|
-
listStyle: "bullet",
|
|
1017
|
-
},
|
|
991
|
+
type: "bulletListItem",
|
|
992
|
+
props: baseProps,
|
|
993
|
+
content: [{ type: "text", text: "Bullet", styles: {} }],
|
|
1018
994
|
children: [],
|
|
1019
995
|
},
|
|
1020
996
|
]);
|
|
@@ -314,7 +314,7 @@ function serializeBlock(
|
|
|
314
314
|
}
|
|
315
315
|
case "bulletListItem": {
|
|
316
316
|
const text = inlineToMarkdown(block.content);
|
|
317
|
-
lines.push(`${indent}
|
|
317
|
+
lines.push(`${indent}* ${text}`.trimEnd());
|
|
318
318
|
lines.push(...serializeChildren(block, ctx));
|
|
319
319
|
return lines;
|
|
320
320
|
}
|
|
@@ -777,14 +777,12 @@ function parseList(
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// Only try to parse as testStep for top-level items (indentLevel === 0)
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
if (indentLevel === 0 && (allowEmptySteps ||
|
|
783
|
-
// For bullet lists, always try to parse as test steps
|
|
784
|
-
// For numbered lists, only try if they have step-like characteristics
|
|
780
|
+
// Under a Steps heading (allowEmptySteps=true): always try for both bullet and numbered
|
|
781
|
+
// Outside Steps heading: only if the item looks like a test step (has Expected markers or indented data)
|
|
782
|
+
if (indentLevel === 0 && (allowEmptySteps || isLikelyStep(lines, index))) {
|
|
785
783
|
const looksLikeTestStep = listType === "bullet" ||
|
|
786
784
|
(listType === "numbered" && (
|
|
787
|
-
isLikelyStep(lines, index)
|
|
785
|
+
allowEmptySteps || isLikelyStep(lines, index)
|
|
788
786
|
));
|
|
789
787
|
|
|
790
788
|
if (looksLikeTestStep) {
|
|
@@ -836,20 +834,26 @@ function parseList(
|
|
|
836
834
|
|
|
837
835
|
function isLikelyStep(lines: string[], index: number): boolean {
|
|
838
836
|
// Look ahead to see if there's indented content or expected result
|
|
839
|
-
|
|
837
|
+
// Look ahead through subsequent lines for expected result markers or indented content
|
|
838
|
+
for (let i = index + 1; i < lines.length; i++) {
|
|
839
|
+
const line = lines[i];
|
|
840
|
+
const trimmed = line.trim();
|
|
841
|
+
|
|
842
|
+
// Stop at blank lines
|
|
843
|
+
if (!trimmed) break;
|
|
840
844
|
|
|
841
|
-
|
|
842
|
-
|
|
845
|
+
// Check for indented content (step data) first — indented lines indicate a test step
|
|
846
|
+
const hasIndent = /^\s{2,}/.test(line);
|
|
847
|
+
if (hasIndent) return true;
|
|
843
848
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
849
|
+
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
850
|
+
if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
|
|
851
|
+
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::")) break;
|
|
847
852
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed)) return true;
|
|
853
|
+
// Check for expected result markers
|
|
854
|
+
if (EXPECTED_LABEL_REGEX.test(trimmed)) return true;
|
|
855
|
+
if (trimmed.match(/^\*[^*]*expected/i)) return true;
|
|
856
|
+
}
|
|
853
857
|
|
|
854
858
|
return false;
|
|
855
859
|
}
|
|
@@ -1377,7 +1381,9 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1377
1381
|
continue;
|
|
1378
1382
|
}
|
|
1379
1383
|
|
|
1380
|
-
const stepLikeBlock =
|
|
1384
|
+
const stepLikeBlock = (stepsHeadingLevel !== null || isLikelyStep(lines, index))
|
|
1385
|
+
? parseTestStep(lines, index, stepsHeadingLevel !== null)
|
|
1386
|
+
: null;
|
|
1381
1387
|
if (stepLikeBlock) {
|
|
1382
1388
|
blocks.push(stepLikeBlock.block);
|
|
1383
1389
|
index = stepLikeBlock.nextIndex;
|