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.
- package/README.md +13 -11
- package/package/editor/blocks/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +160 -0
- package/package/editor/blocks/snippet.d.ts +38 -0
- package/package/editor/blocks/snippet.js +65 -0
- package/package/editor/blocks/step.d.ts +32 -0
- package/package/editor/blocks/step.js +97 -0
- package/package/editor/blocks/stepField.d.ts +26 -0
- package/package/editor/blocks/stepField.js +316 -0
- package/package/editor/customMarkdownConverter.js +111 -80
- package/package/editor/customSchema.d.ts +31 -45
- package/package/editor/customSchema.js +6 -616
- package/package/editor/snippetAutocomplete.d.ts +28 -0
- package/package/editor/snippetAutocomplete.js +94 -0
- package/package/editor/stepAutocomplete.d.ts +1 -1
- package/package/editor/stepAutocomplete.js +15 -2
- package/package/editor/stepImageUpload.d.ts +1 -1
- package/package/editor/stepImageUpload.js +4 -9
- package/package/index.d.ts +2 -2
- package/package/index.js +2 -2
- package/package/styles.css +57 -0
- package/package.json +1 -1
- package/src/App.tsx +161 -45
- package/src/editor/blocks/blocks.test.ts +22 -0
- package/src/editor/blocks/markdown.ts +199 -0
- package/src/editor/blocks/snippet.tsx +109 -0
- package/src/editor/blocks/step.tsx +175 -0
- package/src/editor/blocks/stepField.tsx +487 -0
- package/src/editor/customMarkdownConverter.test.ts +121 -36
- package/src/editor/customMarkdownConverter.ts +128 -85
- package/src/editor/customSchema.tsx +6 -935
- package/src/editor/snippetAutocomplete.test.ts +54 -0
- package/src/editor/snippetAutocomplete.ts +133 -0
- package/src/editor/stepAutocomplete.test.ts +20 -0
- package/src/editor/stepAutocomplete.tsx +15 -2
- package/src/editor/stepImageUpload.test.ts +25 -0
- package/src/editor/stepImageUpload.ts +11 -0
- package/src/editor/styles.css +57 -0
- package/src/index.ts +2 -2
- package/src/editor/customSchema.test.ts +0 -47
- 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
|
|
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
|
|
4
|
+
export function setStepsFetcher(fetcher) {
|
|
5
5
|
globalFetcher = fetcher;
|
|
6
6
|
cachedSuggestions = [];
|
|
7
7
|
}
|
|
8
8
|
export function useStepAutocomplete() {
|
|
9
|
-
const [suggestions, setSuggestions] = useState(
|
|
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
|
|
4
|
+
export declare function setImageUploadHandler(handler: StepImageUploadHandler | null): void;
|
|
5
5
|
export declare function useStepImageUpload(): StepImageUploadHandler | null;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
globalUploadHandler = handler;
|
|
1
|
+
let imageUploadHandler = null;
|
|
2
|
+
export function setImageUploadHandler(handler) {
|
|
3
|
+
imageUploadHandler = handler;
|
|
5
4
|
}
|
|
6
5
|
export function useStepImageUpload() {
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
setHandler(globalUploadHandler);
|
|
10
|
-
}, []);
|
|
11
|
-
return handler;
|
|
6
|
+
return imageUploadHandler;
|
|
12
7
|
}
|
package/package/index.d.ts
CHANGED
|
@@ -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,
|
|
4
|
-
export { useStepImageUpload,
|
|
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,
|
|
4
|
-
export { useStepImageUpload,
|
|
3
|
+
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
|
|
4
|
+
export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
|
|
5
5
|
export const testomatioEditorClassName = "markdown testomatio-editor";
|
package/package/styles.css
CHANGED
|
@@ -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
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 {
|
|
22
|
-
import {
|
|
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
|
|
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="
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
332
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
355
|
-
|
|
475
|
+
setStepsFetcher(null);
|
|
476
|
+
setSnippetFetcher(null);
|
|
477
|
+
setImageUploadHandler(null);
|
|
356
478
|
};
|
|
357
|
-
}, [uploadStepImage]);
|
|
479
|
+
}, [editor, uploadStepImage]);
|
|
358
480
|
|
|
359
|
-
const
|
|
481
|
+
const createTestStepBlock = useMemo<() => CustomPartialBlock>(() => {
|
|
360
482
|
return () => ({
|
|
361
|
-
type: "
|
|
483
|
+
type: "testStep",
|
|
362
484
|
props: {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
493
|
+
const createSnippetBlock = useMemo<() => CustomPartialBlock>(() => {
|
|
379
494
|
return () => ({
|
|
380
|
-
type: "
|
|
495
|
+
type: "snippet",
|
|
381
496
|
props: {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
520
|
+
if (firstInserted && focusFieldName) {
|
|
521
|
+
focusStepField(editor, firstInserted.id, focusFieldName);
|
|
406
522
|
}
|
|
407
523
|
};
|
|
408
524
|
|
|
409
|
-
const
|
|
410
|
-
const
|
|
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={
|
|
614
|
+
onClick={insertSnippet}
|
|
499
615
|
>
|
|
500
|
-
Insert
|
|
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
|
+
});
|