@portabletext/editor 2.12.0 → 2.12.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.
@@ -0,0 +1,779 @@
1
+ import {defineSchema} from '@portabletext/schema'
2
+ import {getTersePt, parseTersePt} from '@portabletext/test'
3
+ import {userEvent} from '@vitest/browser/context'
4
+ import {Given, Then, When} from 'racejar'
5
+ import {assert, expect, vi} from 'vitest'
6
+ import {getEditorSelection} from '../../internal-utils/editor-selection'
7
+ import {
8
+ parseBlocks,
9
+ parseInlineObject,
10
+ parseSpan,
11
+ } from '../../internal-utils/parse-blocks'
12
+ import {getSelectionText} from '../../internal-utils/selection-text'
13
+ import {getTextBlockKey} from '../../internal-utils/text-block-key'
14
+ import {getTextMarks} from '../../internal-utils/text-marks'
15
+ import {
16
+ getSelectionAfterText,
17
+ getSelectionBeforeText,
18
+ getTextSelection,
19
+ } from '../../internal-utils/text-selection'
20
+ import {getValueAnnotations} from '../../internal-utils/value-annotations'
21
+ import {createTestEditor} from '../../test/vitest'
22
+ import {
23
+ reverseSelection,
24
+ selectionPointToBlockOffset,
25
+ spanSelectionPointToBlockOffset,
26
+ } from '../../utils'
27
+ import type {Parameter} from '../gherkin-parameter-types'
28
+ import type {Context} from './step-context'
29
+
30
+ /**
31
+ * @internal
32
+ */
33
+ export const stepDefinitions = [
34
+ Given('one editor', async (context: Context) => {
35
+ const {editor, locator} = await createTestEditor({
36
+ schemaDefinition: defineSchema({
37
+ annotations: [{name: 'comment'}, {name: 'link'}],
38
+ decorators: [{name: 'em'}, {name: 'strong'}],
39
+ blockObjects: [{name: 'image'}, {name: 'break'}],
40
+ inlineObjects: [{name: 'stock-ticker'}],
41
+ lists: [{name: 'bullet'}, {name: 'number'}],
42
+ styles: [
43
+ {name: 'normal'},
44
+ {name: 'h1'},
45
+ {name: 'h2'},
46
+ {name: 'h3'},
47
+ {name: 'h4'},
48
+ {name: 'h5'},
49
+ {name: 'h6'},
50
+ {name: 'blockquote'},
51
+ ],
52
+ }),
53
+ })
54
+
55
+ context.locator = locator
56
+ context.editor = editor
57
+ }),
58
+
59
+ Given('a global keymap', (context: Context) => {
60
+ context.keyMap = new Map()
61
+ }),
62
+
63
+ Given('the editor is focused', async (context: Context) => {
64
+ context.editor.send({
65
+ type: 'focus',
66
+ })
67
+
68
+ await vi.waitFor(() => {
69
+ const selection = context.editor.getSnapshot().context.selection
70
+ expect(selection).not.toBeNull()
71
+ })
72
+ }),
73
+
74
+ Given(
75
+ 'the text {terse-pt}',
76
+ (context: Context, tersePt: Parameter['tersePt']) => {
77
+ const blocks = parseTersePt(
78
+ {
79
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
80
+ schema: context.editor.getSnapshot().context.schema,
81
+ },
82
+ tersePt,
83
+ )
84
+
85
+ context.editor.send({
86
+ type: 'insert.blocks',
87
+ blocks,
88
+ placement: 'auto',
89
+ select: 'end',
90
+ })
91
+ },
92
+ ),
93
+
94
+ /**
95
+ * Block steps
96
+ */
97
+ Given(
98
+ 'blocks {placement}',
99
+ (context: Context, placement: Parameter['placement'], blocks: string) => {
100
+ context.editor.send({
101
+ type: 'insert.blocks',
102
+ blocks: parseBlocks({
103
+ context: {
104
+ schema: context.editor.getSnapshot().context.schema,
105
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
106
+ },
107
+ blocks: JSON.parse(blocks),
108
+ options: {
109
+ removeUnusedMarkDefs: false,
110
+ validateFields: true,
111
+ },
112
+ }),
113
+ placement,
114
+ })
115
+ },
116
+ ),
117
+
118
+ /**
119
+ * Child steps
120
+ */
121
+ When('a child is inserted', (context: Context, child: string) => {
122
+ const parsedChild =
123
+ parseSpan({
124
+ context: {
125
+ schema: context.editor.getSnapshot().context.schema,
126
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
127
+ },
128
+ span: JSON.parse(child),
129
+ markDefKeyMap: new Map(),
130
+ options: {validateFields: true},
131
+ }) ??
132
+ parseInlineObject({
133
+ context: {
134
+ schema: context.editor.getSnapshot().context.schema,
135
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
136
+ },
137
+ inlineObject: JSON.parse(child),
138
+ options: {validateFields: true},
139
+ })
140
+
141
+ if (!parsedChild) {
142
+ throw new Error(`Unable to parse child ${child}`)
143
+ }
144
+
145
+ context.editor.send({
146
+ type: 'insert.child',
147
+ child: parsedChild,
148
+ })
149
+ }),
150
+ When(
151
+ 'a(n) {inline-object} is inserted',
152
+ (context: Context, inlineObject: Parameter['inlineObject']) => {
153
+ context.editor.send({
154
+ type: 'insert.inline object',
155
+ inlineObject: {
156
+ name: inlineObject,
157
+ value: {},
158
+ },
159
+ })
160
+ },
161
+ ),
162
+
163
+ /**
164
+ * Text steps
165
+ */
166
+ Given(
167
+ 'a block {key} with text {text}',
168
+ (context: Context, key: string, text: Parameter['text']) => {
169
+ context.editor.send({
170
+ type: 'insert.block',
171
+ block: {
172
+ _key: key,
173
+ _type: 'block',
174
+ children: [{_type: 'span', text, marks: []}],
175
+ },
176
+ placement: 'auto',
177
+ select: 'end',
178
+ })
179
+ },
180
+ ),
181
+ When('{string} is typed', async (context: Context, text: string) => {
182
+ await userEvent.type(context.locator, text)
183
+ }),
184
+ When('{string} is inserted', (context: Context, text: string) => {
185
+ context.editor.send({type: 'insert.text', text})
186
+ }),
187
+ Then(
188
+ '{terse-pt} is in block {key}',
189
+ (context: Context, text: Array<string>, key: string) => {
190
+ const string = text.at(0)
191
+
192
+ if (string === undefined) {
193
+ assert.fail('Expected at least one text string')
194
+ }
195
+
196
+ if (text.length > 1) {
197
+ assert.fail('Expected at most one text string')
198
+ }
199
+
200
+ expect(
201
+ getTextBlockKey(context.editor.getSnapshot().context, string),
202
+ ).toBe(key)
203
+ },
204
+ ),
205
+
206
+ /**
207
+ * Button steps
208
+ */
209
+ When(
210
+ '{button} is pressed',
211
+ async (_: Context, button: Parameter['button']) => {
212
+ await userEvent.keyboard(button)
213
+ },
214
+ ),
215
+ When(
216
+ '{button} is pressed {int} times',
217
+ async (_: Context, button: Parameter['button'], times: number) => {
218
+ for (let i = 0; i < times; i++) {
219
+ await userEvent.keyboard(button)
220
+ }
221
+ },
222
+ ),
223
+
224
+ /**
225
+ * Selection steps
226
+ */
227
+ When(
228
+ 'the caret is put before {string}',
229
+ async (context: Context, text: string) => {
230
+ await vi.waitFor(() => {
231
+ const selection = getSelectionBeforeText(
232
+ context.editor.getSnapshot().context,
233
+ text,
234
+ )
235
+ expect(selection).not.toBeNull()
236
+
237
+ context.editor.send({
238
+ type: 'select',
239
+ at: selection,
240
+ })
241
+ })
242
+ },
243
+ ),
244
+ Then(
245
+ 'the caret is before {string}',
246
+ async (context: Context, text: string) => {
247
+ await vi.waitFor(() => {
248
+ const selection = getSelectionBeforeText(
249
+ context.editor.getSnapshot().context,
250
+ text,
251
+ )
252
+ expect(selection).not.toBeNull()
253
+ expect(context.editor.getSnapshot().context.selection).toEqual(
254
+ selection,
255
+ )
256
+ })
257
+ },
258
+ ),
259
+ When(
260
+ 'the caret is put after {string}',
261
+ async (context: Context, text: string) => {
262
+ await vi.waitFor(() => {
263
+ const selection = getSelectionAfterText(
264
+ context.editor.getSnapshot().context,
265
+ text,
266
+ )
267
+ expect(selection).not.toBeNull()
268
+
269
+ context.editor.send({
270
+ type: 'select',
271
+ at: getSelectionAfterText(context.editor.getSnapshot().context, text),
272
+ })
273
+ })
274
+ },
275
+ ),
276
+ Then(
277
+ 'the caret is after {string}',
278
+ async (context: Context, text: string) => {
279
+ await vi.waitFor(() => {
280
+ const selection = getSelectionAfterText(
281
+ context.editor.getSnapshot().context,
282
+ text,
283
+ )
284
+ expect(selection).not.toBeNull()
285
+ expect(context.editor.getSnapshot().context.selection).toEqual(
286
+ selection,
287
+ )
288
+ })
289
+ },
290
+ ),
291
+ Then('nothing is selected', async (context: Context) => {
292
+ await vi.waitFor(() => {
293
+ expect(context.editor.getSnapshot().context.selection).toBeNull()
294
+ })
295
+ }),
296
+ When('everything is selected', (context: Context) => {
297
+ const editorSelection = getEditorSelection(
298
+ context.editor.getSnapshot().context,
299
+ )
300
+
301
+ context.editor.send({
302
+ type: 'select',
303
+ at: editorSelection,
304
+ })
305
+ }),
306
+ When('everything is selected backwards', (context: Context) => {
307
+ const editorSelection = reverseSelection(
308
+ getEditorSelection(context.editor.getSnapshot().context),
309
+ )
310
+
311
+ context.editor.send({
312
+ type: 'select',
313
+ at: editorSelection,
314
+ })
315
+ }),
316
+ When('{string} is selected', async (context: Context, text: string) => {
317
+ await vi.waitFor(() => {
318
+ const selection = getTextSelection(
319
+ context.editor.getSnapshot().context,
320
+ text,
321
+ )
322
+ expect(selection).not.toBeNull()
323
+
324
+ context.editor.send({
325
+ type: 'select',
326
+ at: selection,
327
+ })
328
+ })
329
+ }),
330
+ When(
331
+ '{string} is selected backwards',
332
+ async (context: Context, text: string) => {
333
+ await vi.waitFor(() => {
334
+ const selection = reverseSelection(
335
+ getTextSelection(context.editor.getSnapshot().context, text),
336
+ )
337
+ expect(selection).not.toBeNull()
338
+
339
+ context.editor.send({
340
+ type: 'select',
341
+ at: selection,
342
+ })
343
+ })
344
+ },
345
+ ),
346
+ Then(
347
+ '{terse-pt} is selected',
348
+ async (context: Context, text: Array<string>) => {
349
+ await vi.waitFor(() => {
350
+ expect(
351
+ getSelectionText(context.editor.getSnapshot().context),
352
+ 'Unexpected selection',
353
+ ).toEqual(text)
354
+ })
355
+ },
356
+ ),
357
+
358
+ When(
359
+ '{terse-pt} is inserted at {placement}',
360
+ (
361
+ context: Context,
362
+ tersePt: Parameter['tersePt'],
363
+ placement: Parameter['placement'],
364
+ ) => {
365
+ const blocks = parseTersePt(
366
+ {
367
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
368
+ schema: context.editor.getSnapshot().context.schema,
369
+ },
370
+ tersePt,
371
+ )
372
+
373
+ context.editor.send({
374
+ type: 'insert.blocks',
375
+ blocks,
376
+ placement,
377
+ })
378
+ },
379
+ ),
380
+ When(
381
+ '{terse-pt} is inserted at {placement} and selected at the {select-position}',
382
+ (
383
+ context: Context,
384
+ tersePt: Parameter['tersePt'],
385
+ placement: Parameter['placement'],
386
+ selectPosition: Parameter['selectPosition'],
387
+ ) => {
388
+ const blocks = parseTersePt(
389
+ {
390
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
391
+ schema: context.editor.getSnapshot().context.schema,
392
+ },
393
+ tersePt,
394
+ )
395
+
396
+ context.editor.send({
397
+ type: 'insert.blocks',
398
+ blocks,
399
+ placement,
400
+ select: selectPosition,
401
+ })
402
+ },
403
+ ),
404
+ When(
405
+ 'blocks are inserted at {placement} and selected at the {select-position}',
406
+ (
407
+ context: Context,
408
+ placement: Parameter['placement'],
409
+ selectPosition: Parameter['selectPosition'],
410
+ blocks: string,
411
+ ) => {
412
+ context.editor.send({
413
+ type: 'insert.blocks',
414
+ blocks: parseBlocks({
415
+ context: {
416
+ schema: context.editor.getSnapshot().context.schema,
417
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
418
+ },
419
+ blocks: JSON.parse(blocks),
420
+ options: {
421
+ removeUnusedMarkDefs: false,
422
+ validateFields: true,
423
+ },
424
+ }),
425
+ placement,
426
+ select: selectPosition,
427
+ })
428
+ },
429
+ ),
430
+
431
+ Then(
432
+ 'the text is {terse-pt}',
433
+ async (context: Context, tersePt: Parameter['tersePt']) => {
434
+ await vi.waitFor(() => {
435
+ expect(
436
+ getTersePt(context.editor.getSnapshot().context),
437
+ 'Unexpected editor text',
438
+ ).toEqual(tersePt)
439
+ })
440
+ },
441
+ ),
442
+
443
+ /**
444
+ * Annotation steps
445
+ */
446
+ Given(
447
+ 'a(n) {annotation} {keyKeys} around {string}',
448
+ async (
449
+ context: Context,
450
+ annotation: Parameter['annotation'],
451
+ keyKeys: Array<string>,
452
+ text: string,
453
+ ) => {
454
+ await vi.waitFor(() => {
455
+ const selection = getTextSelection(
456
+ context.editor.getSnapshot().context,
457
+ text,
458
+ )
459
+ expect(selection).not.toBeNull()
460
+
461
+ context.editor.send({
462
+ type: 'select',
463
+ at: selection,
464
+ })
465
+ })
466
+
467
+ const value = context.editor.getSnapshot().context.value
468
+ const priorAnnotationKeys = getValueAnnotations(
469
+ context.editor.getSnapshot().context.schema,
470
+ value,
471
+ )
472
+
473
+ context.editor.send({
474
+ type: 'annotation.toggle',
475
+ annotation: {
476
+ name: annotation,
477
+ value: {},
478
+ },
479
+ })
480
+
481
+ let newAnnotationKeys: Array<string> = []
482
+
483
+ await vi.waitFor(() => {
484
+ const newValue = context.editor.getSnapshot().context.value
485
+
486
+ expect(priorAnnotationKeys).not.toEqual(
487
+ getValueAnnotations(
488
+ context.editor.getSnapshot().context.schema,
489
+ newValue,
490
+ ),
491
+ )
492
+
493
+ newAnnotationKeys = getValueAnnotations(
494
+ context.editor.getSnapshot().context.schema,
495
+ newValue,
496
+ ).filter(
497
+ (newAnnotationKey) => !priorAnnotationKeys.includes(newAnnotationKey),
498
+ )
499
+ })
500
+
501
+ if (newAnnotationKeys.length !== keyKeys.length) {
502
+ assert.fail(
503
+ `Expected ${keyKeys.length} new annotation keys, but got ${newAnnotationKeys.length}`,
504
+ )
505
+ }
506
+
507
+ keyKeys.forEach((keyKey, index) => {
508
+ context.keyMap?.set(keyKey, newAnnotationKeys[index])
509
+ })
510
+ },
511
+ ),
512
+ When(
513
+ '{annotation} is toggled',
514
+ (context: Context, annotation: Parameter['annotation']) => {
515
+ context.editor.send({
516
+ type: 'annotation.toggle',
517
+ annotation: {
518
+ name: annotation,
519
+ value: {},
520
+ },
521
+ })
522
+ },
523
+ ),
524
+ When(
525
+ '{annotation} {keyKeys} is toggled',
526
+ async (
527
+ context: Context,
528
+ annotation: Parameter['annotation'],
529
+ keyKeys: Array<string>,
530
+ ) => {
531
+ const value = context.editor.getSnapshot().context.value
532
+ const priorAnnotationKeys = getValueAnnotations(
533
+ context.editor.getSnapshot().context.schema,
534
+ value,
535
+ )
536
+
537
+ context.editor.send({
538
+ type: 'annotation.toggle',
539
+ annotation: {
540
+ name: annotation,
541
+ value: {},
542
+ },
543
+ })
544
+
545
+ let newAnnotationKeys: Array<string> = []
546
+
547
+ await vi.waitFor(() => {
548
+ const newValue = context.editor.getSnapshot().context.value
549
+
550
+ expect(priorAnnotationKeys).not.toEqual(
551
+ getValueAnnotations(
552
+ context.editor.getSnapshot().context.schema,
553
+ newValue,
554
+ ),
555
+ )
556
+
557
+ newAnnotationKeys = getValueAnnotations(
558
+ context.editor.getSnapshot().context.schema,
559
+ newValue,
560
+ ).filter(
561
+ (newAnnotationKey) => !priorAnnotationKeys.includes(newAnnotationKey),
562
+ )
563
+ })
564
+
565
+ if (newAnnotationKeys.length !== keyKeys.length) {
566
+ assert.fail(
567
+ `Expected ${keyKeys.length} new annotation keys, but got ${newAnnotationKeys.length}`,
568
+ )
569
+ }
570
+
571
+ keyKeys.forEach((keyKey, index) => {
572
+ context.keyMap?.set(keyKey, newAnnotationKeys[index])
573
+ })
574
+ },
575
+ ),
576
+ Then(
577
+ '{string} has an annotation different than {key}',
578
+ (context: Context, text: string, key: string) => {
579
+ const marks = getTextMarks(context.editor.getSnapshot().context, text)
580
+ const expectedMarks = [context.keyMap?.get(key) ?? key]
581
+
582
+ expect(marks).not.toEqual(expectedMarks)
583
+ },
584
+ ),
585
+
586
+ /**
587
+ * Decorator steps
588
+ */
589
+ Given(
590
+ '{decorator} around {string}',
591
+ async (
592
+ context: Context,
593
+ decorator: Parameter['decorator'],
594
+ text: string,
595
+ ) => {
596
+ await vi.waitFor(() => {
597
+ const selection = getTextSelection(
598
+ context.editor.getSnapshot().context,
599
+ text,
600
+ )
601
+ const anchorOffset = selection
602
+ ? selectionPointToBlockOffset({
603
+ context: {
604
+ schema: context.editor.getSnapshot().context.schema,
605
+ value: context.editor.getSnapshot().context.value,
606
+ },
607
+ selectionPoint: selection.anchor,
608
+ })
609
+ : undefined
610
+ const focusOffset = selection
611
+ ? selectionPointToBlockOffset({
612
+ context: {
613
+ schema: context.editor.getSnapshot().context.schema,
614
+ value: context.editor.getSnapshot().context.value,
615
+ },
616
+ selectionPoint: selection.focus,
617
+ })
618
+ : undefined
619
+ expect(anchorOffset).toBeDefined()
620
+ expect(focusOffset).toBeDefined()
621
+
622
+ context.editor.send({
623
+ type: 'decorator.toggle',
624
+ decorator,
625
+ at: {
626
+ anchor: anchorOffset!,
627
+ focus: focusOffset!,
628
+ },
629
+ })
630
+ })
631
+ },
632
+ ),
633
+ When(
634
+ '{decorator} is toggled',
635
+ async (context: Context, decorator: Parameter['decorator']) => {
636
+ await vi.waitFor(() => {
637
+ context.editor.send({
638
+ type: 'decorator.toggle',
639
+ decorator,
640
+ })
641
+ })
642
+ },
643
+ ),
644
+ When(
645
+ '{string} is marked with {decorator}',
646
+ async (
647
+ context: Context,
648
+ text: string,
649
+ decorator: Parameter['decorator'],
650
+ ) => {
651
+ await vi.waitFor(() => {
652
+ const selection = getTextSelection(
653
+ context.editor.getSnapshot().context,
654
+ text,
655
+ )
656
+ const anchorOffset = selection
657
+ ? spanSelectionPointToBlockOffset({
658
+ context: {
659
+ schema: context.editor.getSnapshot().context.schema,
660
+ value: context.editor.getSnapshot().context.value,
661
+ },
662
+ selectionPoint: selection.anchor,
663
+ })
664
+ : undefined
665
+ const focusOffset = selection
666
+ ? spanSelectionPointToBlockOffset({
667
+ context: {
668
+ schema: context.editor.getSnapshot().context.schema,
669
+ value: context.editor.getSnapshot().context.value,
670
+ },
671
+ selectionPoint: selection.focus,
672
+ })
673
+ : undefined
674
+
675
+ expect(anchorOffset).toBeDefined()
676
+ expect(focusOffset).toBeDefined()
677
+
678
+ context.editor.send({
679
+ type: 'decorator.toggle',
680
+ decorator,
681
+ at: {
682
+ anchor: anchorOffset!,
683
+ focus: focusOffset!,
684
+ },
685
+ })
686
+ })
687
+ },
688
+ ),
689
+
690
+ /**
691
+ * Mark steps
692
+ */
693
+ Then(
694
+ '{string} has marks {marks}',
695
+ async (context: Context, text: string, marks: Parameter['marks']) => {
696
+ await vi.waitFor(() => {
697
+ const actualMarks =
698
+ getTextMarks(context.editor.getSnapshot().context, text) ?? []
699
+ const expectedMarks = marks.map(
700
+ (mark) => context.keyMap?.get(mark) ?? mark,
701
+ )
702
+
703
+ expect(actualMarks).toEqual(expectedMarks)
704
+ })
705
+ },
706
+ ),
707
+ Then('{string} has no marks', async (context: Context, text: string) => {
708
+ await vi.waitFor(() => {
709
+ const textMarks = getTextMarks(context.editor.getSnapshot().context, text)
710
+ expect(textMarks).toEqual([])
711
+ })
712
+ }),
713
+ Then(
714
+ '{string} and {string} have the same marks',
715
+ (context: Context, textA: string, textB: string) => {
716
+ const marksA = getTextMarks(context.editor.getSnapshot().context, textA)
717
+ const marksB = getTextMarks(context.editor.getSnapshot().context, textB)
718
+
719
+ expect(
720
+ marksA,
721
+ `Expected "${textA}" and "${textB}" to have the same marks`,
722
+ ).toEqual(marksB)
723
+ },
724
+ ),
725
+
726
+ /**
727
+ * Style steps
728
+ */
729
+ When('{style} is toggled', (context: Context, style: Parameter['style']) => {
730
+ context.editor.send({type: 'style.toggle', style})
731
+ }),
732
+
733
+ /**
734
+ * Clipboard steps
735
+ */
736
+ When('x-portable-text is pasted', (context: Context, blocks: string) => {
737
+ const dataTransfer = new DataTransfer()
738
+ dataTransfer.setData('application/x-portable-text', blocks)
739
+ dataTransfer.setData('application/json', blocks)
740
+
741
+ context.editor.send({
742
+ type: 'clipboard.paste',
743
+ originEvent: {
744
+ dataTransfer,
745
+ },
746
+ position: {
747
+ selection: context.editor.getSnapshot().context.selection!,
748
+ },
749
+ })
750
+ }),
751
+ When(
752
+ 'data is pasted',
753
+ (context: Context, dataTable: Array<[mime: string, data: string]>) => {
754
+ const dataTransfer = new DataTransfer()
755
+
756
+ for (const data of dataTable) {
757
+ dataTransfer.setData(data[0], data[1])
758
+ }
759
+
760
+ context.editor.send({
761
+ type: 'clipboard.paste',
762
+ originEvent: {dataTransfer},
763
+ position: {
764
+ selection: context.editor.getSnapshot().context.selection!,
765
+ },
766
+ })
767
+ },
768
+ ),
769
+
770
+ /**
771
+ * Undo/Redo steps
772
+ */
773
+ When('undo is performed', (context: Context) => {
774
+ context.editor.send({type: 'history.undo'})
775
+ }),
776
+ When('redo is performed', (context: Context) => {
777
+ context.editor.send({type: 'history.redo'})
778
+ }),
779
+ ]