testomatio-editor-blocks 0.1.0 → 0.1.2

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.
@@ -140,6 +140,7 @@
140
140
  display: flex;
141
141
  flex-direction: column;
142
142
  gap: 0.35rem;
143
+ position: relative;
143
144
  }
144
145
 
145
146
  .bn-step-field__top {
@@ -150,6 +151,9 @@
150
151
  }
151
152
 
152
153
  .bn-step-field__label {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 0.35rem;
153
157
  font-size: 0.8rem;
154
158
  text-transform: uppercase;
155
159
  letter-spacing: 0.08em;
@@ -216,6 +220,54 @@
216
220
  pointer-events: none;
217
221
  }
218
222
 
223
+ .bn-step-suggestions {
224
+ position: absolute;
225
+ left: 0;
226
+ right: 0;
227
+ top: calc(100% + 0.25rem);
228
+ background: rgba(255, 255, 255, 0.98);
229
+ border-radius: 0.65rem;
230
+ border: 1px solid rgba(37, 99, 235, 0.25);
231
+ box-shadow: 0 18px 35px rgba(15, 23, 42, 0.16);
232
+ padding: 0.25rem;
233
+ display: flex;
234
+ flex-direction: column;
235
+ gap: 0.15rem;
236
+ z-index: 5;
237
+ max-height: 12rem;
238
+ overflow-y: auto;
239
+ }
240
+
241
+ .bn-step-suggestion {
242
+ border: none;
243
+ background: transparent;
244
+ border-radius: 0.5rem;
245
+ padding: 0.45rem 0.75rem;
246
+ text-align: left;
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: space-between;
250
+ gap: 0.5rem;
251
+ cursor: pointer;
252
+ color: #0f172a;
253
+ }
254
+
255
+ .bn-step-suggestion:hover,
256
+ .bn-step-suggestion--active {
257
+ background: rgba(59, 130, 246, 0.1);
258
+ }
259
+
260
+ .bn-step-suggestion__title {
261
+ font-weight: 600;
262
+ font-size: 0.95rem;
263
+ }
264
+
265
+ .bn-step-suggestion__meta {
266
+ font-size: 0.75rem;
267
+ font-weight: 600;
268
+ color: rgba(15, 23, 42, 0.65);
269
+ }
270
+
219
271
  .bn-inline-image {
220
272
  display: block;
221
273
  max-width: 100%;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
package/src/App.css CHANGED
@@ -147,6 +147,47 @@
147
147
  color: #f87171;
148
148
  }
149
149
 
