testomatio-editor-blocks 0.4.0 → 0.4.1

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.
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from "react";
2
+ import { StepField } from "./stepField";
3
+ import type { StepSuggestion } from "../stepAutocomplete";
4
+
5
+ const STEP_PLACEHOLDER = "Enter step name";
6
+ const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
7
+
8
+ type StepHorizontalViewProps = {
9
+ blockId: string;
10
+ stepNumber: number;
11
+ stepValue: string;
12
+ expectedResult: string;
13
+ onStepChange: (next: string) => void;
14
+ onExpectedChange: (next: string) => void;
15
+ onInsertNextStep: () => void;
16
+ onFieldFocus: () => void;
17
+ viewToggle?: ReactNode;
18
+ };
19
+
20
+ export function StepHorizontalView({
21
+ blockId,
22
+ stepNumber,
23
+ stepValue,
24
+ expectedResult,
25
+ onStepChange,
26
+ onExpectedChange,
27
+ onInsertNextStep,
28
+ onFieldFocus,
29
+ viewToggle,
30
+ }: StepHorizontalViewProps) {
31
+ return (
32
+ <div className="bn-teststep bn-teststep--horizontal" data-block-id={blockId}>
33
+ <div className="bn-teststep__timeline">
34
+ <span className="bn-teststep__number">{stepNumber}</span>
35
+ <div className="bn-teststep__line" />
36
+ </div>
37
+ <div className="bn-teststep__content">
38
+ <div className="bn-teststep__horizontal-fields">
39
+ <div className="bn-teststep__horizontal-col">
40
+ <div className="bn-teststep__header">
41
+ <span className="bn-teststep__title">Step</span>
42
+ </div>
43
+ <StepField
44
+ label="Step"
45
+ showLabel={false}
46
+ value={stepValue}
47
+ onChange={onStepChange}
48
+ placeholder={STEP_PLACEHOLDER}
49
+ enableAutocomplete
50
+ fieldName="title"
51
+ suggestionFilter={(suggestion) => (suggestion as StepSuggestion).isSnippet !== true}
52
+ onFieldFocus={onFieldFocus}
53
+ multiline
54
+ enableImageUpload
55
+ showFormattingButtons
56
+ showImageButton
57
+ />
58
+ </div>
59
+ <div className="bn-teststep__horizontal-col">
60
+ <div className="bn-teststep__header">
61
+ <span className="bn-teststep__title">Expected result</span>
62
+ {viewToggle}
63
+ </div>
64
+ <StepField
65
+ label="Expected result"
66
+ showLabel={false}
67
+ value={expectedResult}
68
+ onChange={onExpectedChange}
69
+ placeholder={EXPECTED_RESULT_PLACEHOLDER}
70
+ multiline
71
+ enableAutocomplete
72
+ enableImageUpload
73
+ showFormattingButtons
74
+ showImageButton
75
+ onFieldFocus={onFieldFocus}
76
+ />
77
+ </div>
78
+ </div>
79
+ <div className="bn-step-actions">
80
+ <button type="button" className="bn-step-action-btn" onClick={onInsertNextStep}>
81
+ <svg className="bn-step-action-btn__icon" width="16" height="16" viewBox="0 0 13.334 13.334" fill="none" aria-hidden="true">
82
+ <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"/>
83
+ </svg>
84
+ Add new step
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  blocksToMarkdown,
4
4
  markdownToBlocks,
5
+ fixMalformedImageBlocks,
5
6
  type CustomEditorBlock,
6
7
  type CustomPartialBlock,
7
8
  } from "./customMarkdownConverter";
@@ -633,7 +634,7 @@ describe("markdownToBlocks", () => {
633
634
  "",
634
635
  "* Verify that each individual unit test completes in ≤ 50 ms (target) and never exceeds 200 ms (hard limit).",
635
636
  "",
636
- "### Steps",
637
+ "### Instructions",
637
638
  "",
638
639
  "1. Execute the full unit test suite with a timer wrapper.",
639
640
  " * Each individual test case.",
@@ -834,6 +835,61 @@ describe("markdownToBlocks", () => {
834
835
  ]);
835
836
  });
836
837
 
838
+ it("handles multiple steps with expected results without extra asterisks", () => {
839
+ const markdown = [
840
+ "### Preconditions",
841
+ "",
842
+ "User on the Sign In with Email Screen",
843
+ "",
844
+ "### Steps",
845
+ "",
846
+ "* Existing email + invalid password",
847
+ " *Expected*: 'Oops, wrong email or password' is displayed",
848
+ "* Not existing email + valid password",
849
+ " *Expected*: 'Oops, wrong email or password' is displayed",
850
+ ].join("\n");
851
+
852
+ const blocks = markdownToBlocks(markdown);
853
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
854
+
855
+ expect(stepBlocks).toHaveLength(2);
856
+
857
+ expect(stepBlocks[0]).toEqual({
858
+ type: "testStep",
859
+ props: {
860
+ stepTitle: "Existing email + invalid password",
861
+ stepData: "",
862
+ expectedResult: "'Oops, wrong email or password' is displayed",
863
+ },
864
+ children: [],
865
+ });
866
+
867
+ expect(stepBlocks[1]).toEqual({
868
+ type: "testStep",
869
+ props: {
870
+ stepTitle: "Not existing email + valid password",
871
+ stepData: "",
872
+ expectedResult: "'Oops, wrong email or password' is displayed",
873
+ },
874
+ children: [],
875
+ });
876
+
877
+ // Verify round-trip doesn't add extra asterisks
878
+ const roundTrip = blocksToMarkdown(stepBlocks.map((block, index) => ({
879
+ ...block,
880
+ id: `step${index}`,
881
+ })) as CustomEditorBlock[]);
882
+
883
+ expect(roundTrip).toBe(
884
+ [
885
+ "* Existing email + invalid password",
886
+ " *Expected*: 'Oops, wrong email or password' is displayed",
887
+ "* Not existing email + valid password",
888
+ " *Expected*: 'Oops, wrong email or password' is displayed",
889
+ ].join("\n"),
890
+ );
891
+ });
892
+
837
893
  it("round-trips simple blocks", () => {
838
894
  const blocks: CustomEditorBlock[] = [
839
895
  {
@@ -977,4 +1033,390 @@ describe("markdownToBlocks", () => {
977
1033
  children: [],
978
1034
  });
979
1035
  });
1036
+
1037
+ it("correctly parses images at the end of the document", () => {
1038
+ const markdown = [
1039
+ "#### Steps:",
1040
+ "1. Navigate to the product listing page.",
1041
+ " Expected open",
1042
+ "3. Select a product and click the \"Add to Cart\" button.",
1043
+ " Expected result close",
1044
+ "5. Open the shopping cart page.",
1045
+ " **Expected** edit",
1046
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1047
+ " _Expected_ close",
1048
+ "",
1049
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1050
+ "",
1051
+ "![logs](/attachments/se2n8jaGon.png)",
1052
+ "",
1053
+ "![](/attachments/p5DgklVeMg.png)",
1054
+ ].join("\n");
1055
+
1056
+ const blocks = markdownToBlocks(markdown);
1057
+
1058
+ // Find the paragraph blocks that contain the images (links)
1059
+ const imageBlocks = blocks.filter(block =>
1060
+ block.type === "paragraph" &&
1061
+ block.content &&
1062
+ Array.isArray(block.content) &&
1063
+ block.content.some((item: any) =>
1064
+ (item.type === "text" && item.text === "!") ||
1065
+ (item.type === "link" && item.href && item.href.includes("/attachments/"))
1066
+ )
1067
+ );
1068
+
1069
+ // Should have two paragraph blocks with images
1070
+ expect(imageBlocks.length).toBe(2);
1071
+
1072
+ // Check that both image links are properly parsed
1073
+ const imageLinks: any[] = [];
1074
+ imageBlocks.forEach(block => {
1075
+ if (block.content && Array.isArray(block.content)) {
1076
+ const link = (block.content as any[]).find(item => item.type === "link");
1077
+ if (link) {
1078
+ imageLinks.push(link);
1079
+ }
1080
+ }
1081
+ });
1082
+
1083
+ expect(imageLinks).toHaveLength(2);
1084
+ expect(imageLinks[0].href).toBe("/attachments/se2n8jaGon.png");
1085
+ expect(imageLinks[0].content).toEqual([{ type: "text", text: "logs", styles: {} }]);
1086
+ expect(imageLinks[1].href).toBe("/attachments/p5DgklVeMg.png");
1087
+ expect(imageLinks[1].content).toEqual([{ type: "text", text: "", styles: {} }]);
1088
+
1089
+ // Test round-trip conversion
1090
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1091
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1092
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1093
+ });
1094
+
1095
+ it("parses numbered lists under a Steps heading as test steps", () => {
1096
+ const markdown = [
1097
+ "#### Steps:",
1098
+ "1. Navigate to the product listing page.",
1099
+ " Expected open",
1100
+ "3. Select a product and click the \"Add to Cart\" button.",
1101
+ " Expected result close",
1102
+ "5. Open the shopping cart page.",
1103
+ " **Expected** edit",
1104
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1105
+ " _Expected_ close",
1106
+ "",
1107
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1108
+ "",
1109
+ "![logs](/attachments/se2n8jaGon.png)",
1110
+ "",
1111
+ "![](/attachments/p5DgklVeMg.png)",
1112
+ ].join("\n");
1113
+
1114
+ const blocks = markdownToBlocks(markdown);
1115
+ const testSteps = blocks.filter(block => block.type === "testStep");
1116
+
1117
+ // Should have 4 test steps
1118
+ expect(testSteps).toHaveLength(4);
1119
+
1120
+ // Check the first test step
1121
+ expect(testSteps[0]).toEqual({
1122
+ type: "testStep",
1123
+ props: {
1124
+ stepTitle: "Navigate to the product listing page.",
1125
+ stepData: "Expected open",
1126
+ expectedResult: "",
1127
+ },
1128
+ children: [],
1129
+ });
1130
+
1131
+ // Check the second test step
1132
+ expect(testSteps[1]).toEqual({
1133
+ type: "testStep",
1134
+ props: {
1135
+ stepTitle: "Select a product and click the \"Add to Cart\" button.",
1136
+ stepData: "Expected result close",
1137
+ expectedResult: "",
1138
+ },
1139
+ children: [],
1140
+ });
1141
+
1142
+ // Check the third test step
1143
+ expect(testSteps[2]).toEqual({
1144
+ type: "testStep",
1145
+ props: {
1146
+ stepTitle: "Open the shopping cart page.",
1147
+ stepData: "**Expected** edit",
1148
+ expectedResult: "",
1149
+ },
1150
+ children: [],
1151
+ });
1152
+
1153
+ // Check the fourth test step
1154
+ expect(testSteps[3]).toEqual({
1155
+ type: "testStep",
1156
+ props: {
1157
+ stepTitle: "Verify that the added item is displayed with the correct name, price, and quantity.",
1158
+ stepData: "_Expected_ close",
1159
+ expectedResult: "",
1160
+ },
1161
+ children: [],
1162
+ });
1163
+
1164
+ // Test round-trip conversion
1165
+ // Note: Test steps are always serialized as bullet lists, not numbered lists
1166
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1167
+ expect(roundTripMarkdown).toContain("* Navigate to the product listing page.");
1168
+ expect(roundTripMarkdown).toContain("* Select a product and click the \"Add to Cart\" button.");
1169
+ expect(roundTripMarkdown).toContain("* Open the shopping cart page.");
1170
+ expect(roundTripMarkdown).toContain("* Verify that the added item is displayed with the correct name, price, and quantity.");
1171
+ // Check that step data is preserved
1172
+ expect(roundTripMarkdown).toContain(" Expected open");
1173
+ expect(roundTripMarkdown).toContain(" Expected result close");
1174
+ expect(roundTripMarkdown).toContain(" **Expected** edit");
1175
+ expect(roundTripMarkdown).toContain(" _Expected_ close");
1176
+ });
1177
+
1178
+ it("handles standalone images without bullet list interference", () => {
1179
+ const markdown = [
1180
+ "### Steps:",
1181
+ "1. Navigate to the product listing page.",
1182
+ " Expected open",
1183
+ "",
1184
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1185
+ "",
1186
+ "![logs](/attachments/se2n8jaGon.png)",
1187
+ "",
1188
+ "![](/attachments/p5DgklVeMg.png)",
1189
+ ].join("\n");
1190
+
1191
+ const blocks = markdownToBlocks(markdown);
1192
+
1193
+ // Find image paragraphs
1194
+ const imageParagraphs = blocks.filter(block =>
1195
+ block.type === "paragraph" &&
1196
+ block.content &&
1197
+ Array.isArray(block.content) &&
1198
+ block.content.some((item: any) => item.type === "link")
1199
+ );
1200
+
1201
+ // Should have exactly 2 image paragraphs
1202
+ expect(imageParagraphs).toHaveLength(2);
1203
+
1204
+ // First image with alt text
1205
+ expect(imageParagraphs[0].content).toContainEqual({
1206
+ type: "text",
1207
+ text: "!",
1208
+ styles: {}
1209
+ });
1210
+ expect(imageParagraphs[0].content).toContainEqual({
1211
+ type: "link",
1212
+ href: "/attachments/se2n8jaGon.png",
1213
+ content: [{ type: "text", text: "logs", styles: {} }]
1214
+ });
1215
+
1216
+ // Second image without alt text
1217
+ expect(imageParagraphs[1].content).toContainEqual({
1218
+ type: "text",
1219
+ text: "!",
1220
+ styles: {}
1221
+ });
1222
+ expect(imageParagraphs[1].content).toContainEqual({
1223
+ type: "link",
1224
+ href: "/attachments/p5DgklVeMg.png",
1225
+ content: [{ type: "text", text: "", styles: {} }]
1226
+ });
1227
+
1228
+ // No extra empty paragraphs
1229
+ const emptyParagraphs = blocks.filter(block =>
1230
+ block.type === "paragraph" &&
1231
+ (!block.content || block.content.length === 0)
1232
+ );
1233
+ expect(emptyParagraphs).toHaveLength(0);
1234
+
1235
+ // Test round-trip conversion
1236
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1237
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1238
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1239
+ });
1240
+
1241
+ it("handles images with multiple blank lines between them", () => {
1242
+ const markdown = `
1243
+
1244
+ ![logs](/attachments/se2n8jaGon.png)
1245
+
1246
+
1247
+
1248
+ ![](/attachments/p5DgklVeMg.png)
1249
+
1250
+
1251
+
1252
+ `;
1253
+
1254
+ const blocks = markdownToBlocks(markdown);
1255
+
1256
+ // Should have exactly 2 image paragraphs, no empty paragraphs
1257
+ const imageParagraphs = blocks.filter(block =>
1258
+ block.type === "paragraph" &&
1259
+ block.content &&
1260
+ Array.isArray(block.content) &&
1261
+ block.content.some((item: any) => item.type === "link")
1262
+ );
1263
+
1264
+ const emptyParagraphs = blocks.filter(block =>
1265
+ block.type === "paragraph" &&
1266
+ (!block.content || block.content.length === 0)
1267
+ );
1268
+
1269
+ // Check for malformed image blocks (paragraphs with just "!" but no link)
1270
+ const malformedBlocks = blocks.filter(block =>
1271
+ block.type === "paragraph" &&
1272
+ block.content &&
1273
+ Array.isArray(block.content) &&
1274
+ block.content.some((item: any) => item.type === "text" && item.text === "!") &&
1275
+ !block.content.some((item: any) => item.type === "link")
1276
+ );
1277
+
1278
+ expect(imageParagraphs).toHaveLength(2);
1279
+ expect(emptyParagraphs).toHaveLength(0);
1280
+ expect(malformedBlocks).toHaveLength(0);
1281
+
1282
+ // Test round-trip conversion
1283
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1284
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1285
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1286
+ });
1287
+
1288
+ it("reproduces the exact issue from user's example", () => {
1289
+ const markdown = `#### Steps:
1290
+ 1. Navigate to the product listing page.
1291
+ Expected open
1292
+ 3. Select a product and click the "Add to Cart" button.
1293
+ Expected result close
1294
+ 5. Open the shopping cart page.
1295
+ **Expected** edit
1296
+ 6. Verify that the added item is displayed with the correct name, price, and quantity.
1297
+ _Expected_ close
1298
+
1299
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1300
+
1301
+
1302
+
1303
+ ![logs](/attachments/se2n8jaGon.png)
1304
+
1305
+
1306
+
1307
+ ![](/attachments/p5DgklVeMg.png)
1308
+
1309
+
1310
+
1311
+ `;
1312
+
1313
+ const blocks = markdownToBlocks(markdown);
1314
+
1315
+ // Test round-trip conversion
1316
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1317
+
1318
+ // Check that both images are preserved
1319
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1320
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1321
+
1322
+ // Make sure we don't have a standalone "!" without the rest of the image
1323
+ const lines = roundTripMarkdown.split('\n');
1324
+ const exclamationLines = lines.filter(line => line.trim() === '!' || line.trim() === '! ');
1325
+ expect(exclamationLines.length).toBe(0);
1326
+ });
1327
+
1328
+ it("handles empty alt text images correctly", () => {
1329
+ const markdown = "![](/attachments/test.png)";
1330
+ const blocks = markdownToBlocks(markdown);
1331
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1332
+
1333
+ expect(roundTripMarkdown).toContain("![](/attachments/test.png)");
1334
+ });
1335
+
1336
+ it("removes malformed image blocks through post-processing", () => {
1337
+ // Simulate the malformed blocks you're seeing
1338
+ const malformedBlocks: any[] = [
1339
+ {
1340
+ type: "heading",
1341
+ props: { level: 3 },
1342
+ content: [{ type: "text", text: "Expected Result:", styles: {} }],
1343
+ children: []
1344
+ },
1345
+ {
1346
+ type: "paragraph",
1347
+ props: {},
1348
+ content: [
1349
+ { type: "text", text: "!", styles: {} },
1350
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1351
+ ],
1352
+ children: []
1353
+ },
1354
+ {
1355
+ type: "paragraph",
1356
+ props: {},
1357
+ content: [{ type: "text", text: "!", styles: {} }],
1358
+ children: []
1359
+ },
1360
+ {
1361
+ type: "paragraph",
1362
+ props: {},
1363
+ content: [],
1364
+ children: []
1365
+ }
1366
+ ];
1367
+
1368
+ // Apply the fixMalformedImageBlocks function
1369
+ const fixedBlocks = fixMalformedImageBlocks(malformedBlocks);
1370
+
1371
+ // Should have removed the malformed image blocks (both the "!" only block and the empty block)
1372
+ expect(fixedBlocks.length).toBe(2);
1373
+ expect(fixedBlocks[0].type).toBe("heading");
1374
+ expect(fixedBlocks[1].type).toBe("paragraph");
1375
+ expect(fixedBlocks[1].content).toContainEqual(
1376
+ { type: "text", text: "!", styles: {} }
1377
+ );
1378
+ expect(fixedBlocks[1].content).toContainEqual(
1379
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1380
+ );
1381
+ });
1382
+
1383
+ it("reproduces the exact Unsplash URL issue", () => {
1384
+ const markdown = `
1385
+
1386
+
1387
+
1388
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1389
+
1390
+
1391
+
1392
+ ![logs](https://images.unsplash.com/photo-1765873360413-6c79486d1fda?q=80&w=2350&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
1393
+
1394
+
1395
+
1396
+ ![](https://plus.unsplash.com/premium_photo-1765228499795-e58288bc382b?q=80&w=1925&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
1397
+
1398
+
1399
+
1400
+ `;
1401
+
1402
+ const blocks = markdownToBlocks(markdown);
1403
+
1404
+ // Should have at least 3 blocks
1405
+ expect(blocks.length).toBeGreaterThanOrEqual(3);
1406
+
1407
+ // Should have at least one paragraph with content (images)
1408
+ const imageBlocks = blocks.filter(b =>
1409
+ b.type === "paragraph" &&
1410
+ b.content &&
1411
+ Array.isArray(b.content) &&
1412
+ b.content.some((item: any) => item.type === "link")
1413
+ );
1414
+ expect(imageBlocks.length).toBeGreaterThan(0);
1415
+
1416
+ // Test round-trip conversion - check that we get the images back
1417
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1418
+
1419
+ // Most importantly: should not have a standalone "!" at the end
1420
+ expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
1421
+ });
980
1422
  });