testomatio-editor-blocks 0.4.34 → 0.4.36

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.
@@ -105,6 +105,67 @@ describe("blocksToMarkdown", () => {
105
105
  );
106
106
  });
107
107
 
108
+ it("serializes a step with empty title but with stepData", () => {
109
+ const blocks: CustomEditorBlock[] = [
110
+ {
111
+ id: "s1",
112
+ type: "testStep",
113
+ props: {
114
+ stepTitle: "",
115
+ stepData: "Navigate to the page",
116
+ expectedResult: "",
117
+ listStyle: "bullet",
118
+ },
119
+ content: undefined,
120
+ children: [],
121
+ },
122
+ ];
123
+
124
+ expect(blocksToMarkdown(blocks)).toBe(
125
+ ["* ", " Navigate to the page"].join("\n"),
126
+ );
127
+ });
128
+
129
+ it("serializes a step with empty title but with expectedResult", () => {
130
+ const blocks: CustomEditorBlock[] = [
131
+ {
132
+ id: "s1",
133
+ type: "testStep",
134
+ props: {
135
+ stepTitle: "",
136
+ stepData: "",
137
+ expectedResult: "Login form visible",
138
+ listStyle: "bullet",
139
+ },
140
+ content: undefined,
141
+ children: [],
142
+ },
143
+ ];
144
+
145
+ expect(blocksToMarkdown(blocks)).toBe(
146
+ ["* ", " *Expected*: Login form visible"].join("\n"),
147
+ );
148
+ });
149
+
150
+ it("drops completely empty step (no title, no data, no expected)", () => {
151
+ const blocks: CustomEditorBlock[] = [
152
+ {
153
+ id: "s1",
154
+ type: "testStep",
155
+ props: {
156
+ stepTitle: "",
157
+ stepData: "",
158
+ expectedResult: "",
159
+ listStyle: "bullet",
160
+ },
161
+ content: undefined,
162
+ children: [],
163
+ },
164
+ ];
165
+
166
+ expect(blocksToMarkdown(blocks)).toBe("");
167
+ });
168
+
108
169
  it("serializes a snippet block with prefixed title", () => {
109
170
  const blocks: CustomEditorBlock[] = [
110
171
  {
@@ -328,6 +389,116 @@ describe("blocksToMarkdown", () => {
328
389
  );
329
390
  });
330
391
 
392
+ it("serializes table cells containing newlines as <br/>", () => {
393
+ const blocks: CustomEditorBlock[] = [
394
+ {
395
+ id: "tbl2",
396
+ type: "table",
397
+ props: { textColor: "default" },
398
+ content: {
399
+ type: "tableContent",
400
+ columnWidths: [undefined, undefined],
401
+ headerRows: 1,
402
+ rows: [
403
+ {
404
+ cells: [
405
+ {
406
+ type: "tableCell",
407
+ props: cellProps,
408
+ content: [{ type: "text", text: "Steps", styles: {} }],
409
+ },
410
+ {
411
+ type: "tableCell",
412
+ props: cellProps,
413
+ content: [{ type: "text", text: "Expected Results", styles: {} }],
414
+ },
415
+ ],
416
+ },
417
+ {
418
+ cells: [
419
+ {
420
+ type: "tableCell",
421
+ props: cellProps,
422
+ content: [{ type: "text", text: "line1\nline2", styles: {} }],
423
+ },
424
+ {
425
+ type: "tableCell",
426
+ props: cellProps,
427
+ content: [{ type: "text", text: "ok", styles: {} }],
428
+ },
429
+ ],
430
+ },
431
+ ],
432
+ },
433
+ children: [],
434
+ },
435
+ ];
436
+
437
+ expect(blocksToMarkdown(blocks)).toBe(
438
+ [
439
+ "| Steps | Expected Results |",
440
+ "| --- | --- |",
441
+ "| line1<br/>line2 | ok |",
442
+ ].join("\n"),
443
+ );
444
+ });
445
+
446
+ it("serializes table cells with styled text and newlines without trapping <br/> inside markers", () => {
447
+ const blocks: CustomEditorBlock[] = [
448
+ {
449
+ id: "tbl3",
450
+ type: "table",
451
+ props: { textColor: "default" },
452
+ content: {
453
+ type: "tableContent",
454
+ columnWidths: [undefined, undefined],
455
+ headerRows: 1,
456
+ rows: [
457
+ {
458
+ cells: [
459
+ {
460
+ type: "tableCell",
461
+ props: cellProps,
462
+ content: [{ type: "text", text: "Col A", styles: {} }],
463
+ },
464
+ {
465
+ type: "tableCell",
466
+ props: cellProps,
467
+ content: [{ type: "text", text: "Col B", styles: {} }],
468
+ },
469
+ ],
470
+ },
471
+ {
472
+ cells: [
473
+ {
474
+ type: "tableCell",
475
+ props: cellProps,
476
+ content: [{ type: "text", text: "ok", styles: {} }],
477
+ },
478
+ {
479
+ type: "tableCell",
480
+ props: cellProps,
481
+ content: [
482
+ { type: "text", text: "opened\nnewline", styles: { bold: true } },
483
+ ],
484
+ },
485
+ ],
486
+ },
487
+ ],
488
+ },
489
+ children: [],
490
+ },
491
+ ];
492
+
493
+ expect(blocksToMarkdown(blocks)).toBe(
494
+ [
495
+ "| Col A | Col B |",
496
+ "| --- | --- |",
497
+ "| ok | **opened**<br/>**newline** |",
498
+ ].join("\n"),
499
+ );
500
+ });
501
+
331
502
  it("parses a test step with inline image in the title, moving the image to step data", () => {
332
503
  const markdown = [
333
504
  "## Steps",
@@ -365,6 +536,55 @@ describe("markdownToBlocks", () => {
365
536
  ]);
366
537
  });
367
538
 
539
+ it("parses a step with empty title but with step data", () => {
540
+ const markdown = ["* ", " Navigate to the page"].join("\n");
541
+
542
+ expect(markdownToBlocks(markdown)).toEqual([
543
+ {
544
+ type: "testStep",
545
+ props: {
546
+ stepTitle: "",
547
+ stepData: "Navigate to the page",
548
+ expectedResult: "",
549
+ listStyle: "bullet",
550
+ },
551
+ children: [],
552
+ },
553
+ ]);
554
+ });
555
+
556
+ it("round-trips a title-less step with data", () => {
557
+ const blocks: CustomEditorBlock[] = [
558
+ {
559
+ id: "s1",
560
+ type: "testStep",
561
+ props: {
562
+ stepTitle: "",
563
+ stepData: "Open the browser",
564
+ expectedResult: "Page loads",
565
+ listStyle: "bullet",
566
+ },
567
+ content: undefined,
568
+ children: [],
569
+ },
570
+ ];
571
+
572
+ const md = blocksToMarkdown(blocks);
573
+ const parsed = markdownToBlocks(md);
574
+ expect(parsed).toEqual([
575
+ {
576
+ type: "testStep",
577
+ props: {
578
+ stepTitle: "",
579
+ stepData: "Open the browser",
580
+ expectedResult: "Page loads",
581
+ listStyle: "bullet",
582
+ },
583
+ children: [],
584
+ },
585
+ ]);
586
+ });
587
+
368
588
  it("parses snippet markdown into snippet blocks", () => {
369
589
  const markdown = [
370
590
  "<!-- begin snippet #501 -->",
@@ -1212,6 +1432,114 @@ describe("markdownToBlocks", () => {
1212
1432
  ]);
1213
1433
  });
1214
1434
 
1435
+ it("parses <br/> in table cells back to newline", () => {
1436
+ const markdown = [
1437
+ "| A | B |",
1438
+ "| --- | --- |",
1439
+ "| line1<br/>line2 | ok |",
1440
+ ].join("\n");
1441
+
1442
+ const blocks = markdownToBlocks(markdown);
1443
+ expect(blocks).toEqual([
1444
+ {
1445
+ type: "table",
1446
+ props: { textColor: "default" },
1447
+ content: {
1448
+ type: "tableContent",
1449
+ columnWidths: [undefined, undefined],
1450
+ headerRows: 1,
1451
+ rows: [
1452
+ {
1453
+ cells: [
1454
+ {
1455
+ type: "tableCell",
1456
+ props: cellProps,
1457
+ content: [{ type: "text", text: "A", styles: {} }],
1458
+ },
1459
+ {
1460
+ type: "tableCell",
1461
+ props: cellProps,
1462
+ content: [{ type: "text", text: "B", styles: {} }],
1463
+ },
1464
+ ],
1465
+ },
1466
+ {
1467
+ cells: [
1468
+ {
1469
+ type: "tableCell",
1470
+ props: cellProps,
1471
+ content: [{ type: "text", text: "line1\nline2", styles: {} }],
1472
+ },
1473
+ {
1474
+ type: "tableCell",
1475
+ props: cellProps,
1476
+ content: [{ type: "text", text: "ok", styles: {} }],
1477
+ },
1478
+ ],
1479
+ },
1480
+ ],
1481
+ },
1482
+ children: [],
1483
+ },
1484
+ ]);
1485
+ });
1486
+
1487
+ it("round-trips newlines in table cells", () => {
1488
+ const blocks: CustomEditorBlock[] = [
1489
+ {
1490
+ id: "tbl3",
1491
+ type: "table",
1492
+ props: { textColor: "default" },
1493
+ content: {
1494
+ type: "tableContent",
1495
+ columnWidths: [undefined, undefined],
1496
+ headerRows: 1,
1497
+ rows: [
1498
+ {
1499
+ cells: [
1500
+ {
1501
+ type: "tableCell",
1502
+ props: cellProps,
1503
+ content: [{ type: "text", text: "Header", styles: {} }],
1504
+ },
1505
+ {
1506
+ type: "tableCell",
1507
+ props: cellProps,
1508
+ content: [{ type: "text", text: "Info", styles: {} }],
1509
+ },
1510
+ ],
1511
+ },
1512
+ {
1513
+ cells: [
1514
+ {
1515
+ type: "tableCell",
1516
+ props: cellProps,
1517
+ content: [{ type: "text", text: "first\nsecond\nthird", styles: {} }],
1518
+ },
1519
+ {
1520
+ type: "tableCell",
1521
+ props: cellProps,
1522
+ content: [{ type: "text", text: "value", styles: {} }],
1523
+ },
1524
+ ],
1525
+ },
1526
+ ],
1527
+ },
1528
+ children: [],
1529
+ },
1530
+ ];
1531
+
1532
+ const markdown = blocksToMarkdown(blocks);
1533
+ expect(markdown).toContain("first<br/>second<br/>third");
1534
+
1535
+ const parsed = markdownToBlocks(markdown);
1536
+ const row = (parsed[0] as any).content.rows[1];
1537
+ const cellContent = row.cells[0].content;
1538
+ expect(cellContent).toEqual([
1539
+ { type: "text", text: "first\nsecond\nthird", styles: {} },
1540
+ ]);
1541
+ });
1542
+
1215
1543
  it("parses expected result lines written with bold 'Expected Result' prefix for compatibility", () => {
1216
1544
  const markdown = [
1217
1545
  "* Step 1: Send a chat message to the user.",
@@ -1823,3 +2151,46 @@ describe("file block parsing", () => {
1823
2151
  expect(md).toBe(markdown);
1824
2152
  });
1825
2153
  });
2154
+
2155
+ describe("video/audio block serialization", () => {
2156
+ it("serializes a video block using the file format", () => {
2157
+ const blocks: CustomEditorBlock[] = [
2158
+ {
2159
+ id: "1",
2160
+ type: "video",
2161
+ props: {
2162
+ ...baseProps,
2163
+ url: "https://example.com/video.mp4",
2164
+ name: "recording.mp4",
2165
+ caption: "/images/file-type-icons/mp4.svg",
2166
+ showPreview: true,
2167
+ previewWidth: 512,
2168
+ },
2169
+ content: undefined as any,
2170
+ children: [],
2171
+ },
2172
+ ];
2173
+ const md = blocksToMarkdown(blocks);
2174
+ expect(md).toBe("[![recording.mp4](/images/file-type-icons/mp4.svg)](https://example.com/video.mp4)");
2175
+ });
2176
+
2177
+ it("serializes an audio block using the file format", () => {
2178
+ const blocks: CustomEditorBlock[] = [
2179
+ {
2180
+ id: "1",
2181
+ type: "audio",
2182
+ props: {
2183
+ ...baseProps,
2184
+ url: "https://example.com/sound.mp3",
2185
+ name: "sound.mp3",
2186
+ caption: "/images/file-type-icons/file.svg",
2187
+ showPreview: true,
2188
+ },
2189
+ content: undefined as any,
2190
+ children: [],
2191
+ },
2192
+ ];
2193
+ const md = blocksToMarkdown(blocks);
2194
+ expect(md).toBe("[![sound.mp3](/images/file-type-icons/file.svg)](https://example.com/sound.mp3)");
2195
+ });
2196
+ });
@@ -174,12 +174,21 @@ function applyTextStyles(text: string, styles: EditorStyles | undefined): string
174
174
  });
175
175
  }
176
176
 
177
- for (const wrapper of wrappers) {
178
- const suffix = wrapper.suffix ?? wrapper.prefix;
179
- result = `${wrapper.prefix}${result}${suffix}`;
180
- }
177
+ // Split on newlines so that style markers wrap each line individually.
178
+ // This prevents <br/> (inserted by table cell formatting) from being
179
+ // trapped inside markers like **bold<br/>text**.
180
+ const segments = result.split("\n");
181
+ const wrapped = segments.map((segment) => {
182
+ if (!segment) return segment;
183
+ let s = segment;
184
+ for (const wrapper of wrappers) {
185
+ const suffix = wrapper.suffix ?? wrapper.prefix;
186
+ s = `${wrapper.prefix}${s}${suffix}`;
187
+ }
188
+ return s;
189
+ });
181
190
 
182
- return result;
191
+ return wrapped.join("\n");
183
192
  }
184
193
 
185
194
  function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
@@ -347,7 +356,9 @@ function serializeBlock(
347
356
  }
348
357
  return flattenWithBlankLine(lines, true);
349
358
  }
350
- case "file": {
359
+ case "file":
360
+ case "video":
361
+ case "audio": {
351
362
  const url = (block.props as any).url || "";
352
363
  const name = (block.props as any).name || "";
353
364
  const caption = (block.props as any).caption || "";
@@ -390,18 +401,19 @@ function serializeBlock(
390
401
  return flattenWithBlankLine(lines, true);
391
402
  }
392
403
 
393
- if (stepTitle.length > 0) {
394
- const normalizedTitle = stepTitle
395
- .split(/\r?\n/)
396
- .map((segment: string) => segment.trim())
397
- .filter((segment: string) => segment.length > 0)
398
- .join(" ");
399
-
400
- if (normalizedTitle.length > 0) {
401
- const listStyle = (block.props as any).listStyle ?? "bullet";
402
- const prefix = listStyle === "ordered" ? `${(stepIndex ?? 0) + 1}.` : "*";
403
- lines.push(`${prefix} ${normalizedTitle}`);
404
- }
404
+ const normalizedTitle = stepTitle
405
+ .split(/\r?\n/)
406
+ .map((segment: string) => segment.trim())
407
+ .filter((segment: string) => segment.length > 0)
408
+ .join(" ");
409
+
410
+ const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
411
+ const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
412
+
413
+ if (normalizedTitle.length > 0 || hasContent) {
414
+ const listStyle = (block.props as any).listStyle ?? "bullet";
415
+ const prefix = listStyle === "ordered" ? `${(stepIndex ?? 0) + 1}.` : "*";
416
+ lines.push(normalizedTitle.length > 0 ? `${prefix} ${normalizedTitle}` : `${prefix} `);
405
417
  }
406
418
 
407
419
  if (stepData.length > 0) {
@@ -511,7 +523,10 @@ function serializeBlock(
511
523
  };
512
524
 
513
525
  const formattedRows = rows.map(normalizeRow);
514
- const formatCell = (value: string) => (value.length ? value : " ");
526
+ const formatCell = (value: string) => {
527
+ if (!value.length) return " ";
528
+ return value.replace(/\n/g, "<br/>");
529
+ };
515
530
  const toAlignmentToken = (alignment: string) => {
516
531
  switch (alignment) {
517
532
  case "center":
@@ -694,6 +709,13 @@ function parseInlineMarkdown(text: string): EditorInline[] {
694
709
  }
695
710
  }
696
711
 
712
+ const brMatch = cleaned.slice(i).match(/^<br\s*\/?\s*>/i);
713
+ if (brMatch) {
714
+ buffer += "\n";
715
+ i += brMatch[0].length;
716
+ continue;
717
+ }
718
+
697
719
  buffer += cleaned[i];
698
720
  i += 1;
699
721
  }
@@ -722,7 +744,7 @@ function detectListType(trimmed: string): "bullet" | "numbered" | "check" | null
722
744
  if (/^\d+[.)]\s+/.test(trimmed)) {
723
745
  return "numbered";
724
746
  }
725
- if (/^[-*+]\s+/.test(trimmed)) {
747
+ if (/^[-*+](\s+|$)/.test(trimmed)) {
726
748
  return "bullet";
727
749
  }
728
750
  return null;
@@ -838,7 +860,7 @@ function parseList(
838
860
  });
839
861
  } else {
840
862
  const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
841
- const text = bulletMatch?.[1] ?? trimmed.slice(2);
863
+ const text = bulletMatch?.[1] ?? (trimmed.length <= 1 ? "" : trimmed.slice(2));
842
864
  items.push({
843
865
  type: "bulletListItem",
844
866
  props: cloneBaseProps(),
@@ -868,7 +890,7 @@ function isLikelyStep(lines: string[], index: number): boolean {
868
890
  if (hasIndent) return true;
869
891
 
870
892
  // Stop at new list items, headings, or other block-level elements (only if not indented)
871
- if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
893
+ if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
872
894
  if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::")) break;
873
895
 
874
896
  // Check for expected result markers
@@ -887,7 +909,7 @@ function parseTestStep(
887
909
  ): { block: CustomPartialBlock; nextIndex: number } | null {
888
910
  const current = lines[index];
889
911
  const trimmed = current.trim();
890
- const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
912
+ const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ") || trimmed === "*" || trimmed === "-";
891
913
  const isNumbered = /^\d+[.)]\s+/.test(trimmed);
892
914
 
893
915
  if (!isBullet && !isNumbered) {
@@ -903,7 +925,9 @@ function parseTestStep(
903
925
 
904
926
  let rawTitle: string;
905
927
  if (isBullet) {
906
- rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
928
+ rawTitle = unescapeMarkdown(
929
+ (trimmed.startsWith("* ") || trimmed.startsWith("- ")) ? trimmed.slice(2) : trimmed.slice(1)
930
+ ).trim();
907
931
  } else {
908
932
  // For numbered lists, remove the number and delimiter
909
933
  rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
@@ -951,7 +975,7 @@ function parseTestStep(
951
975
  }
952
976
  const isNumberedStep = NUMBERED_STEP_REGEX.test(rawTrimmed);
953
977
  const isNewStep =
954
- (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- "))) ||
978
+ (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- ") || rawTrimmed === "*" || rawTrimmed === "-")) ||
955
979
  (!hasIndent && isNumberedStep);
956
980
 
957
981
  if (isNewStep) {
@@ -1074,7 +1074,8 @@ html.dark .bn-step-image-preview__content {
1074
1074
  }
1075
1075
 
1076
1076
  .bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
1077
- font-weight: bold !important;
1077
+ -webkit-text-stroke: 0.5px currentColor;
1078
+ font-weight: inherit !important;
1078
1079
  color: inherit !important;
1079
1080
  }
1080
1081
 
@@ -1083,6 +1084,12 @@ html.dark .bn-step-image-preview__content {
1083
1084
  color: inherit !important;
1084
1085
  }
1085
1086
 
1087
+ .bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
1088
+ background-color: rgba(135, 131, 120, 0.15) !important;
1089
+ border-radius: 3px !important;
1090
+ color: inherit !important;
1091
+ }
1092
+
1086
1093
  .bn-step-custom-caret {
1087
1094
  display: none;
1088
1095
  position: absolute;