testomatio-editor-blocks 0.4.47 → 0.4.49
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 +11 -1
- package/package/editor/blocks/step.js +56 -0
- package/package/editor/blocks/stepField.js +2 -0
- package/package/editor/customMarkdownConverter.js +14 -35
- package/package/index.d.ts +1 -1
- package/package/index.js +1 -1
- package/package/styles.css +5 -0
- package/package.json +1 -1
- package/src/App.tsx +27 -48
- package/src/editor/blocks/step.tsx +61 -1
- package/src/editor/blocks/stepField.tsx +4 -0
- package/src/editor/customMarkdownConverter.test.ts +133 -15
- package/src/editor/customMarkdownConverter.ts +15 -36
- package/src/editor/markdownToBlocks.test.ts +15 -3
- package/src/editor/styles.css +5 -0
- package/src/index.ts +1 -1
|
@@ -22,7 +22,17 @@ export declare function canInsertStepOrSnippet(editor: {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function addStepsBlock(editor: {
|
|
24
24
|
document: any[];
|
|
25
|
-
insertBlocks: (blocks: any[], referenceId: string, placement:
|
|
25
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
26
|
+
}): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Programmatically add an empty snippet block to the editor.
|
|
29
|
+
* - If a "Steps" heading exists, inserts after the last step/snippet under it.
|
|
30
|
+
* - Otherwise, appends a "Steps" heading + empty snippet at the end.
|
|
31
|
+
* Returns the inserted snippet's block ID (for focusing), or null.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addSnippetBlock(editor: {
|
|
34
|
+
document: any[];
|
|
35
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
26
36
|
}): string | null;
|
|
27
37
|
export declare const stepBlock: {
|
|
28
38
|
config: {
|
|
@@ -142,6 +142,62 @@ export function addStepsBlock(editor) {
|
|
|
142
142
|
const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
|
|
143
143
|
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Programmatically add an empty snippet block to the editor.
|
|
147
|
+
* - If a "Steps" heading exists, inserts after the last step/snippet under it.
|
|
148
|
+
* - Otherwise, appends a "Steps" heading + empty snippet at the end.
|
|
149
|
+
* Returns the inserted snippet's block ID (for focusing), or null.
|
|
150
|
+
*/
|
|
151
|
+
export function addSnippetBlock(editor) {
|
|
152
|
+
var _a, _b, _c, _d;
|
|
153
|
+
const allBlocks = editor.document;
|
|
154
|
+
const emptySnippet = {
|
|
155
|
+
type: "snippet",
|
|
156
|
+
props: { snippetId: "", snippetTitle: "", snippetData: "", snippetExpectedResult: "" },
|
|
157
|
+
children: [],
|
|
158
|
+
};
|
|
159
|
+
let stepsHeadingIndex = -1;
|
|
160
|
+
for (let i = 0; i < allBlocks.length; i++) {
|
|
161
|
+
const b = allBlocks[i];
|
|
162
|
+
if (b.type !== "heading")
|
|
163
|
+
continue;
|
|
164
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
165
|
+
.filter((n) => n.type === "text")
|
|
166
|
+
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
167
|
+
.join("")
|
|
168
|
+
.trim()
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/[:\-–—]$/, "");
|
|
171
|
+
if (isStepsHeading(text)) {
|
|
172
|
+
stepsHeadingIndex = i;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (stepsHeadingIndex >= 0) {
|
|
177
|
+
let lastIndex = stepsHeadingIndex;
|
|
178
|
+
for (let i = stepsHeadingIndex + 1; i < allBlocks.length; i++) {
|
|
179
|
+
const b = allBlocks[i];
|
|
180
|
+
if (b.type === "testStep" || b.type === "snippet") {
|
|
181
|
+
lastIndex = i;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isEmptyParagraph(b))
|
|
185
|
+
continue;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const inserted = editor.insertBlocks([emptySnippet], allBlocks[lastIndex].id, "after");
|
|
189
|
+
return (_b = (_a = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
|
|
190
|
+
}
|
|
191
|
+
const lastBlock = allBlocks[allBlocks.length - 1];
|
|
192
|
+
const stepsHeading = {
|
|
193
|
+
type: "heading",
|
|
194
|
+
props: { level: 3 },
|
|
195
|
+
content: [{ type: "text", text: "Steps" }],
|
|
196
|
+
children: [],
|
|
197
|
+
};
|
|
198
|
+
const inserted = editor.insertBlocks([stepsHeading, emptySnippet], lastBlock.id, "after");
|
|
199
|
+
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
200
|
+
}
|
|
145
201
|
export const stepBlock = createReactBlockSpec({
|
|
146
202
|
type: "testStep",
|
|
147
203
|
content: "none",
|
|
@@ -880,6 +880,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
880
880
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
881
881
|
}
|
|
882
882
|
else if (start !== end) {
|
|
883
|
+
// Remove overlapping formatting of other types before applying new format
|
|
884
|
+
formattingRef.current = formattingRef.current.filter((f) => f.type === fmtType || f.start >= end || f.end <= start);
|
|
883
885
|
// Add formatting for selection
|
|
884
886
|
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
885
887
|
}
|
|
@@ -685,12 +685,9 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
685
685
|
if (detectedType !== listType) {
|
|
686
686
|
break;
|
|
687
687
|
}
|
|
688
|
-
// Only try to parse as testStep for top-level items
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (indentLevel === 0 && (allowEmptySteps || isLikelyStep(lines, index))) {
|
|
692
|
-
const looksLikeTestStep = listType === "bullet" ||
|
|
693
|
-
(listType === "numbered" && (allowEmptySteps || isLikelyStep(lines, index)));
|
|
688
|
+
// Only try to parse as testStep for top-level items under a Steps heading
|
|
689
|
+
if (indentLevel === 0 && allowEmptySteps) {
|
|
690
|
+
const looksLikeTestStep = listType === "bullet" || listType === "numbered";
|
|
694
691
|
if (looksLikeTestStep) {
|
|
695
692
|
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
696
693
|
if (nextStep) {
|
|
@@ -736,32 +733,6 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
736
733
|
}
|
|
737
734
|
return { items, nextIndex: index };
|
|
738
735
|
}
|
|
739
|
-
function isLikelyStep(lines, index) {
|
|
740
|
-
// Look ahead to see if there's indented content or expected result
|
|
741
|
-
// Look ahead through subsequent lines for expected result markers or indented content
|
|
742
|
-
for (let i = index + 1; i < lines.length; i++) {
|
|
743
|
-
const line = lines[i];
|
|
744
|
-
const trimmed = line.trim();
|
|
745
|
-
// Stop at blank lines
|
|
746
|
-
if (!trimmed)
|
|
747
|
-
break;
|
|
748
|
-
// Check for indented content (step data) first — indented lines indicate a test step
|
|
749
|
-
const hasIndent = /^\s{2,}/.test(line);
|
|
750
|
-
if (hasIndent)
|
|
751
|
-
return true;
|
|
752
|
-
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
753
|
-
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
754
|
-
break;
|
|
755
|
-
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::"))
|
|
756
|
-
break;
|
|
757
|
-
// Check for expected result markers
|
|
758
|
-
if (EXPECTED_LABEL_REGEX.test(trimmed))
|
|
759
|
-
return true;
|
|
760
|
-
if (trimmed.match(/^\*[^*]*expected/i))
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
765
736
|
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
766
737
|
const current = lines[index];
|
|
767
738
|
const trimmed = current.trim();
|
|
@@ -805,6 +776,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
805
776
|
let expectedResult = "";
|
|
806
777
|
let next = index + 1;
|
|
807
778
|
let inExpectedResult = false;
|
|
779
|
+
let blankLineSeenOutsideCodeBlock = false;
|
|
808
780
|
const stepIndent = current.length - current.trimStart().length;
|
|
809
781
|
while (next < lines.length) {
|
|
810
782
|
const line = lines[next];
|
|
@@ -818,6 +790,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
818
790
|
}
|
|
819
791
|
else {
|
|
820
792
|
stepDataLines.push("");
|
|
793
|
+
blankLineSeenOutsideCodeBlock = true;
|
|
821
794
|
}
|
|
822
795
|
}
|
|
823
796
|
next += 1;
|
|
@@ -923,6 +896,10 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
923
896
|
next += 1;
|
|
924
897
|
continue;
|
|
925
898
|
}
|
|
899
|
+
// After a blank line outside a code block, stop adding to step data
|
|
900
|
+
if (blankLineSeenOutsideCodeBlock) {
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
926
903
|
if (STEP_DATA_LINE_REGEX.test(rawTrimmed)) {
|
|
927
904
|
const content = unescapeMarkdown(rawTrimmed);
|
|
928
905
|
stepDataLines.push(content);
|
|
@@ -1225,14 +1202,16 @@ export function markdownToBlocks(markdown, options) {
|
|
|
1225
1202
|
}
|
|
1226
1203
|
continue;
|
|
1227
1204
|
}
|
|
1228
|
-
const snippetWrapper =
|
|
1205
|
+
const snippetWrapper = stepsHeadingLevel !== null
|
|
1206
|
+
? parseSnippetWrapper(lines, index)
|
|
1207
|
+
: null;
|
|
1229
1208
|
if (snippetWrapper) {
|
|
1230
1209
|
blocks.push(snippetWrapper.block);
|
|
1231
1210
|
index = snippetWrapper.nextIndex;
|
|
1232
1211
|
continue;
|
|
1233
1212
|
}
|
|
1234
|
-
const stepLikeBlock =
|
|
1235
|
-
? parseTestStep(lines, index,
|
|
1213
|
+
const stepLikeBlock = stepsHeadingLevel !== null
|
|
1214
|
+
? parseTestStep(lines, index, true)
|
|
1236
1215
|
: null;
|
|
1237
1216
|
if (stepLikeBlock) {
|
|
1238
1217
|
blocks.push(stepLikeBlock.block);
|
package/package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
package/package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
package/package/styles.css
CHANGED
|
@@ -1339,6 +1339,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
1339
1339
|
pointer-events: none;
|
|
1340
1340
|
}
|
|
1341
1341
|
|
|
1342
|
+
.bn-suggestion-menu .mantine-Badge-label {
|
|
1343
|
+
text-transform: none;
|
|
1344
|
+
font-size: var(--badge-fz-sm);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1342
1347
|
.bn-suggestion-icon {
|
|
1343
1348
|
display: inline-flex;
|
|
1344
1349
|
align-items: center;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
blocksToMarkdown,
|
|
16
16
|
markdownToBlocks,
|
|
17
17
|
type CustomEditorBlock,
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
} from "./editor/customMarkdownConverter";
|
|
20
20
|
import { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
21
21
|
import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
22
22
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
23
23
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
24
24
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
25
|
+
import { canInsertStepOrSnippet, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
25
26
|
import "./App.css";
|
|
26
27
|
|
|
27
28
|
const focusStepField = (
|
|
@@ -291,7 +292,18 @@ function CustomSlashMenu() {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
const getItems = async (query: string) => {
|
|
294
|
-
const
|
|
295
|
+
const isMac =
|
|
296
|
+
typeof navigator !== "undefined" &&
|
|
297
|
+
(/Mac/.test(navigator.platform) ||
|
|
298
|
+
(/AppleWebKit/.test(navigator.userAgent) &&
|
|
299
|
+
/Mobile\/\w+/.test(navigator.userAgent)));
|
|
300
|
+
|
|
301
|
+
const defaultItems = getDefaultReactSlashMenuItems(editor).map((item) => {
|
|
302
|
+
if (item.badge && isMac) {
|
|
303
|
+
return { ...item, badge: item.badge.replace("Alt", "Option") };
|
|
304
|
+
}
|
|
305
|
+
return item;
|
|
306
|
+
});
|
|
295
307
|
|
|
296
308
|
const stepItem = {
|
|
297
309
|
key: "test_step" as any,
|
|
@@ -334,7 +346,12 @@ function CustomSlashMenu() {
|
|
|
334
346
|
},
|
|
335
347
|
};
|
|
336
348
|
|
|
337
|
-
|
|
349
|
+
const currentBlock = editor.getTextCursorPosition().block;
|
|
350
|
+
const canInsert = canInsertStepOrSnippet(editor, currentBlock.id);
|
|
351
|
+
const items = canInsert
|
|
352
|
+
? [...defaultItems, stepItem, snippetItem]
|
|
353
|
+
: defaultItems;
|
|
354
|
+
return filterSuggestionItems(items, query);
|
|
338
355
|
};
|
|
339
356
|
|
|
340
357
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -451,52 +468,14 @@ function App() {
|
|
|
451
468
|
};
|
|
452
469
|
}, [editor, uploadStepImage]);
|
|
453
470
|
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
},
|
|
462
|
-
children: [],
|
|
463
|
-
});
|
|
464
|
-
}, []);
|
|
465
|
-
|
|
466
|
-
const createSnippetBlock = useMemo<() => CustomPartialBlock>(() => {
|
|
467
|
-
return () => ({
|
|
468
|
-
type: "snippet",
|
|
469
|
-
props: {
|
|
470
|
-
snippetId: "",
|
|
471
|
-
snippetTitle: "",
|
|
472
|
-
snippetData: "",
|
|
473
|
-
snippetExpectedResult: "",
|
|
474
|
-
},
|
|
475
|
-
children: [],
|
|
476
|
-
});
|
|
477
|
-
}, []);
|
|
478
|
-
|
|
479
|
-
const insertBlockAfterSelection = (createBlock: () => CustomPartialBlock, focusFieldName?: string) => {
|
|
480
|
-
const selection = editor.getSelection();
|
|
481
|
-
const selectedBlocks = selection?.blocks ?? [];
|
|
482
|
-
const selectedBlock = selectedBlocks[selectedBlocks.length - 1];
|
|
483
|
-
const documentBlocks = editor.document;
|
|
484
|
-
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
485
|
-
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
486
|
-
|
|
487
|
-
if (!referenceId) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const inserted = editor.insertBlocks([createBlock()], referenceId, "after");
|
|
492
|
-
const firstInserted = inserted[0];
|
|
493
|
-
if (firstInserted && focusFieldName) {
|
|
494
|
-
focusStepField(editor, firstInserted.id, focusFieldName);
|
|
495
|
-
}
|
|
471
|
+
const insertTestStep = () => {
|
|
472
|
+
const id = addStepsBlock(editor);
|
|
473
|
+
if (id) focusStepField(editor, id, "title");
|
|
474
|
+
};
|
|
475
|
+
const insertSnippet = () => {
|
|
476
|
+
const id = addSnippetBlock(editor);
|
|
477
|
+
if (id) focusStepField(editor, id, "snippet-title");
|
|
496
478
|
};
|
|
497
|
-
|
|
498
|
-
const insertTestStep = () => insertBlockAfterSelection(createTestStepBlock, "title");
|
|
499
|
-
const insertSnippet = () => insertBlockAfterSelection(createSnippetBlock, "snippet-title");
|
|
500
479
|
|
|
501
480
|
const handleCopyMarkdown = async () => {
|
|
502
481
|
if (conversionError) {
|
|
@@ -105,7 +105,7 @@ export function canInsertStepOrSnippet(
|
|
|
105
105
|
*/
|
|
106
106
|
export function addStepsBlock(editor: {
|
|
107
107
|
document: any[];
|
|
108
|
-
insertBlocks: (blocks: any[], referenceId: string, placement:
|
|
108
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
109
109
|
}): string | null {
|
|
110
110
|
const allBlocks = editor.document;
|
|
111
111
|
const emptyStep = {
|
|
@@ -157,6 +157,66 @@ export function addStepsBlock(editor: {
|
|
|
157
157
|
return inserted?.[1]?.id ?? null;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Programmatically add an empty snippet block to the editor.
|
|
162
|
+
* - If a "Steps" heading exists, inserts after the last step/snippet under it.
|
|
163
|
+
* - Otherwise, appends a "Steps" heading + empty snippet at the end.
|
|
164
|
+
* Returns the inserted snippet's block ID (for focusing), or null.
|
|
165
|
+
*/
|
|
166
|
+
export function addSnippetBlock(editor: {
|
|
167
|
+
document: any[];
|
|
168
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
169
|
+
}): string | null {
|
|
170
|
+
const allBlocks = editor.document;
|
|
171
|
+
const emptySnippet = {
|
|
172
|
+
type: "snippet" as const,
|
|
173
|
+
props: { snippetId: "", snippetTitle: "", snippetData: "", snippetExpectedResult: "" },
|
|
174
|
+
children: [],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
let stepsHeadingIndex = -1;
|
|
178
|
+
for (let i = 0; i < allBlocks.length; i++) {
|
|
179
|
+
const b = allBlocks[i];
|
|
180
|
+
if (b.type !== "heading") continue;
|
|
181
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
182
|
+
.filter((n: any) => n.type === "text")
|
|
183
|
+
.map((n: any) => n.text ?? "")
|
|
184
|
+
.join("")
|
|
185
|
+
.trim()
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.replace(/[:\-–—]$/, "");
|
|
188
|
+
if (isStepsHeading(text)) {
|
|
189
|
+
stepsHeadingIndex = i;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (stepsHeadingIndex >= 0) {
|
|
195
|
+
let lastIndex = stepsHeadingIndex;
|
|
196
|
+
for (let i = stepsHeadingIndex + 1; i < allBlocks.length; i++) {
|
|
197
|
+
const b = allBlocks[i];
|
|
198
|
+
if (b.type === "testStep" || b.type === "snippet") {
|
|
199
|
+
lastIndex = i;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (isEmptyParagraph(b)) continue;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const inserted = editor.insertBlocks([emptySnippet], allBlocks[lastIndex].id, "after");
|
|
206
|
+
return inserted?.[0]?.id ?? null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lastBlock = allBlocks[allBlocks.length - 1];
|
|
210
|
+
const stepsHeading = {
|
|
211
|
+
type: "heading" as const,
|
|
212
|
+
props: { level: 3 },
|
|
213
|
+
content: [{ type: "text" as const, text: "Steps" }],
|
|
214
|
+
children: [],
|
|
215
|
+
};
|
|
216
|
+
const inserted = editor.insertBlocks([stepsHeading, emptySnippet], lastBlock.id, "after");
|
|
217
|
+
return inserted?.[1]?.id ?? null;
|
|
218
|
+
}
|
|
219
|
+
|
|
160
220
|
export const stepBlock = createReactBlockSpec(
|
|
161
221
|
{
|
|
162
222
|
type: "testStep",
|
|
@@ -1076,6 +1076,10 @@ export function StepField({
|
|
|
1076
1076
|
// Remove formatting
|
|
1077
1077
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
1078
1078
|
} else if (start !== end) {
|
|
1079
|
+
// Remove overlapping formatting of other types before applying new format
|
|
1080
|
+
formattingRef.current = formattingRef.current.filter(
|
|
1081
|
+
(f) => f.type === fmtType || f.start >= end || f.end <= start,
|
|
1082
|
+
);
|
|
1079
1083
|
// Add formatting for selection
|
|
1080
1084
|
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
1081
1085
|
} else {
|
|
@@ -704,11 +704,15 @@ describe("markdownToBlocks", () => {
|
|
|
704
704
|
|
|
705
705
|
it("parses test steps and test cases", () => {
|
|
706
706
|
const markdown = [
|
|
707
|
+
"### Steps",
|
|
708
|
+
"",
|
|
707
709
|
"* Open the Login page.",
|
|
708
710
|
" *Expected*: The Login page loads successfully.",
|
|
709
711
|
].join("\n");
|
|
710
712
|
|
|
711
|
-
|
|
713
|
+
const blocks = markdownToBlocks(markdown);
|
|
714
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
715
|
+
expect(stepBlocks).toEqual([
|
|
712
716
|
{
|
|
713
717
|
type: "testStep",
|
|
714
718
|
props: {
|
|
@@ -723,9 +727,11 @@ describe("markdownToBlocks", () => {
|
|
|
723
727
|
});
|
|
724
728
|
|
|
725
729
|
it("parses a step with empty title but with step data", () => {
|
|
726
|
-
const markdown = ["* ", " Navigate to the page"].join("\n");
|
|
730
|
+
const markdown = ["### Steps", "", "* ", " Navigate to the page"].join("\n");
|
|
727
731
|
|
|
728
|
-
|
|
732
|
+
const blocks = markdownToBlocks(markdown);
|
|
733
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
734
|
+
expect(stepBlocks).toEqual([
|
|
729
735
|
{
|
|
730
736
|
type: "testStep",
|
|
731
737
|
props: {
|
|
@@ -741,6 +747,13 @@ describe("markdownToBlocks", () => {
|
|
|
741
747
|
|
|
742
748
|
it("round-trips a title-less step with data", () => {
|
|
743
749
|
const blocks: CustomEditorBlock[] = [
|
|
750
|
+
{
|
|
751
|
+
id: "h1",
|
|
752
|
+
type: "heading",
|
|
753
|
+
props: { level: 3, textColor: "default", backgroundColor: "default", textAlignment: "left" as const },
|
|
754
|
+
content: [{ type: "text" as const, text: "Steps", styles: {} }],
|
|
755
|
+
children: [],
|
|
756
|
+
},
|
|
744
757
|
{
|
|
745
758
|
id: "s1",
|
|
746
759
|
type: "testStep",
|
|
@@ -757,7 +770,8 @@ describe("markdownToBlocks", () => {
|
|
|
757
770
|
|
|
758
771
|
const md = blocksToMarkdown(blocks);
|
|
759
772
|
const parsed = markdownToBlocks(md);
|
|
760
|
-
|
|
773
|
+
const stepBlocks = parsed.filter((b) => b.type === "testStep");
|
|
774
|
+
expect(stepBlocks).toEqual([
|
|
761
775
|
{
|
|
762
776
|
type: "testStep",
|
|
763
777
|
props: {
|
|
@@ -773,12 +787,16 @@ describe("markdownToBlocks", () => {
|
|
|
773
787
|
|
|
774
788
|
it("parses snippet markdown into snippet blocks", () => {
|
|
775
789
|
const markdown = [
|
|
790
|
+
"### Steps",
|
|
791
|
+
"",
|
|
776
792
|
"<!-- begin snippet #501 -->",
|
|
777
793
|
"Run the seeder",
|
|
778
794
|
"<!-- end snippet #501 -->",
|
|
779
795
|
].join("\n");
|
|
780
796
|
|
|
781
|
-
|
|
797
|
+
const blocks = markdownToBlocks(markdown);
|
|
798
|
+
const snippetBlocks = blocks.filter((b) => b.type === "snippet");
|
|
799
|
+
expect(snippetBlocks).toEqual([
|
|
782
800
|
{
|
|
783
801
|
type: "snippet",
|
|
784
802
|
props: {
|
|
@@ -794,13 +812,17 @@ describe("markdownToBlocks", () => {
|
|
|
794
812
|
|
|
795
813
|
it("parses snippet markdown with space between # and ID", () => {
|
|
796
814
|
const markdown = [
|
|
815
|
+
"### Steps",
|
|
816
|
+
"",
|
|
797
817
|
"<!-- begin snippet # 22289 -->",
|
|
798
818
|
"* Fill `<Email>` with correct registered email",
|
|
799
819
|
"* Verify that the update to the target is successful",
|
|
800
820
|
"<!-- end snippet # 22289 -->",
|
|
801
821
|
].join("\n");
|
|
802
822
|
|
|
803
|
-
|
|
823
|
+
const blocks = markdownToBlocks(markdown);
|
|
824
|
+
const snippetBlocks = blocks.filter((b) => b.type === "snippet");
|
|
825
|
+
expect(snippetBlocks).toEqual([
|
|
804
826
|
{
|
|
805
827
|
type: "snippet",
|
|
806
828
|
props: {
|
|
@@ -816,6 +838,8 @@ describe("markdownToBlocks", () => {
|
|
|
816
838
|
|
|
817
839
|
it("parses snippet bodies and ignores nested snippet markers", () => {
|
|
818
840
|
const markdown = [
|
|
841
|
+
"### Steps",
|
|
842
|
+
"",
|
|
819
843
|
"<!-- begin snippet #888 -->",
|
|
820
844
|
"Prep DB",
|
|
821
845
|
"<!-- begin snippet #ignored -->",
|
|
@@ -824,7 +848,9 @@ describe("markdownToBlocks", () => {
|
|
|
824
848
|
"<!-- end snippet #888 -->",
|
|
825
849
|
].join("\n");
|
|
826
850
|
|
|
827
|
-
|
|
851
|
+
const blocks = markdownToBlocks(markdown);
|
|
852
|
+
const snippetBlocks = blocks.filter((b) => b.type === "snippet");
|
|
853
|
+
expect(snippetBlocks).toEqual([
|
|
828
854
|
{
|
|
829
855
|
type: "snippet",
|
|
830
856
|
props: {
|
|
@@ -940,6 +966,8 @@ describe("markdownToBlocks", () => {
|
|
|
940
966
|
|
|
941
967
|
it("parses step data containing code fences, blank lines, and images", () => {
|
|
942
968
|
const markdown = [
|
|
969
|
+
"### Steps",
|
|
970
|
+
"",
|
|
943
971
|
"* Step 2: Update an order status.",
|
|
944
972
|
" ```",
|
|
945
973
|
" SQL CREATE bnbmnbm mnbmb mm",
|
|
@@ -969,7 +997,9 @@ describe("markdownToBlocks", () => {
|
|
|
969
997
|
"",
|
|
970
998
|
].join("\n");
|
|
971
999
|
|
|
972
|
-
|
|
1000
|
+
const blocks = markdownToBlocks(markdown);
|
|
1001
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1002
|
+
expect(stepBlocks).toEqual([
|
|
973
1003
|
{
|
|
974
1004
|
type: "testStep",
|
|
975
1005
|
props: {
|
|
@@ -1016,6 +1046,26 @@ describe("markdownToBlocks", () => {
|
|
|
1016
1046
|
);
|
|
1017
1047
|
});
|
|
1018
1048
|
|
|
1049
|
+
it("does not include content after a blank line in step data", () => {
|
|
1050
|
+
const markdown = [
|
|
1051
|
+
"### Steps",
|
|
1052
|
+
"",
|
|
1053
|
+
"* step",
|
|
1054
|
+
" expected",
|
|
1055
|
+
"",
|
|
1056
|
+
" ",
|
|
1057
|
+
].join("\n");
|
|
1058
|
+
|
|
1059
|
+
const blocks = markdownToBlocks(markdown);
|
|
1060
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1061
|
+
expect(stepBlocks).toHaveLength(1);
|
|
1062
|
+
expect(stepBlocks[0].props).toMatchObject({
|
|
1063
|
+
stepTitle: "step",
|
|
1064
|
+
stepData: "expected",
|
|
1065
|
+
expectedResult: "",
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1019
1069
|
it("parses bullet lists written with asterisk markers", () => {
|
|
1020
1070
|
const markdown = [
|
|
1021
1071
|
"### Preconditions",
|
|
@@ -1104,12 +1154,16 @@ describe("markdownToBlocks", () => {
|
|
|
1104
1154
|
|
|
1105
1155
|
it("parses expected result prefixes with emphasis", () => {
|
|
1106
1156
|
const markdown = [
|
|
1157
|
+
"### Steps",
|
|
1158
|
+
"",
|
|
1107
1159
|
"* Open the form.",
|
|
1108
1160
|
" **Expected:** The form opens.",
|
|
1109
1161
|
" Expected: Fields are empty.",
|
|
1110
1162
|
].join("\n");
|
|
1111
1163
|
|
|
1112
|
-
|
|
1164
|
+
const blocks = markdownToBlocks(markdown);
|
|
1165
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1166
|
+
expect(stepBlocks).toEqual([
|
|
1113
1167
|
{
|
|
1114
1168
|
type: "testStep",
|
|
1115
1169
|
props: {
|
|
@@ -1125,13 +1179,17 @@ describe("markdownToBlocks", () => {
|
|
|
1125
1179
|
|
|
1126
1180
|
it("parses test step with data", () => {
|
|
1127
1181
|
const markdown = [
|
|
1182
|
+
"### Steps",
|
|
1183
|
+
"",
|
|
1128
1184
|
"* Navigate to login",
|
|
1129
1185
|
" Open browser",
|
|
1130
1186
|
" Go to login page",
|
|
1131
1187
|
" *Expected*: Login form visible",
|
|
1132
1188
|
].join("\n");
|
|
1133
1189
|
|
|
1134
|
-
|
|
1190
|
+
const blocks = markdownToBlocks(markdown);
|
|
1191
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1192
|
+
expect(stepBlocks).toEqual([
|
|
1135
1193
|
{
|
|
1136
1194
|
type: "testStep",
|
|
1137
1195
|
props: {
|
|
@@ -1147,13 +1205,17 @@ describe("markdownToBlocks", () => {
|
|
|
1147
1205
|
|
|
1148
1206
|
it("parses unindented step data between the title and expected result", () => {
|
|
1149
1207
|
const markdown = [
|
|
1208
|
+
"### Steps",
|
|
1209
|
+
"",
|
|
1150
1210
|
"* Prepare test fixtures",
|
|
1151
1211
|
"Collect user accounts from staging.",
|
|
1152
1212
|
"Reset passwords for all test accounts.",
|
|
1153
1213
|
"*Expected*: Test accounts are ready for execution.",
|
|
1154
1214
|
].join("\n");
|
|
1155
1215
|
|
|
1156
|
-
|
|
1216
|
+
const blocks = markdownToBlocks(markdown);
|
|
1217
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1218
|
+
expect(stepBlocks).toEqual([
|
|
1157
1219
|
{
|
|
1158
1220
|
type: "testStep",
|
|
1159
1221
|
props: {
|
|
@@ -1169,11 +1231,15 @@ describe("markdownToBlocks", () => {
|
|
|
1169
1231
|
|
|
1170
1232
|
it("parses expected result containing a markdown image", () => {
|
|
1171
1233
|
const markdown = [
|
|
1234
|
+
"### Steps",
|
|
1235
|
+
"",
|
|
1172
1236
|
"* Display the generated report.",
|
|
1173
1237
|
" *Expected*: ",
|
|
1174
1238
|
].join("\n");
|
|
1175
1239
|
|
|
1176
|
-
|
|
1240
|
+
const blocks = markdownToBlocks(markdown);
|
|
1241
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1242
|
+
expect(stepBlocks).toEqual([
|
|
1177
1243
|
{
|
|
1178
1244
|
type: "testStep",
|
|
1179
1245
|
props: {
|
|
@@ -1201,17 +1267,26 @@ describe("markdownToBlocks", () => {
|
|
|
1201
1267
|
},
|
|
1202
1268
|
]);
|
|
1203
1269
|
|
|
1204
|
-
expect(markdownRoundTrip).toBe(
|
|
1270
|
+
expect(markdownRoundTrip).toBe(
|
|
1271
|
+
[
|
|
1272
|
+
"* Display the generated report.",
|
|
1273
|
+
" *Expected*: ",
|
|
1274
|
+
].join("\n"),
|
|
1275
|
+
);
|
|
1205
1276
|
});
|
|
1206
1277
|
|
|
1207
1278
|
it("parses expected result with short expected label and image", () => {
|
|
1208
1279
|
const markdown = [
|
|
1280
|
+
"### Steps",
|
|
1281
|
+
"",
|
|
1209
1282
|
"* Should open login screen",
|
|
1210
1283
|
" *Expected*: Login should look like this",
|
|
1211
1284
|
" ",
|
|
1212
1285
|
].join("\n");
|
|
1213
1286
|
|
|
1214
|
-
|
|
1287
|
+
const blocks = markdownToBlocks(markdown);
|
|
1288
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1289
|
+
expect(stepBlocks).toEqual([
|
|
1215
1290
|
{
|
|
1216
1291
|
type: "testStep",
|
|
1217
1292
|
props: {
|
|
@@ -1755,11 +1830,15 @@ describe("markdownToBlocks", () => {
|
|
|
1755
1830
|
|
|
1756
1831
|
it("parses expected result lines written with bold 'Expected Result' prefix for compatibility", () => {
|
|
1757
1832
|
const markdown = [
|
|
1833
|
+
"### Steps",
|
|
1834
|
+
"",
|
|
1758
1835
|
"* Step 1: Send a chat message to the user.",
|
|
1759
1836
|
"**Expected Result**: The user receives a real-time notification for the chat message.",
|
|
1760
1837
|
].join("\n");
|
|
1761
1838
|
|
|
1762
|
-
|
|
1839
|
+
const blocks = markdownToBlocks(markdown);
|
|
1840
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
1841
|
+
expect(stepBlocks).toEqual([
|
|
1763
1842
|
{
|
|
1764
1843
|
type: "testStep",
|
|
1765
1844
|
props: {
|
|
@@ -2407,3 +2486,42 @@ describe("video/audio block serialization", () => {
|
|
|
2407
2486
|
expect(md).toBe("[](https://example.com/sound.mp3)");
|
|
2408
2487
|
});
|
|
2409
2488
|
});
|
|
2489
|
+
|
|
2490
|
+
describe("steps require Steps heading", () => {
|
|
2491
|
+
it("does not parse bullet items with Expected markers as steps without a Steps heading", () => {
|
|
2492
|
+
const markdown = [
|
|
2493
|
+
"### Preconditions",
|
|
2494
|
+
"",
|
|
2495
|
+
"* Open the Login page.",
|
|
2496
|
+
" *Expected*: The Login page loads successfully.",
|
|
2497
|
+
].join("\n");
|
|
2498
|
+
|
|
2499
|
+
const blocks = markdownToBlocks(markdown);
|
|
2500
|
+
expect(blocks.filter((b) => b.type === "testStep")).toHaveLength(0);
|
|
2501
|
+
expect(blocks.filter((b) => b.type === "bulletListItem")).toHaveLength(1);
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
it("does not parse snippet comments as snippet blocks without a Steps heading", () => {
|
|
2505
|
+
const markdown = [
|
|
2506
|
+
"<!-- begin snippet #501 -->",
|
|
2507
|
+
"Run the seeder",
|
|
2508
|
+
"<!-- end snippet #501 -->",
|
|
2509
|
+
].join("\n");
|
|
2510
|
+
|
|
2511
|
+
const blocks = markdownToBlocks(markdown);
|
|
2512
|
+
expect(blocks.filter((b) => b.type === "snippet")).toHaveLength(0);
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
it("parses bullet items as steps when preceded by a Steps heading", () => {
|
|
2516
|
+
const markdown = [
|
|
2517
|
+
"### Steps",
|
|
2518
|
+
"",
|
|
2519
|
+
"* next 22",
|
|
2520
|
+
].join("\n");
|
|
2521
|
+
|
|
2522
|
+
const blocks = markdownToBlocks(markdown);
|
|
2523
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
2524
|
+
expect(stepBlocks).toHaveLength(1);
|
|
2525
|
+
expect((stepBlocks[0].props as any).stepTitle).toBe("next 22");
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
@@ -834,14 +834,9 @@ function parseList(
|
|
|
834
834
|
break;
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
-
// Only try to parse as testStep for top-level items
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (indentLevel === 0 && (allowEmptySteps || isLikelyStep(lines, index))) {
|
|
841
|
-
const looksLikeTestStep = listType === "bullet" ||
|
|
842
|
-
(listType === "numbered" && (
|
|
843
|
-
allowEmptySteps || isLikelyStep(lines, index)
|
|
844
|
-
));
|
|
837
|
+
// Only try to parse as testStep for top-level items under a Steps heading
|
|
838
|
+
if (indentLevel === 0 && allowEmptySteps) {
|
|
839
|
+
const looksLikeTestStep = listType === "bullet" || listType === "numbered";
|
|
845
840
|
|
|
846
841
|
if (looksLikeTestStep) {
|
|
847
842
|
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
@@ -890,31 +885,6 @@ function parseList(
|
|
|
890
885
|
return { items, nextIndex: index };
|
|
891
886
|
}
|
|
892
887
|
|
|
893
|
-
function isLikelyStep(lines: string[], index: number): boolean {
|
|
894
|
-
// Look ahead to see if there's indented content or expected result
|
|
895
|
-
// Look ahead through subsequent lines for expected result markers or indented content
|
|
896
|
-
for (let i = index + 1; i < lines.length; i++) {
|
|
897
|
-
const line = lines[i];
|
|
898
|
-
const trimmed = line.trim();
|
|
899
|
-
|
|
900
|
-
// Stop at blank lines
|
|
901
|
-
if (!trimmed) break;
|
|
902
|
-
|
|
903
|
-
// Check for indented content (step data) first — indented lines indicate a test step
|
|
904
|
-
const hasIndent = /^\s{2,}/.test(line);
|
|
905
|
-
if (hasIndent) return true;
|
|
906
|
-
|
|
907
|
-
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
908
|
-
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
|
|
909
|
-
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::")) break;
|
|
910
|
-
|
|
911
|
-
// Check for expected result markers
|
|
912
|
-
if (EXPECTED_LABEL_REGEX.test(trimmed)) return true;
|
|
913
|
-
if (trimmed.match(/^\*[^*]*expected/i)) return true;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
888
|
|
|
919
889
|
function parseTestStep(
|
|
920
890
|
lines: string[],
|
|
@@ -971,6 +941,7 @@ function parseTestStep(
|
|
|
971
941
|
let expectedResult = "";
|
|
972
942
|
let next = index + 1;
|
|
973
943
|
let inExpectedResult = false;
|
|
944
|
+
let blankLineSeenOutsideCodeBlock = false;
|
|
974
945
|
const stepIndent = current.length - current.trimStart().length;
|
|
975
946
|
|
|
976
947
|
while (next < lines.length) {
|
|
@@ -985,6 +956,7 @@ function parseTestStep(
|
|
|
985
956
|
expectedResult += "\n";
|
|
986
957
|
} else {
|
|
987
958
|
stepDataLines.push("");
|
|
959
|
+
blankLineSeenOutsideCodeBlock = true;
|
|
988
960
|
}
|
|
989
961
|
}
|
|
990
962
|
next += 1;
|
|
@@ -1096,6 +1068,11 @@ function parseTestStep(
|
|
|
1096
1068
|
continue;
|
|
1097
1069
|
}
|
|
1098
1070
|
|
|
1071
|
+
// After a blank line outside a code block, stop adding to step data
|
|
1072
|
+
if (blankLineSeenOutsideCodeBlock) {
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1099
1076
|
if (STEP_DATA_LINE_REGEX.test(rawTrimmed)) {
|
|
1100
1077
|
const content = unescapeMarkdown(rawTrimmed);
|
|
1101
1078
|
stepDataLines.push(content);
|
|
@@ -1453,15 +1430,17 @@ export function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOpt
|
|
|
1453
1430
|
continue;
|
|
1454
1431
|
}
|
|
1455
1432
|
|
|
1456
|
-
const snippetWrapper =
|
|
1433
|
+
const snippetWrapper = stepsHeadingLevel !== null
|
|
1434
|
+
? parseSnippetWrapper(lines, index)
|
|
1435
|
+
: null;
|
|
1457
1436
|
if (snippetWrapper) {
|
|
1458
1437
|
blocks.push(snippetWrapper.block);
|
|
1459
1438
|
index = snippetWrapper.nextIndex;
|
|
1460
1439
|
continue;
|
|
1461
1440
|
}
|
|
1462
1441
|
|
|
1463
|
-
const stepLikeBlock =
|
|
1464
|
-
? parseTestStep(lines, index,
|
|
1442
|
+
const stepLikeBlock = stepsHeadingLevel !== null
|
|
1443
|
+
? parseTestStep(lines, index, true)
|
|
1465
1444
|
: null;
|
|
1466
1445
|
if (stepLikeBlock) {
|
|
1467
1446
|
blocks.push(stepLikeBlock.block);
|
|
@@ -10,11 +10,15 @@ const baseProps = {
|
|
|
10
10
|
describe("markdownToBlocks", () => {
|
|
11
11
|
it("parses test steps and test cases", () => {
|
|
12
12
|
const markdown = [
|
|
13
|
+
"### Steps",
|
|
14
|
+
"",
|
|
13
15
|
"* Open the Login page.",
|
|
14
16
|
" *Expected*: The Login page loads successfully.",
|
|
15
17
|
].join("\n");
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const blocks = markdownToBlocks(markdown);
|
|
20
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
21
|
+
expect(stepBlocks).toEqual([
|
|
18
22
|
{
|
|
19
23
|
type: "testStep",
|
|
20
24
|
props: {
|
|
@@ -30,12 +34,16 @@ describe("markdownToBlocks", () => {
|
|
|
30
34
|
|
|
31
35
|
it("parses snippet markdown into snippet blocks", () => {
|
|
32
36
|
const markdown = [
|
|
37
|
+
"### Steps",
|
|
38
|
+
"",
|
|
33
39
|
"<!-- begin snippet #501 -->",
|
|
34
40
|
"Run the seeder",
|
|
35
41
|
"<!-- end snippet #501 -->",
|
|
36
42
|
].join("\n");
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
const blocks = markdownToBlocks(markdown);
|
|
45
|
+
const snippetBlocks = blocks.filter((b) => b.type === "snippet");
|
|
46
|
+
expect(snippetBlocks).toEqual([
|
|
39
47
|
{
|
|
40
48
|
type: "snippet",
|
|
41
49
|
props: {
|
|
@@ -51,6 +59,8 @@ describe("markdownToBlocks", () => {
|
|
|
51
59
|
|
|
52
60
|
it("parses snippet bodies and ignores nested snippet markers", () => {
|
|
53
61
|
const markdown = [
|
|
62
|
+
"### Steps",
|
|
63
|
+
"",
|
|
54
64
|
"<!-- begin snippet #888 -->",
|
|
55
65
|
"Prep DB",
|
|
56
66
|
"<!-- begin snippet #ignored -->",
|
|
@@ -59,7 +69,9 @@ describe("markdownToBlocks", () => {
|
|
|
59
69
|
"<!-- end snippet #888 -->",
|
|
60
70
|
].join("\n");
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
const blocks = markdownToBlocks(markdown);
|
|
73
|
+
const snippetBlocks = blocks.filter((b) => b.type === "snippet");
|
|
74
|
+
expect(snippetBlocks).toEqual([
|
|
63
75
|
{
|
|
64
76
|
type: "snippet",
|
|
65
77
|
props: {
|
package/src/editor/styles.css
CHANGED
|
@@ -1339,6 +1339,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
1339
1339
|
pointer-events: none;
|
|
1340
1340
|
}
|
|
1341
1341
|
|
|
1342
|
+
.bn-suggestion-menu .mantine-Badge-label {
|
|
1343
|
+
text-transform: none;
|
|
1344
|
+
font-size: var(--badge-fz-sm);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1342
1347
|
.bn-suggestion-icon {
|
|
1343
1348
|
display: inline-flex;
|
|
1344
1349
|
align-items: center;
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
type CustomBlock,
|
|
5
5
|
type CustomEditor,
|
|
6
6
|
} from "./editor/customSchema";
|
|
7
|
-
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
7
|
+
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
8
8
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
9
9
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
10
10
|
|