150
+ .app__panel--light {
151
+ background: #ffffff;
152
+ color: #0f172a;
153
+ }
154
+
155
+ .app__panel-text {
156
+ margin: 0;
157
+ color: rgba(15, 23, 42, 0.85);
158
+ font-size: 0.9rem;
159
+ }
160
+
161
+ .app__step-list {
162
+ list-style: none;
163
+ margin: 0;
164
+ padding: 0;
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 0.35rem;
168
+ }
169
+
170
+ .app__step-list li {
171
+ display: flex;
172
+ justify-content: space-between;
173
+ gap: 0.5rem;
174
+ padding: 0.4rem 0.5rem;
175
+ border-radius: 0.5rem;
176
+ background: rgba(37, 99, 235, 0.08);
177
+ color: inherit;
178
+ font-size: 0.95rem;
179
+ }
180
+
181
+ .app__step-title {
182
+ font-weight: 600;
183
+ }
184
+
185
+ .app__step-meta {
186
+ font-size: 0.8rem;
187
+ font-weight: 600;
188
+ color: rgba(15, 23, 42, 0.6);
189
+ }
190
+
150
191
  @media (max-width: 960px) {
151
192
  #root {
152
193
  padding: 1.5rem;
package/src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  import { BlockNoteView } from "@blocknote/mantine";
3
3
  import {
4
4
  useCreateBlockNote,
@@ -17,9 +17,39 @@ import {
17
17
  type CustomEditorBlock,
18
18
  type CustomPartialBlock,
19
19
  } from "./editor/customMarkdownConverter";
20
- import { customSchema } from "./editor/customSchema";
20
+ import { customSchema, type CustomEditor } from "./editor/customSchema";
21
+ import { setGlobalStepSuggestionsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
22
+ import { setImageUploadHandler } from "./editor/stepImageUpload";
21
23
  import "./App.css";
22
24
 
25
+ const focusTestStepTitle = (editor: CustomEditor | null | undefined, blockId?: string) => {
26
+ if (!editor || !blockId) {
27
+ return;
28
+ }
29
+
30
+ const focus = () => {
31
+ const stepTitle = document.querySelector<HTMLElement>(
32
+ `[data-block-id="${blockId}"] [data-step-field="title"]`,
33
+ );
34
+
35
+ if (stepTitle) {
36
+ stepTitle.focus();
37
+ return;
38
+ }
39
+
40
+ editor.setSelection(blockId, blockId);
41
+ editor.setTextCursorPosition(blockId, "end");
42
+ editor.focus();
43
+ };
44
+
45
+ if (typeof requestAnimationFrame === "function") {
46
+ requestAnimationFrame(focus);
47
+ return;
48
+ }
49
+
50
+ setTimeout(focus, 0);
51
+ };
52
+
23
53
  type Schema = typeof customSchema;
24
54
 
25
55
  const DEFAULT_BLOCK_PROPS = {
@@ -28,6 +58,151 @@ const DEFAULT_BLOCK_PROPS = {
28
58
  backgroundColor: "default" as const,
29
59
  };
30
60
 
61
+ const DEMO_STEP_FIXTURES: StepJsonApiDocument = {
62
+ data: [
63
+ {
64
+ id: "145",
65
+ type: "step",
66
+ attributes: {
67
+ labels: [],
68
+ title: "Donec placerat, dui vitae",
69
+ kind: "manual",
70
+ description: null,
71
+ keywords: [],
72
+ "is-snippet": null,
73
+ "usage-count": 23,
74
+ "comments-count": 0,
75
+ },
76
+ },
77
+ {
78
+ id: "146",
79
+ type: "step",
80
+ attributes: {
81
+ labels: [],
82
+ title: "Ut auctor mi erat ac dolor.",
83
+ kind: "manual",
84
+ description: null,
85
+ keywords: [],
86
+ "is-snippet": null,
87
+ "usage-count": 23,
88
+ "comments-count": 0,
89
+ },
90
+ },
91
+ {
92
+ id: "147",
93
+ type: "step",
94
+ attributes: {
95
+ labels: [],
96
+ title: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
97
+ kind: "manual",
98
+ description: null,
99
+ keywords: [],
100
+ "is-snippet": null,
101
+ "usage-count": 23,
102
+ "comments-count": 0,
103
+ },
104
+ },
105
+ {
106
+ id: "148",
107
+ type: "step",
108
+ attributes: {
109
+ labels: [],
110
+ title: "Felis libero varius orci, in vulputate",
111
+ kind: "manual",
112
+ description: null,
113
+ keywords: [],
114
+ "is-snippet": null,
115
+ "usage-count": 19,
116
+ "comments-count": 0,
117
+ },
118
+ },
119
+ {
120
+ id: "149",
121
+ type: "step",
122
+ attributes: {
123
+ labels: [],
124
+ title: "Massa turpis scelerisque diam.",
125
+ kind: "manual",
126
+ description: null,
127
+ keywords: [],
128
+ "is-snippet": null,
129
+ "usage-count": 19,
130
+ "comments-count": 0,
131
+ },
132
+ },
133
+ {
134
+ id: "150",
135
+ type: "step",
136
+ attributes: {
137
+ labels: [],
138
+ title: "Nunc et felis est. Phasellus laoreet nibh vel augue",
139
+ kind: "manual",
140
+ description: null,
141
+ keywords: [],
142
+ "is-snippet": null,
143
+ "usage-count": 19,
144
+ "comments-count": 0,
145
+ },
146
+ },
147
+ {
148
+ id: "151",
149
+ type: "step",
150
+ attributes: {
151
+ labels: [],
152
+ title: "Suspendisse interdum sem non sem cursus consequat.",
153
+ kind: "manual",
154
+ description: null,
155
+ keywords: [],
156
+ "is-snippet": null,
157
+ "usage-count": 17,
158
+ "comments-count": 0,
159
+ },
160
+ },
161
+ {
162
+ id: "152",
163
+ type: "step",
164
+ attributes: {
165
+ labels: [],
166
+ title: "Aliquam tempor, nibh sed facilisis lacinia, nisl velit aliquet nunc.",
167
+ kind: "manual",
168
+ description: null,
169
+ keywords: [],
170
+ "is-snippet": null,
171
+ "usage-count": 16,
172
+ "comments-count": 0,
173
+ },
174
+ },
175
+ {
176
+ id: "153",
177
+ type: "step",
178
+ attributes: {
179
+ labels: [],
180
+ title: "Praesent tellus neque, efficitur vel hendrerit sed, porta id sapien.",
181
+ kind: "manual",
182
+ description: null,
183
+ keywords: [],
184
+ "is-snippet": null,
185
+ "usage-count": 14,
186
+ "comments-count": 0,
187
+ },
188
+ },
189
+ {
190
+ id: "154",
191
+ type: "step",
192
+ attributes: {
193
+ labels: [],
194
+ title: "Maecenas suscipit lacus vitae viverra fermentum.",
195
+ kind: "manual",
196
+ description: null,
197
+ keywords: [],
198
+ "is-snippet": null,
199
+ "usage-count": 12,
200
+ "comments-count": 0,
201
+ },
202
+ },
203
+ ],
204
+ };
205
+
31
206
  function CustomSlashMenu() {
32
207
  const editor = useBlockNoteEditor<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>();
33
208
 
@@ -46,7 +221,7 @@ function CustomSlashMenu() {
46
221
  icon: <span className="bn-suggestion-icon">TS</span>,
47
222
  aliases: ["step", "test step", "expected"],
48
223
  onItemClick: () => {
49
- insertOrUpdateBlock(editor, {
224
+ const inserted = insertOrUpdateBlock(editor, {
50
225
  type: "testStep",
51
226
  props: {
52
227
  stepTitle: "",
@@ -54,6 +229,7 @@ function CustomSlashMenu() {
54
229
  expectedResult: "",
55
230
  },
56
231
  });
232
+ focusTestStepTitle(editor, inserted.id);
57
233
  },
58
234
  };
59
235
 
@@ -113,6 +289,9 @@ function App() {
113
289
  const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">(
114
290
  "idle",
115
291
  );
292
+ const [copyBlocksStatus, setCopyBlocksStatus] = useState<"idle" | "copied" | "failed">(
293
+ "idle",
294
+ );
116
295
 
117
296
  useEditorChange((editorInstance) => {
118
297
  try {
@@ -130,6 +309,67 @@ function App() {
130
309
  }
131
310
  }, editor);
132
311
 
312
+ useEffect(() => {
313
+ if (!editor) {
314
+ return;
315
+ }
316
+
317
+ const unsubscribe = editor.onChange((instance, context) => {
318
+ const changes = context.getChanges();
319
+ const newlyInsertedStep = changes.find(({ type, block, source }) => {
320
+ if (type !== "insert") {
321
+ return false;
322
+ }
323
+
324
+ if (source?.type === "yjs-remote") {
325
+ return false;
326
+ }
327
+
328
+ return block.type === "testStep" && ((block.props as any)?.stepTitle ?? "") === "";
329
+ });
330
+
331
+ if (newlyInsertedStep) {
332
+ focusTestStepTitle(instance, newlyInsertedStep.block.id);
333
+ }
334
+ });
335
+
336
+ return () => {
337
+ if (typeof unsubscribe === "function") {
338
+ unsubscribe();
339
+ }
340
+ };
341
+ }, [editor]);
342
+
343
+ const uploadStepImage = useMemo(
344
+ () => async (_image: Blob) => ({ url: `https://placehold.co/600x400?text=Uploaded+${Date.now()}` }),
345
+ [],
346
+ );
347
+
348
+ useEffect(() => {
349
+ // Demo defaults: configure global handlers so the editor works without manual providers.
350
+ setGlobalStepSuggestionsFetcher(() => DEMO_STEP_FIXTURES);
351
+
352
+ const handler = editor?.uploadFile
353
+ ? async (file: Blob) => {
354
+ const result = await editor.uploadFile!(file as File);
355
+ if (typeof result === "string") {
356
+ return { url: result };
357
+ }
358
+ if (result && typeof result === "object" && "url" in result && typeof (result as any).url === "string") {
359
+ return { url: (result as any).url as string };
360
+ }
361
+ throw new Error("uploadFile did not return a URL");
362
+ }
363
+ : uploadStepImage;
364
+
365
+ setImageUploadHandler(handler);
366
+
367
+ return () => {
368
+ setGlobalStepSuggestionsFetcher(null);
369
+ setImageUploadHandler(null);
370
+ };
371
+ }, [editor, uploadStepImage]);
372
+
133
373
  const createTestCaseBlock = useMemo<() => CustomPartialBlock>(() => {
134
374
  return () => ({
135
375
  type: "testCase",
@@ -173,7 +413,11 @@ function App() {
173
413
  return;
174
414
  }
175
415
 
176
- editor.insertBlocks([createBlock()], referenceId, "after");
416
+ const inserted = editor.insertBlocks([createBlock()], referenceId, "after");
417
+ const firstInserted = inserted[0];
418
+ if (firstInserted) {
419
+ focusTestStepTitle(editor, firstInserted.id);
420
+ }
177
421
  };
178
422
 
179
423
  const insertTestCase = () => insertBlockAfterSelection(createTestCaseBlock);
@@ -214,10 +458,6 @@ function App() {
214
458
  }
215
459
  };
216
460
 
217
- const [copyBlocksStatus, setCopyBlocksStatus] = useState<
218
- "idle" | "copied" | "failed"
219
- >("idle");
220
-
221
461
  const handleCopyBlocks = async () => {
222
462
  if (conversionError) {
223
463
  return;
@@ -317,6 +557,24 @@ function App() {
317
557
  <pre>{markdown}</pre>
318
558
  )}
319
559
  </div>
560
+ <div className="app__panel app__panel--light">
561
+ <div className="app__panel-header">
562
+ <h2>Autocomplete Steps</h2>
563
+ </div>
564
+ <p className="app__panel-text">
565
+ Start typing in the Step Title field to filter this list instantly.
566
+ </p>
567
+ <ol className="app__step-list">
568
+ {(DEMO_STEP_FIXTURES.data ?? []).map((step) => (
569
+ <li key={step.id ?? step.attributes?.title}>
570
+ <span className="app__step-title">{step.attributes?.title}</span>
571
+ {typeof step.attributes?.["usage-count"] === "number" && (
572
+ <span className="app__step-meta">{step.attributes?.["usage-count"]} uses</span>
573
+ )}
574
+ </li>
575
+ ))}
576
+ </ol>
577
+ </div>
320
578
  <div className="app__panel">
321
579
  <div className="app__panel-header">
322
580
  <h2>Blocks JSON</h2>
@@ -94,9 +94,9 @@ describe("blocksToMarkdown", () => {
94
94
  expect(blocksToMarkdown(blocks)).toBe(
95
95
  [
96
96
  "* Open the Login page.",
97
- " *Expected Result*: The Login page loads successfully.",
97
+ " *Expected*: The Login page loads successfully.",
98
98
  "* Enter a valid username.",
99
- " *Expected Result*: The username is accepted.",
99
+ " *Expected*: The username is accepted.",
100
100
  ].join("\n"),
101
101
  );
102
102
  });
@@ -119,7 +119,7 @@ describe("blocksToMarkdown", () => {
119
119
  expect(blocksToMarkdown(blocks)).toBe(
120
120
  [
121
121
  "* **Click** the _Login_ button",
122
- " *Expected Result*: **Success** is shown",
122
+ " *Expected*: **Success** is shown",
123
123
  " Second line with <u>underline</u>",
124
124
  ].join("\n"),
125
125
  );
@@ -140,15 +140,15 @@ describe("blocksToMarkdown", () => {
140
140
  },
141
141
  ];
142
142
 
143
- expect(blocksToMarkdown(blocks)).toBe(
144
- [
145
- "* Navigate to login",
146
- " Open browser",
147
- " Go to login page",
148
- " *Expected Result*: Login form visible",
149
- ].join("\n"),
150
- );
151
- });
143
+ expect(blocksToMarkdown(blocks)).toBe(
144
+ [
145
+ "* Navigate to login",
146
+ " Open browser",
147
+ " Go to login page",
148
+ " *Expected*: Login form visible",
149
+ ].join("\n"),
150
+ );
151
+ });
152
152
 
153
153
  it("serializes step data containing code fences, blank lines, and images", () => {
154
154
  const blocks: CustomEditorBlock[] = [
@@ -191,7 +191,7 @@ describe("blocksToMarkdown", () => {
191
191
  " asdsadas",
192
192
  " ```",
193
193
  " ![](/attachments/HMhkVtlDrO.png)",
194
- " *Expected Result*: The user receives a real-time notification for the order update.",
194
+ " *Expected*: The user receives a real-time notification for the order update.",
195
195
  ].join("\n"),
196
196
  );
197
197
  });
@@ -275,13 +275,28 @@ describe("blocksToMarkdown", () => {
275
275
  ].join("\n"),
276
276
  );
277
277
  });
278
+
279
+ it("parses a test step with inline image in the title, moving the image to step data", () => {
280
+ const markdown = [
281
+ "## Steps",
282
+ "* asdsadsad aaaaa asd ![](https://placehold.co/600x400?text=Uploaded+1763329962213)",
283
+ ].join("\n");
284
+
285
+ const blocks = markdownToBlocks(markdown);
286
+ const step = blocks.find((b) => b.type === "testStep") as any;
287
+
288
+ expect(step).toBeTruthy();
289
+ expect(step.props.stepTitle).toBe("asdsadsad aaaaa asd !");
290
+ expect(step.props.stepData).toBe("![](https://placehold.co/600x400?text=Uploaded+1763329962213)");
291
+ expect(step.props.expectedResult).toBe("");
292
+ });
278
293
  });
279
294
 
280
295
  describe("markdownToBlocks", () => {
281
296
  it("parses test steps and test cases", () => {
282
297
  const markdown = [
283
298
  "* Open the Login page.",
284
- " *Expected Result*: The Login page loads successfully.",
299
+ " *Expected*: The Login page loads successfully.",
285
300
  "",
286
301
  ":::test-case status=\"ready\" reference=\"QA-7\"",
287
302
  "Run the smoke tests.",
@@ -325,13 +340,13 @@ describe("markdownToBlocks", () => {
325
340
  "### Steps",
326
341
  "",
327
342
  "* Step 1: Send a chat message to the user.",
328
- "**Expected Result**: The user receives a real-time notification for the chat message.",
343
+ "**Expected**: The user receives a real-time notification for the chat message.",
329
344
  "* Step 2: Update an order status.",
330
- "**Expected Result**: The user receives a real-time notification for the order update.",
345
+ "**Expected**: The user receives a real-time notification for the order update.",
331
346
  "* Step 3: Send a file to the user.",
332
- "**Expected Result**: The user receives a real-time notification for the file received.",
347
+ "**Expected**: The user receives a real-time notification for the file received.",
333
348
  "* Step 4: Verify that the notifications are displayed correctly in the application's notification panel.",
334
- "**Expected Result**: All notifications (chat message, order update, file received) are listed in the notification panel with the correct information (e.g., timestamp, message content).",
349
+ "**Expected**: All notifications (chat message, order update, file received) are listed in the notification panel with the correct information (e.g., timestamp, message content).",
335
350
  "",
336
351
  "### Postconditions",
337
352
  "* The user has received and viewed the notifications.",
@@ -395,7 +410,7 @@ describe("markdownToBlocks", () => {
395
410
  " asdsadas",
396
411
  " ```",
397
412
  " ![](/attachments/HMhkVtlDrO.png)",
398
- "**Expected Result**: The user receives a real-time notification for the order update.",
413
+ "**Expected**: The user receives a real-time notification for the order update.",
399
414
  ].join("\n");
400
415
 
401
416
  const expectedData = [
@@ -452,7 +467,7 @@ describe("markdownToBlocks", () => {
452
467
  " asdsadas",
453
468
  " ```",
454
469
  " ![](/attachments/HMhkVtlDrO.png)",
455
- " *Expected Result*: The user receives a real-time notification for the order update.",
470
+ " *Expected*: The user receives a real-time notification for the order update.",
456
471
  ].join("\n"),
457
472
  );
458
473
  });
@@ -537,7 +552,7 @@ describe("markdownToBlocks", () => {
537
552
  it("parses expected result prefixes with emphasis", () => {
538
553
  const markdown = [
539
554
  "* Open the form.",
540
- " **Expected Result:** The form opens.",
555
+ " **Expected:** The form opens.",
541
556
  " Expected: Fields are empty.",
542
557
  ].join("\n");
543
558
 
@@ -559,7 +574,7 @@ describe("markdownToBlocks", () => {
559
574
  "* Navigate to login",
560
575
  " Open browser",
561
576
  " Go to login page",
562
- " *Expected Result*: Login form visible",
577
+ " *Expected*: Login form visible",
563
578
  ].join("\n");
564
579
 
565
580
  expect(markdownToBlocks(markdown)).toEqual([
@@ -580,7 +595,7 @@ describe("markdownToBlocks", () => {
580
595
  "* Prepare test fixtures",
581
596
  "Collect user accounts from staging.",
582
597
  "Reset passwords for all test accounts.",
583
- "*Expected Result*: Test accounts are ready for execution.",
598
+ "*Expected*: Test accounts are ready for execution.",
584
599
  ].join("\n");
585
600
 
586
601
  expect(markdownToBlocks(markdown)).toEqual([
@@ -599,7 +614,7 @@ describe("markdownToBlocks", () => {
599
614
  it("parses expected result containing a markdown image", () => {
600
615
  const markdown = [
601
616
  "* Display the generated report.",
602
- " *Expected Result*: ![](/attachments/report.png)",
617
+ " *Expected*: ![](/attachments/report.png)",
603
618
  ].join("\n");
604
619
 
605
620
  expect(markdownToBlocks(markdown)).toEqual([
@@ -667,7 +682,7 @@ describe("markdownToBlocks", () => {
667
682
  expect(roundTrip).toBe(
668
683
  [
669
684
  "* Should open login screen",
670
- " *Expected Result*: Login should look like this",
685
+ " *Expected*: Login should look like this",
671
686
  " ![](/login.png)",
672
687
  ].join("\n"),
673
688
  );
@@ -746,4 +761,23 @@ describe("markdownToBlocks", () => {
746
761
  },
747
762
  ]);
748
763
  });
764
+
765
+ it("parses expected result lines written with bold 'Expected Result' prefix for compatibility", () => {
766
+ const markdown = [
767
+ "* Step 1: Send a chat message to the user.",
768
+ "**Expected Result**: The user receives a real-time notification for the chat message.",
769
+ ].join("\n");
770
+
771
+ expect(markdownToBlocks(markdown)).toEqual([
772
+ {
773
+ type: "testStep",
774
+ props: {
775
+ stepTitle: "Step 1: Send a chat message to the user.",
776
+ stepData: "",
777
+ expectedResult: "The user receives a real-time notification for the chat message.",
778
+ },
779
+ children: [],
780
+ },
781
+ ]);
782
+ });
749
783
  });