testomatio-editor-blocks 0.1.1 → 0.2.0

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.
Files changed (41) hide show
  1. package/README.md +13 -11
  2. package/package/editor/blocks/markdown.d.ts +5 -0
  3. package/package/editor/blocks/markdown.js +160 -0
  4. package/package/editor/blocks/snippet.d.ts +38 -0
  5. package/package/editor/blocks/snippet.js +65 -0
  6. package/package/editor/blocks/step.d.ts +32 -0
  7. package/package/editor/blocks/step.js +97 -0
  8. package/package/editor/blocks/stepField.d.ts +26 -0
  9. package/package/editor/blocks/stepField.js +316 -0
  10. package/package/editor/customMarkdownConverter.js +111 -80
  11. package/package/editor/customSchema.d.ts +31 -45
  12. package/package/editor/customSchema.js +6 -616
  13. package/package/editor/snippetAutocomplete.d.ts +28 -0
  14. package/package/editor/snippetAutocomplete.js +94 -0
  15. package/package/editor/stepAutocomplete.d.ts +1 -1
  16. package/package/editor/stepAutocomplete.js +15 -2
  17. package/package/editor/stepImageUpload.d.ts +1 -1
  18. package/package/editor/stepImageUpload.js +4 -9
  19. package/package/index.d.ts +2 -2
  20. package/package/index.js +2 -2
  21. package/package/styles.css +57 -0
  22. package/package.json +1 -1
  23. package/src/App.tsx +161 -45
  24. package/src/editor/blocks/blocks.test.ts +22 -0
  25. package/src/editor/blocks/markdown.ts +199 -0
  26. package/src/editor/blocks/snippet.tsx +109 -0
  27. package/src/editor/blocks/step.tsx +175 -0
  28. package/src/editor/blocks/stepField.tsx +487 -0
  29. package/src/editor/customMarkdownConverter.test.ts +121 -36
  30. package/src/editor/customMarkdownConverter.ts +128 -85
  31. package/src/editor/customSchema.tsx +6 -935
  32. package/src/editor/snippetAutocomplete.test.ts +54 -0
  33. package/src/editor/snippetAutocomplete.ts +133 -0
  34. package/src/editor/stepAutocomplete.test.ts +20 -0
  35. package/src/editor/stepAutocomplete.tsx +15 -2
  36. package/src/editor/stepImageUpload.test.ts +25 -0
  37. package/src/editor/stepImageUpload.ts +11 -0
  38. package/src/editor/styles.css +57 -0
  39. package/src/index.ts +2 -2
  40. package/src/editor/customSchema.test.ts +0 -47
  41. package/src/editor/stepImageUpload.tsx +0 -19
@@ -0,0 +1,94 @@
1
+ import { useEffect, useState } from "react";
2
+ let globalFetcher = null;
3
+ let cachedSuggestions = [];
4
+ export function setSnippetFetcher(fetcher) {
5
+ globalFetcher = fetcher;
6
+ cachedSuggestions = [];
7
+ }
8
+ export function useSnippetAutocomplete() {
9
+ const [suggestions, setSuggestions] = useState(() => {
10
+ if (cachedSuggestions.length > 0) {
11
+ return cachedSuggestions;
12
+ }
13
+ if (globalFetcher) {
14
+ const result = globalFetcher();
15
+ if (!result || typeof result.then !== "function") {
16
+ const normalized = normalizeSnippetSuggestions(result);
17
+ cachedSuggestions = normalized;
18
+ return normalized;
19
+ }
20
+ }
21
+ return [];
22
+ });
23
+ useEffect(() => {
24
+ if (suggestions.length > 0) {
25
+ return;
26
+ }
27
+ if (!globalFetcher) {
28
+ return;
29
+ }
30
+ let cancelled = false;
31
+ Promise.resolve(globalFetcher())
32
+ .then((result) => normalizeSnippetSuggestions(result))
33
+ .then((items) => {
34
+ if (cancelled)
35
+ return;
36
+ cachedSuggestions = items;
37
+ setSuggestions(items);
38
+ })
39
+ .catch((error) => console.error("Failed to fetch snippet suggestions", error));
40
+ return () => {
41
+ cancelled = true;
42
+ };
43
+ }, [suggestions.length]);
44
+ return suggestions;
45
+ }
46
+ export function parseSnippetsFromJsonApi(document) {
47
+ const resources = Array.isArray(document) ? document : document === null || document === void 0 ? void 0 : document.data;
48
+ if (!Array.isArray(resources) || resources.length === 0) {
49
+ return [];
50
+ }
51
+ return resources
52
+ .map((resource) => normalizeJsonApiResource(resource))
53
+ .filter((value) => Boolean(value));
54
+ }
55
+ function normalizeSnippetSuggestions(snippets) {
56
+ if (!snippets)
57
+ return [];
58
+ if (Array.isArray(snippets)) {
59
+ if (snippets.length === 0)
60
+ return [];
61
+ if (isSnippetSuggestionArray(snippets))
62
+ return snippets;
63
+ return parseSnippetsFromJsonApi(snippets);
64
+ }
65
+ return parseSnippetsFromJsonApi(snippets);
66
+ }
67
+ function normalizeJsonApiResource(resource) {
68
+ var _a, _b, _c;
69
+ if (!resource)
70
+ return null;
71
+ const attrs = resource.attributes;
72
+ const id = resource.id;
73
+ const title = (_a = attrs === null || attrs === void 0 ? void 0 : attrs.title) !== null && _a !== void 0 ? _a : "";
74
+ if (!id || !title)
75
+ return null;
76
+ return {
77
+ id: String(id),
78
+ title: String(title),
79
+ body: (_b = attrs === null || attrs === void 0 ? void 0 : attrs.body) !== null && _b !== void 0 ? _b : null,
80
+ description: (_c = attrs === null || attrs === void 0 ? void 0 : attrs.description) !== null && _c !== void 0 ? _c : null,
81
+ usageCount: coerceNumber(attrs === null || attrs === void 0 ? void 0 : attrs["usage-count"]),
82
+ isSnippet: true,
83
+ };
84
+ }
85
+ function isSnippetSuggestionArray(value) {
86
+ var _a;
87
+ return Array.isArray(value) && value.length > 0 && typeof ((_a = value[0]) === null || _a === void 0 ? void 0 : _a.title) === "string";
88
+ }
89
+ function coerceNumber(value) {
90
+ if (value === null || value === undefined)
91
+ return null;
92
+ const num = Number(value);
93
+ return Number.isFinite(num) ? num : null;
94
+ }
@@ -29,7 +29,7 @@ export type StepJsonApiDocument = {
29
29
  };
30
30
  export type StepSuggestionsFetcher = () => Promise<StepInput> | StepInput;
31
31
  type StepInput = StepSuggestion[] | StepJsonApiDocument | StepJsonApiResource[] | null | undefined;
32
- export declare function setGlobalStepSuggestionsFetcher(fetcher: StepSuggestionsFetcher | null): void;
32
+ export declare function setStepsFetcher(fetcher: StepSuggestionsFetcher | null): void;
33
33
  export declare function useStepAutocomplete(): StepSuggestion[];
34
34
  export declare function parseStepsFromJsonApi(document: StepJsonApiDocument | StepJsonApiResource[] | null | undefined): StepSuggestion[];
35
35
  export {};
@@ -1,12 +1,25 @@
1
1
  import { useEffect, useState } from "react";
2
2
  let globalFetcher = null;
3
3
  let cachedSuggestions = [];
4
- export function setGlobalStepSuggestionsFetcher(fetcher) {
4
+ export function setStepsFetcher(fetcher) {
5
5
  globalFetcher = fetcher;
6
6
  cachedSuggestions = [];
7
7
  }
8
8
  export function useStepAutocomplete() {
9
- const [suggestions, setSuggestions] = useState(cachedSuggestions);
9
+ const [suggestions, setSuggestions] = useState(() => {
10
+ if (cachedSuggestions.length > 0) {
11
+ return cachedSuggestions;
12
+ }
13
+ if (globalFetcher) {
14
+ const result = globalFetcher();
15
+ if (!result || typeof result.then !== "function") {
16
+ const normalized = normalizeStepSuggestions(result);
17
+ cachedSuggestions = normalized;
18
+ return normalized;
19
+ }
20
+ }
21
+ return [];
22
+ });
10
23
  useEffect(() => {
11
24
  if (suggestions.length > 0) {
12
25
  return;
@@ -1,5 +1,5 @@
1
1
  export type StepImageUploadHandler = (image: Blob) => Promise<{
2
2
  url: string;
3
3
  }>;
4
- export declare function setGlobalStepImageUploadHandler(handler: StepImageUploadHandler | null): void;
4
+ export declare function setImageUploadHandler(handler: StepImageUploadHandler | null): void;
5
5
  export declare function useStepImageUpload(): StepImageUploadHandler | null;
@@ -1,12 +1,7 @@
1
- import { useEffect, useState } from "react";
2
- let globalUploadHandler = null;
3
- export function setGlobalStepImageUploadHandler(handler) {
4
- globalUploadHandler = handler;
1
+ let imageUploadHandler = null;
2
+ export function setImageUploadHandler(handler) {
3
+ imageUploadHandler = handler;
5
4
  }
6
5
  export function useStepImageUpload() {
7
- const [handler, setHandler] = useState(globalUploadHandler);
8
- useEffect(() => {
9
- setHandler(globalUploadHandler);
10
- }, []);
11
- return handler;
6
+ return imageUploadHandler;
12
7
  }
@@ -1,5 +1,5 @@
1
1
  export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
2
2
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
3
- export { useStepAutocomplete, parseStepsFromJsonApi, setGlobalStepSuggestionsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
4
- export { useStepImageUpload, setGlobalStepImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
3
+ export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
4
+ export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
5
5
  export declare const testomatioEditorClassName = "markdown testomatio-editor";
package/package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { customSchema, } from "./editor/customSchema";
2
2
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
3
- export { useStepAutocomplete, parseStepsFromJsonApi, setGlobalStepSuggestionsFetcher, } from "./editor/stepAutocomplete";
4
- export { useStepImageUpload, setGlobalStepImageUploadHandler, } from "./editor/stepImageUpload";
3
+ export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
4
+ export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
5
5
  export const testomatioEditorClassName = "markdown testomatio-editor";
@@ -112,6 +112,63 @@
112
112
  box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.08);
113
113
  }
114
114
 
115
+ .bn-snippet {
116
+ border-left-color: rgba(16, 185, 129, 0.75);
117
+ background: rgba(16, 185, 129, 0.12);
118
+ }
119
+
120
+ .bn-snippet::after {
121
+ box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.16);
122
+ }
123
+
124
+ .bn-snippet .bn-teststep__toggle {
125
+ border-color: rgba(16, 185, 129, 0.45);
126
+ background: rgba(16, 185, 129, 0.1);
127
+ color: #0f766e;
128
+ }
129
+
130
+ .bn-snippet .bn-teststep__toggle:hover {
131
+ background: rgba(16, 185, 129, 0.18);
132
+ border-color: rgba(16, 185, 129, 0.55);
133
+ box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.2);
134
+ }
135
+
136
+ .bn-snippet .bn-step-field__label {
137
+ color: #0f766e;
138
+ }
139
+
140
+ .bn-snippet .bn-step-toolbar__button {
141
+ border-color: rgba(16, 185, 129, 0.35);
142
+ color: #0f766e;
143
+ }
144
+
145
+ .bn-snippet .bn-step-toolbar__button:hover {
146
+ background: rgba(16, 185, 129, 0.12);
147
+ border-color: rgba(16, 185, 129, 0.55);
148
+ }
149
+
150
+ .bn-snippet .bn-step-editor {
151
+ border-color: rgba(16, 185, 129, 0.25);
152
+ }
153
+
154
+ .bn-snippet .bn-step-editor:focus-visible {
155
+ border-color: rgba(16, 185, 129, 0.7);
156
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
157
+ }
158
+
159
+ .bn-snippet .bn-step-suggestions {
160
+ border-color: rgba(16, 185, 129, 0.25);
161
+ }
162
+
163
+ .bn-snippet .bn-step-suggestion:hover,
164
+ .bn-snippet .bn-step-suggestion--active {
165
+ background: rgba(16, 185, 129, 0.1);
166
+ }
167
+
168
+ .bn-snippet .bn-step-suggestion__meta {
169
+ color: rgba(15, 118, 110, 0.65);
170
+ }
171
+
115
172
  .bn-teststep__toggle {
116
173
  align-self: flex-start;
117
174
  padding: 0.35rem 0.6rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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.tsx CHANGED
@@ -18,18 +18,23 @@ import {
18
18
  type CustomPartialBlock,
19
19
  } from "./editor/customMarkdownConverter";
20
20
  import { customSchema, type CustomEditor } from "./editor/customSchema";
21
- import { setGlobalStepSuggestionsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
22
- import { setGlobalStepImageUploadHandler } from "./editor/stepImageUpload";
21
+ import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
22
+ import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
23
+ import { setImageUploadHandler } from "./editor/stepImageUpload";
23
24
  import "./App.css";
24
25
 
25
- const focusTestStepTitle = (editor: CustomEditor | null | undefined, blockId?: string) => {
26
+ const focusStepField = (
27
+ editor: CustomEditor | null | undefined,
28
+ blockId?: string,
29
+ fieldName = "title",
30
+ ) => {
26
31
  if (!editor || !blockId) {
27
32
  return;
28
33
  }
29
34
 
30
35
  const focus = () => {
31
36
  const stepTitle = document.querySelector<HTMLElement>(
32
- `[data-block-id="${blockId}"] [data-step-field="title"]`,
37
+ `[data-block-id="${blockId}"] [data-step-field="${fieldName}"]`,
33
38
  );
34
39
 
35
40
  if (stepTitle) {
@@ -52,12 +57,6 @@ const focusTestStepTitle = (editor: CustomEditor | null | undefined, blockId?: s
52
57
 
53
58
  type Schema = typeof customSchema;
54
59
 
55
- const DEFAULT_BLOCK_PROPS = {
56
- textAlignment: "left" as const,
57
- textColor: "default" as const,
58
- backgroundColor: "default" as const,
59
- };
60
-
61
60
  const DEMO_STEP_FIXTURES: StepJsonApiDocument = {
62
61
  data: [
63
62
  {
@@ -200,6 +199,83 @@ const DEMO_STEP_FIXTURES: StepJsonApiDocument = {
200
199
  "comments-count": 0,
201
200
  },
202
201
  },
202
+ {
203
+ id: "301",
204
+ type: "step",
205
+ attributes: {
206
+ labels: ["snippet"],
207
+ title: "Open login page and wait for ready state",
208
+ kind: "snippet",
209
+ description: "Reusable login navigation snippet",
210
+ keywords: ["login", "auth"],
211
+ "is-snippet": true,
212
+ "usage-count": 41,
213
+ "comments-count": 2,
214
+ },
215
+ },
216
+ {
217
+ id: "302",
218
+ type: "step",
219
+ attributes: {
220
+ labels: ["snippet"],
221
+ title: "Fill credentials with provided user object",
222
+ kind: "snippet",
223
+ description: "Populate form fields from test data",
224
+ keywords: ["form", "user"],
225
+ "is-snippet": true,
226
+ "usage-count": 35,
227
+ "comments-count": 1,
228
+ },
229
+ },
230
+ {
231
+ id: "303",
232
+ type: "step",
233
+ attributes: {
234
+ labels: ["snippet"],
235
+ title: "Verify toast message disappears",
236
+ kind: "snippet",
237
+ description: "Shared assertion for ephemeral UI",
238
+ keywords: ["toast", "assertion"],
239
+ "is-snippet": true,
240
+ "usage-count": 18,
241
+ "comments-count": 0,
242
+ },
243
+ },
244
+ ],
245
+ };
246
+
247
+ const DEMO_SNIPPET_FIXTURES: SnippetJsonApiDocument = {
248
+ data: [
249
+ {
250
+ id: "501",
251
+ type: "snippet",
252
+ attributes: {
253
+ title: "Login setup",
254
+ body: "Open /login\nWait for form to render\nEnsure no console errors",
255
+ description: "Navigate to login and wait for readiness",
256
+ "usage-count": 12,
257
+ },
258
+ },
259
+ {
260
+ id: "502",
261
+ type: "snippet",
262
+ attributes: {
263
+ title: "Fill credentials",
264
+ body: "Type email\nType password\nClick Sign In",
265
+ description: "Reusable credentials filler",
266
+ "usage-count": 9,
267
+ },
268
+ },
269
+ {
270
+ id: "503",
271
+ type: "snippet",
272
+ attributes: {
273
+ title: "Verify toast disappears",
274
+ body: "Assert toast visible\nWait 3s\nAssert toast removed",
275
+ description: "Shared assertion for ephemeral notifications",
276
+ "usage-count": 7,
277
+ },
278
+ },
203
279
  ],
204
280
  };
205
281
 
@@ -229,11 +305,32 @@ function CustomSlashMenu() {
229
305
  expectedResult: "",
230
306
  },
231
307
  });
232
- focusTestStepTitle(editor, inserted.id);
308
+ focusStepField(editor, inserted.id, "title");
309
+ },
310
+ };
311
+
312
+ const snippetItem = {
313
+ key: "snippet" as any,
314
+ title: "Snippet",
315
+ subtext: "Insert a reusable snippet with data and an expected result",
316
+ group: "Test documentation",
317
+ icon: <span className="bn-suggestion-icon">SN</span>,
318
+ aliases: ["snippet", "reusable step"],
319
+ onItemClick: () => {
320
+ const inserted = insertOrUpdateBlock(editor, {
321
+ type: "snippet",
322
+ props: {
323
+ snippetId: "",
324
+ snippetTitle: "",
325
+ snippetData: "",
326
+ snippetExpectedResult: "",
327
+ },
328
+ });
329
+ focusStepField(editor, inserted.id, "snippet-title");
233
330
  },
234
331
  };
235
332
 
236
- return filterSuggestionItems([...defaultItems, stepItem], query);
333
+ return filterSuggestionItems([...defaultItems, stepItem, snippetItem], query);
237
334
  };
238
335
 
239
336
  return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
@@ -316,7 +413,7 @@ function App() {
316
413
 
317
414
  const unsubscribe = editor.onChange((instance, context) => {
318
415
  const changes = context.getChanges();
319
- const newlyInsertedStep = changes.find(({ type, block, source }) => {
416
+ const newlyInsertedAction = changes.find(({ type, block, source }) => {
320
417
  if (type !== "insert") {
321
418
  return false;
322
419
  }
@@ -325,11 +422,20 @@ function App() {
325
422
  return false;
326
423
  }
327
424
 
328
- return block.type === "testStep" && ((block.props as any)?.stepTitle ?? "") === "";
425
+ if (block.type === "testStep") {
426
+ return ((block.props as any)?.stepTitle ?? "") === "";
427
+ }
428
+
429
+ if (block.type === "snippet") {
430
+ return ((block.props as any)?.snippetTitle ?? "") === "";
431
+ }
432
+
433
+ return false;
329
434
  });
330
435
 
331
- if (newlyInsertedStep) {
332
- focusTestStepTitle(instance, newlyInsertedStep.block.id);
436
+ if (newlyInsertedAction) {
437
+ const fieldName = newlyInsertedAction.block.type === "snippet" ? "snippet-title" : "title";
438
+ focusStepField(instance, newlyInsertedAction.block.id, fieldName);
333
439
  }
334
440
  });
335
441
 
@@ -347,47 +453,57 @@ function App() {
347
453
 
348
454
  useEffect(() => {
349
455
  // Demo defaults: configure global handlers so the editor works without manual providers.
350
- setGlobalStepSuggestionsFetcher(() => DEMO_STEP_FIXTURES);
351
- setGlobalStepImageUploadHandler(uploadStepImage);
456
+ setStepsFetcher(() => DEMO_STEP_FIXTURES);
457
+ setSnippetFetcher(() => DEMO_SNIPPET_FIXTURES);
458
+
459
+ const handler = editor?.uploadFile
460
+ ? async (file: Blob) => {
461
+ const result = await editor.uploadFile!(file as File);
462
+ if (typeof result === "string") {
463
+ return { url: result };
464
+ }
465
+ if (result && typeof result === "object" && "url" in result && typeof (result as any).url === "string") {
466
+ return { url: (result as any).url as string };
467
+ }
468
+ throw new Error("uploadFile did not return a URL");
469
+ }
470
+ : uploadStepImage;
471
+
472
+ setImageUploadHandler(handler);
352
473
 
353
474
  return () => {
354
- setGlobalStepSuggestionsFetcher(null);
355
- setGlobalStepImageUploadHandler(null);
475
+ setStepsFetcher(null);
476
+ setSnippetFetcher(null);
477
+ setImageUploadHandler(null);
356
478
  };
357
- }, [uploadStepImage]);
479
+ }, [editor, uploadStepImage]);
358
480
 
359
- const createTestCaseBlock = useMemo<() => CustomPartialBlock>(() => {
481
+ const createTestStepBlock = useMemo<() => CustomPartialBlock>(() => {
360
482
  return () => ({
361
- type: "testCase",
483
+ type: "testStep",
362
484
  props: {
363
- ...DEFAULT_BLOCK_PROPS,
364
- status: "draft",
365
- reference: "",
485
+ stepTitle: "",
486
+ stepData: "",
487
+ expectedResult: "",
366
488
  },
367
- content: [
368
- {
369
- type: "text",
370
- text: "Write the expected result, steps, and assertions here…",
371
- styles: {},
372
- },
373
- ],
374
489
  children: [],
375
490
  });
376
491
  }, []);
377
492
 
378
- const createTestStepBlock = useMemo<() => CustomPartialBlock>(() => {
493
+ const createSnippetBlock = useMemo<() => CustomPartialBlock>(() => {
379
494
  return () => ({
380
- type: "testStep",
495
+ type: "snippet",
381
496
  props: {
382
- stepTitle: "",
383
- stepData: "",
384
- expectedResult: "",
497
+ snippetId: "",
498
+ snippetTitle: "",
499
+ snippetData: "",
500
+ snippetExpectedResult: "",
385
501
  },
386
502
  children: [],
387
503
  });
388
504
  }, []);
389
505
 
390
- const insertBlockAfterSelection = (createBlock: () => CustomPartialBlock) => {
506
+ const insertBlockAfterSelection = (createBlock: () => CustomPartialBlock, focusFieldName?: string) => {
391
507
  const selection = editor.getSelection();
392
508
  const selectedBlocks = selection?.blocks ?? [];
393
509
  const selectedBlock = selectedBlocks[selectedBlocks.length - 1];
@@ -401,13 +517,13 @@ function App() {
401
517
 
402
518
  const inserted = editor.insertBlocks([createBlock()], referenceId, "after");
403
519
  const firstInserted = inserted[0];
404
- if (firstInserted) {
405
- focusTestStepTitle(editor, firstInserted.id);
520
+ if (firstInserted && focusFieldName) {
521
+ focusStepField(editor, firstInserted.id, focusFieldName);
406
522
  }
407
523
  };
408
524
 
409
- const insertTestCase = () => insertBlockAfterSelection(createTestCaseBlock);
410
- const insertTestStep = () => insertBlockAfterSelection(createTestStepBlock);
525
+ const insertTestStep = () => insertBlockAfterSelection(createTestStepBlock, "title");
526
+ const insertSnippet = () => insertBlockAfterSelection(createSnippetBlock, "snippet-title");
411
527
 
412
528
  const handleCopyMarkdown = async () => {
413
529
  if (conversionError) {
@@ -495,9 +611,9 @@ function App() {
495
611
  <button
496
612
  type="button"
497
613
  className="app__action app__action--ghost"
498
- onClick={insertTestCase}
614
+ onClick={insertSnippet}
499
615
  >
500
- Insert Test Case
616
+ Insert Snippet
501
617
  </button>
502
618
  </div>
503
619
  </header>
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { customSchema } from "../customSchema";
3
+
4
+ describe("custom block specs", () => {
5
+ it("registers the step block", () => {
6
+ const step = customSchema.blockSpecs.testStep;
7
+ expect(step).toBeDefined();
8
+ expect((step as any).config?.type ?? (step as any).type).toBe("testStep");
9
+ expect((step as any).config?.propSchema?.stepTitle?.default).toBe("");
10
+ expect((step as any).config?.propSchema?.stepData?.default).toBe("");
11
+ expect((step as any).config?.propSchema?.expectedResult?.default).toBe("");
12
+ });
13
+
14
+ it("registers the snippet block", () => {
15
+ const snippet = customSchema.blockSpecs.snippet;
16
+ expect(snippet).toBeDefined();
17
+ expect((snippet as any).config?.type ?? (snippet as any).type).toBe("snippet");
18
+ expect((snippet as any).config?.propSchema?.snippetId?.default).toBe("");
19
+ expect((snippet as any).config?.propSchema?.snippetTitle?.default).toBe("");
20
+ expect((snippet as any).config?.propSchema?.snippetData?.default).toBe("");
21
+ });
22
+ });