@portabletext/editor 2.21.3 → 3.0.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.
Files changed (95) hide show
  1. package/lib/_chunks-dts/index.d.ts +49 -209
  2. package/lib/_chunks-es/selector.is-at-the-start-of-block.js +103 -20
  3. package/lib/_chunks-es/selector.is-at-the-start-of-block.js.map +1 -1
  4. package/lib/_chunks-es/{util.get-text-block-text.js → util.slice-blocks.js} +73 -24
  5. package/lib/_chunks-es/util.slice-blocks.js.map +1 -0
  6. package/lib/_chunks-es/util.slice-text-block.js +13 -2
  7. package/lib/_chunks-es/util.slice-text-block.js.map +1 -1
  8. package/lib/behaviors/index.d.ts +1 -1
  9. package/lib/index.d.ts +2 -2
  10. package/lib/index.js +339 -341
  11. package/lib/index.js.map +1 -1
  12. package/lib/plugins/index.d.ts +2 -133
  13. package/lib/plugins/index.js +2 -796
  14. package/lib/plugins/index.js.map +1 -1
  15. package/lib/selectors/index.d.ts +2 -24
  16. package/lib/selectors/index.js +28 -130
  17. package/lib/selectors/index.js.map +1 -1
  18. package/lib/utils/index.d.ts +6 -4
  19. package/lib/utils/index.js +98 -9
  20. package/lib/utils/index.js.map +1 -1
  21. package/package.json +1 -3
  22. package/src/behaviors/behavior.abstract.split.ts +1 -0
  23. package/src/behaviors/behavior.perform-event.ts +7 -7
  24. package/src/converters/converter.portable-text.ts +1 -0
  25. package/src/converters/converter.text-html.ts +1 -0
  26. package/src/converters/converter.text-plain.ts +1 -0
  27. package/src/editor/Editable.tsx +1 -0
  28. package/src/editor/PortableTextEditor.tsx +0 -19
  29. package/src/editor/create-editor.ts +0 -3
  30. package/src/editor/editor-machine.ts +0 -10
  31. package/src/editor/event-to-change.tsx +5 -1
  32. package/src/editor/plugins/create-with-event-listeners.ts +30 -6
  33. package/src/editor/plugins/createWithObjectKeys.ts +2 -1
  34. package/src/editor/plugins/createWithPatches.ts +3 -3
  35. package/src/editor/plugins/createWithPlaceholderBlock.ts +2 -1
  36. package/src/editor/plugins/createWithPortableTextMarkModel.ts +2 -1
  37. package/src/editor/plugins/with-plugins.ts +10 -14
  38. package/src/editor/relay-machine.ts +0 -4
  39. package/src/editor/sync-machine.ts +2 -2
  40. package/src/editor.ts +0 -4
  41. package/src/history/behavior.operation.history.redo.ts +67 -0
  42. package/src/history/behavior.operation.history.undo.ts +71 -0
  43. package/src/history/event.history.undo.test.tsx +672 -0
  44. package/src/history/history.preserving-keys.test.tsx +112 -0
  45. package/src/history/remote-patches.ts +20 -0
  46. package/src/history/slate-plugin.history.ts +146 -0
  47. package/src/history/slate-plugin.redoing.ts +21 -0
  48. package/src/history/slate-plugin.undoing.ts +21 -0
  49. package/src/history/slate-plugin.without-history.ts +23 -0
  50. package/src/history/transform-operation.ts +245 -0
  51. package/src/history/undo-redo-collaboration.test.tsx +541 -0
  52. package/src/history/undo-redo.feature +125 -0
  53. package/src/history/undo-redo.test.tsx +195 -0
  54. package/src/history/undo-step.ts +148 -0
  55. package/src/index.ts +0 -1
  56. package/src/internal-utils/operation-to-patches.test.ts +23 -25
  57. package/src/internal-utils/operation-to-patches.ts +31 -22
  58. package/src/internal-utils/selection-text.test.ts +3 -0
  59. package/src/internal-utils/selection-text.ts +5 -2
  60. package/src/internal-utils/values.ts +23 -11
  61. package/src/operations/behavior.operation.block.set.ts +1 -0
  62. package/src/operations/behavior.operation.block.unset.ts +2 -0
  63. package/src/operations/behavior.operation.insert.block.ts +1 -0
  64. package/src/operations/behavior.operations.ts +2 -4
  65. package/src/plugins/index.ts +0 -3
  66. package/src/selectors/index.ts +0 -3
  67. package/src/test/vitest/step-definitions.tsx +57 -0
  68. package/src/test/vitest/test-editor.tsx +1 -1
  69. package/src/utils/parse-blocks.test.ts +296 -16
  70. package/src/utils/parse-blocks.ts +81 -22
  71. package/src/utils/util.merge-text-blocks.ts +5 -1
  72. package/src/utils/util.slice-blocks.ts +24 -10
  73. package/lib/_chunks-es/selector.get-selection-text.js +0 -92
  74. package/lib/_chunks-es/selector.get-selection-text.js.map +0 -1
  75. package/lib/_chunks-es/selector.get-text-before.js +0 -36
  76. package/lib/_chunks-es/selector.get-text-before.js.map +0 -1
  77. package/lib/_chunks-es/util.get-text-block-text.js.map +0 -1
  78. package/lib/_chunks-es/util.is-empty-text-block.js +0 -40
  79. package/lib/_chunks-es/util.is-empty-text-block.js.map +0 -1
  80. package/lib/_chunks-es/util.merge-text-blocks.js +0 -101
  81. package/lib/_chunks-es/util.merge-text-blocks.js.map +0 -1
  82. package/src/editor/plugins/createWithMaxBlocks.ts +0 -53
  83. package/src/editor/plugins/createWithUndoRedo.ts +0 -628
  84. package/src/editor/with-undo-step.ts +0 -37
  85. package/src/editor/withUndoRedo.ts +0 -34
  86. package/src/editor-event-listener.tsx +0 -28
  87. package/src/plugins/plugin.decorator-shortcut.ts +0 -238
  88. package/src/plugins/plugin.markdown.test.tsx +0 -42
  89. package/src/plugins/plugin.markdown.tsx +0 -131
  90. package/src/plugins/plugin.one-line.tsx +0 -123
  91. package/src/selectors/selector.get-list-state.test.ts +0 -189
  92. package/src/selectors/selector.get-list-state.ts +0 -96
  93. package/src/selectors/selector.get-selected-slice.ts +0 -13
  94. package/src/selectors/selector.get-trimmed-selection.test.ts +0 -657
  95. package/src/selectors/selector.get-trimmed-selection.ts +0 -189
@@ -0,0 +1,541 @@
1
+ import {getTersePt} from '@portabletext/test'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+ import {describe, expect, test, vi} from 'vitest'
4
+ import {userEvent} from 'vitest/browser'
5
+ import {
6
+ getSelectionAfterText,
7
+ getSelectionBeforeText,
8
+ } from '../internal-utils/text-selection'
9
+ import {createTestEditors} from '../test/vitest'
10
+
11
+ function createInitialValue(text: string): Array<PortableTextBlock> {
12
+ return [
13
+ {
14
+ _type: 'block',
15
+ _key: 'b0',
16
+ style: 'normal',
17
+ markDefs: [],
18
+ children: [
19
+ {
20
+ _type: 'span',
21
+ _key: 'b0s0',
22
+ marks: [],
23
+ text,
24
+ },
25
+ ],
26
+ },
27
+ ]
28
+ }
29
+
30
+ describe('Undo/Redo Collaboration (hand-coded)', () => {
31
+ test('will let editor A undo their change after B did an unrelated change (multi-line block)', async () => {
32
+ const {editor, locator, editorB, locatorB} = await createTestEditors({
33
+ initialValue: createInitialValue('First paragraph\n\nSecond paragraph!'),
34
+ })
35
+
36
+ await userEvent.click(locator)
37
+
38
+ const selection = getSelectionAfterText(editor.getSnapshot().context, '!')
39
+
40
+ editor.send({
41
+ type: 'select',
42
+ at: selection,
43
+ })
44
+
45
+ await vi.waitFor(() => {
46
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
47
+ })
48
+
49
+ await userEvent.type(locator, '?')
50
+
51
+ await vi.waitFor(() => {
52
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
53
+ 'First paragraph\n\nSecond paragraph!?',
54
+ ])
55
+ })
56
+
57
+ await userEvent.click(locatorB)
58
+
59
+ const selectionB = getSelectionBeforeText(
60
+ editorB.getSnapshot().context,
61
+ 'First',
62
+ )
63
+
64
+ editorB.send({
65
+ type: 'select',
66
+ at: selectionB,
67
+ })
68
+
69
+ await vi.waitFor(() => {
70
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
71
+ })
72
+
73
+ await userEvent.type(locatorB, 'Welcome')
74
+
75
+ await userEvent.keyboard('{Shift>}{Enter}{/Shift}')
76
+ await userEvent.keyboard('{Shift>}{Enter}{/Shift}')
77
+
78
+ editor.send({
79
+ type: 'history.undo',
80
+ })
81
+
82
+ await vi.waitFor(() => {
83
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
84
+ 'Welcome\n\nFirst paragraph\n\nSecond paragraph!',
85
+ ])
86
+ })
87
+ })
88
+
89
+ // Flaky
90
+ test.skip('undoing in reverse order as applied', async () => {
91
+ const firstParagraph =
92
+ '速ヒマヤレ誌相ルなあね日諸せ変評ホ真攻同潔ク作先た員勝どそ際接レゅ自17浅ッ実情スヤ籍認ス重力務鳥の。8平はートご多乗12青國暮整ル通国うれけこ能新ロコラハ元横ミ休探ミソ梓批ざょにね薬展むい本隣ば禁抗ワアミ部真えくト提知週むすほ。査ル人形ルおじつ政謙減セヲモ読見れレぞえ録精てざ定第ぐゆとス務接産ヤ写馬エモス聞氏サヘマ有午ごね客岡ヘロ修彩枝雨父のけリド。'
93
+ const secondParagraph =
94
+ '住ゅなぜ日16語約セヤチ任政崎ソオユ枠体ぞン古91一専泉給12関モリレネ解透ぴゃラぼ転地す球北ドざう記番重投ぼづ。期ゃ更緒リだすし夫内オ代他られくド潤刊本クヘフ伊一ウムニヘ感週け出入ば勇起ょ関図ぜ覧説めわぶ室訪おがト強車傾町コ本喰杜椿榎ほれた。暮る生的更芸窓どさはむ近問ラ入必ラニス療心コウ怒応りめけひ載総ア北吾ヌイヘ主最ニ余記エツヤ州5念稼め化浮ヌリ済毎養ぜぼ。'
95
+ const {editor, locator, editorB, locatorB} = await createTestEditors({
96
+ initialValue: createInitialValue(
97
+ `${firstParagraph}\n\n${secondParagraph}`,
98
+ ),
99
+ })
100
+
101
+ await userEvent.click(locator)
102
+
103
+ const selection = getSelectionBeforeText(editor.getSnapshot().context, '速')
104
+ editor.send({
105
+ type: 'select',
106
+ at: selection,
107
+ })
108
+ await vi.waitFor(() => {
109
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
110
+ })
111
+
112
+ await userEvent.type(locator, 'Paragraph 1: ')
113
+
114
+ await vi.waitFor(() => {
115
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
116
+ `Paragraph 1: ${firstParagraph}\n\n${secondParagraph}`,
117
+ ])
118
+ })
119
+
120
+ await userEvent.click(locatorB)
121
+
122
+ const selectionB = getSelectionAfterText(
123
+ editorB.getSnapshot().context,
124
+ 'ド。',
125
+ )
126
+ editorB.send({
127
+ type: 'select',
128
+ at: selectionB,
129
+ })
130
+ await vi.waitFor(() => {
131
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
132
+ })
133
+
134
+ await userEvent.type(locatorB, ' (end of paragraph 1)')
135
+
136
+ await vi.waitFor(() => {
137
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
138
+ `Paragraph 1: ${firstParagraph} (end of paragraph 1)\n\n${secondParagraph}`,
139
+ ])
140
+ })
141
+
142
+ await userEvent.click(locator)
143
+
144
+ const selectionC = getSelectionAfterText(
145
+ editor.getSnapshot().context,
146
+ 'ぼ。',
147
+ )
148
+ editor.send({
149
+ type: 'select',
150
+ at: selectionC,
151
+ })
152
+ await vi.waitFor(() => {
153
+ expect(editor.getSnapshot().context.selection).toEqual(selectionC)
154
+ })
155
+
156
+ await userEvent.type(locator, '. EOL.')
157
+
158
+ await vi.waitFor(() => {
159
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
160
+ `Paragraph 1: ${firstParagraph} (end of paragraph 1)\n\n${secondParagraph}. EOL.`,
161
+ ])
162
+ })
163
+
164
+ // Spaces in the text creates more undo steps
165
+ editor.send({
166
+ type: 'history.undo',
167
+ })
168
+ editor.send({
169
+ type: 'history.undo',
170
+ })
171
+
172
+ await vi.waitFor(() => {
173
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
174
+ `Paragraph 1: ${firstParagraph} (end of paragraph 1)\n\n${secondParagraph}`,
175
+ ])
176
+ })
177
+
178
+ editorB.send({
179
+ type: 'history.undo',
180
+ })
181
+ editorB.send({
182
+ type: 'history.undo',
183
+ })
184
+ editorB.send({
185
+ type: 'history.undo',
186
+ })
187
+ editorB.send({
188
+ type: 'history.undo',
189
+ })
190
+
191
+ await vi.waitFor(() => {
192
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
193
+ `Paragraph 1: ${firstParagraph}\n\n${secondParagraph}`,
194
+ ])
195
+ })
196
+
197
+ editor.send({
198
+ type: 'history.undo',
199
+ })
200
+ editor.send({
201
+ type: 'history.undo',
202
+ })
203
+ editor.send({
204
+ type: 'history.undo',
205
+ })
206
+
207
+ await vi.waitFor(() => {
208
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
209
+ `${firstParagraph}\n\n${secondParagraph}`,
210
+ ])
211
+ })
212
+ })
213
+
214
+ test('undoing out-of-order', async () => {
215
+ const firstParagraph = '速ド。'
216
+ const secondParagraph = '住ぼ。'
217
+ const {editor, editorB, locator, locatorB} = await createTestEditors({
218
+ initialValue: createInitialValue(
219
+ `${firstParagraph}\n\n${secondParagraph}`,
220
+ ),
221
+ })
222
+
223
+ await userEvent.click(locator)
224
+ const selection = getSelectionBeforeText(editor.getSnapshot().context, '速')
225
+ editor.send({
226
+ type: 'select',
227
+ at: selection,
228
+ })
229
+ await vi.waitFor(() => {
230
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
231
+ })
232
+
233
+ await userEvent.type(locator, 'P1>')
234
+
235
+ await vi.waitFor(() => {
236
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
237
+ `P1>${firstParagraph}\n\n${secondParagraph}`,
238
+ ])
239
+ })
240
+
241
+ await userEvent.click(locatorB)
242
+ const selectionB = getSelectionAfterText(
243
+ editorB.getSnapshot().context,
244
+ 'ド。',
245
+ )
246
+ editorB.send({
247
+ type: 'select',
248
+ at: selectionB,
249
+ })
250
+ await vi.waitFor(() => {
251
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
252
+ })
253
+
254
+ await userEvent.type(locatorB, '/P1')
255
+
256
+ await vi.waitFor(() => {
257
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
258
+ `P1>${firstParagraph}/P1\n\n${secondParagraph}`,
259
+ ])
260
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
261
+ `P1>${firstParagraph}/P1\n\n${secondParagraph}`,
262
+ ])
263
+ })
264
+
265
+ await userEvent.click(locator)
266
+
267
+ const selectionC = getSelectionAfterText(
268
+ editor.getSnapshot().context,
269
+ 'ぼ。',
270
+ )
271
+ editor.send({
272
+ type: 'select',
273
+ at: selectionC,
274
+ })
275
+ await vi.waitFor(() => {
276
+ expect(editor.getSnapshot().context.selection).toEqual(selectionC)
277
+ })
278
+
279
+ await userEvent.type(locator, '/P2')
280
+
281
+ await vi.waitFor(() => {
282
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
283
+ `P1>${firstParagraph}/P1\n\n${secondParagraph}/P2`,
284
+ ])
285
+ })
286
+
287
+ editor.send({
288
+ type: 'history.undo',
289
+ })
290
+ editor.send({
291
+ type: 'history.undo',
292
+ })
293
+
294
+ await vi.waitFor(() => {
295
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
296
+ `${firstParagraph}/P1\n\n${secondParagraph}`,
297
+ ])
298
+ })
299
+
300
+ editorB.send({
301
+ type: 'history.undo',
302
+ })
303
+
304
+ await vi.waitFor(() => {
305
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
306
+ `${firstParagraph}\n\n${secondParagraph}`,
307
+ ])
308
+ })
309
+ })
310
+
311
+ test('editor A undo their change after B did an unrelated change (single-line, emoji)', async () => {
312
+ const [beginning, middle, end] = [
313
+ 'A curious 🦊 named Felix lived in the 🪄🌲 of Willowwood. One day, he discovered a mysterious 🕳️, which lead to a magical 🌌. ',
314
+ 'In the Saturn of Celestia, Fox met a friendly Unicorn named Sparkle. ',
315
+ 'They had extraordinary adventures together, befriending a 🧚, who gave them so many 📚 that they never lacked for reading material!',
316
+ ]
317
+ const {editor, locator, editorB, locatorB} = await createTestEditors({
318
+ initialValue: createInitialValue(`${beginning}${end}`),
319
+ })
320
+
321
+ await userEvent.click(locator)
322
+
323
+ const selection = getSelectionAfterText(editor.getSnapshot().context, '!')
324
+ editor.send({
325
+ type: 'select',
326
+ at: selection,
327
+ })
328
+ await vi.waitFor(() => {
329
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
330
+ })
331
+
332
+ await userEvent.keyboard('{Backspace}')
333
+
334
+ await vi.waitFor(() => {
335
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
336
+ `${beginning}${end.slice(0, -1)}`,
337
+ ])
338
+ })
339
+
340
+ await userEvent.click(locatorB)
341
+
342
+ const selectionB = getSelectionAfterText(
343
+ editorB.getSnapshot().context,
344
+ '🌌. ',
345
+ )
346
+ editorB.send({
347
+ type: 'select',
348
+ at: selectionB,
349
+ })
350
+ await vi.waitFor(() => {
351
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
352
+ })
353
+
354
+ await userEvent.type(locatorB, middle)
355
+
356
+ await vi.waitFor(() => {
357
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
358
+ `${beginning}${middle}${end.slice(0, -1)}`,
359
+ ])
360
+ })
361
+
362
+ editor.send({
363
+ type: 'history.undo',
364
+ })
365
+
366
+ await vi.waitFor(() => {
367
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
368
+ `${beginning}${middle}${end}`,
369
+ ])
370
+ })
371
+ })
372
+
373
+ test('editor A undo their change after B did an unrelated change (multi-line block, emoji)', async () => {
374
+ const initialText = `In the 🪐 of Celestia, 🦊 met a friendly 🌈🦄 named Sparkle.\n\nThey had extraordinary adventures together, befriending a 🧚, who gave them so many 📚 that they never lacked for reading material!`
375
+ const {editor, locator, editorB, locatorB} = await createTestEditors({
376
+ initialValue: createInitialValue(initialText),
377
+ })
378
+
379
+ await userEvent.click(locator)
380
+ const selection = getSelectionAfterText(editor.getSnapshot().context, '!')
381
+ editor.send({
382
+ type: 'select',
383
+ at: selection,
384
+ })
385
+ await vi.waitFor(() => {
386
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
387
+ })
388
+ await userEvent.keyboard('{Backspace}')
389
+
390
+ await vi.waitFor(() => {
391
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
392
+ `${initialText.slice(0, -1)}`,
393
+ ])
394
+ })
395
+
396
+ await userEvent.click(locatorB)
397
+
398
+ const selectionB = getSelectionBeforeText(
399
+ editorB.getSnapshot().context,
400
+ 'In',
401
+ )
402
+ editorB.send({
403
+ type: 'select',
404
+ at: selectionB,
405
+ })
406
+ await vi.waitFor(() => {
407
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
408
+ })
409
+ const newPrefix = 'New prefix.'
410
+ await userEvent.type(locatorB, newPrefix)
411
+
412
+ await vi.waitFor(() => {
413
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
414
+ `${newPrefix}${initialText.slice(0, -1)}`,
415
+ ])
416
+ })
417
+
418
+ await userEvent.click(locatorB)
419
+
420
+ const selectionC = getSelectionAfterText(
421
+ editorB.getSnapshot().context,
422
+ newPrefix,
423
+ )
424
+ editorB.send({
425
+ type: 'select',
426
+ at: selectionC,
427
+ })
428
+ await vi.waitFor(() => {
429
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionC)
430
+ })
431
+
432
+ editorB.send({
433
+ type: 'insert.soft break',
434
+ })
435
+ editorB.send({
436
+ type: 'insert.soft break',
437
+ })
438
+
439
+ await vi.waitFor(() => {
440
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
441
+ `${newPrefix}\n\n${initialText.slice(0, -1)}`,
442
+ ])
443
+ })
444
+
445
+ editor.send({
446
+ type: 'history.undo',
447
+ })
448
+
449
+ await vi.waitFor(() => {
450
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
451
+ `${newPrefix}\n\n${initialText}`,
452
+ ])
453
+ })
454
+ })
455
+
456
+ test('editor A undo their change after B did an unrelated change (multi-line block, kanji)', async () => {
457
+ const initialText = `彼は、偉大な番兵がまさに尾根の頂上にいて、裸足では地面から最も低い枝にあることを知っていた。 彼は腹ばいになって雪と泥の中に滑り込み、下の何もない空き地を見下ろした。\n\n彼の心臓は胸の中で止まった。 しばらくの間、彼は息をすることさえできなかった。 月明かりは空き地、キャンプファイヤーの灰、雪に覆われた斜面、大きな岩、半分凍った小さな小川を照らしていました。すべては数1時間前とまったく同じでした。`
458
+ const {editor, locator, editorB, locatorB} = await createTestEditors({
459
+ initialValue: createInitialValue(initialText),
460
+ })
461
+
462
+ await userEvent.click(locator)
463
+ const selection = getSelectionAfterText(
464
+ editor.getSnapshot().context,
465
+ 'じでした。',
466
+ )
467
+ editor.send({
468
+ type: 'select',
469
+ at: selection,
470
+ })
471
+ await vi.waitFor(() => {
472
+ expect(editor.getSnapshot().context.selection).toEqual(selection)
473
+ })
474
+
475
+ await userEvent.keyboard('{Backspace}')
476
+
477
+ await vi.waitFor(() => {
478
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
479
+ `${initialText.slice(0, -1)}`,
480
+ ])
481
+ })
482
+
483
+ const newPrefix = 'new prefix'
484
+
485
+ await userEvent.click(locatorB)
486
+ const selectionB = getSelectionBeforeText(
487
+ editorB.getSnapshot().context,
488
+ '彼は、',
489
+ )
490
+ editorB.send({
491
+ type: 'select',
492
+ at: selectionB,
493
+ })
494
+ await vi.waitFor(() => {
495
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionB)
496
+ })
497
+
498
+ await userEvent.type(locatorB, newPrefix)
499
+
500
+ await vi.waitFor(() => {
501
+ expect(getTersePt(editorB.getSnapshot().context)).toEqual([
502
+ `${newPrefix}${initialText.slice(0, -1)}`,
503
+ ])
504
+ })
505
+
506
+ const selectionC = getSelectionAfterText(
507
+ editorB.getSnapshot().context,
508
+ newPrefix,
509
+ )
510
+ editorB.send({
511
+ type: 'select',
512
+ at: selectionC,
513
+ })
514
+ await vi.waitFor(() => {
515
+ expect(editorB.getSnapshot().context.selection).toEqual(selectionC)
516
+ })
517
+
518
+ editorB.send({
519
+ type: 'insert.soft break',
520
+ })
521
+ editorB.send({
522
+ type: 'insert.soft break',
523
+ })
524
+
525
+ await vi.waitFor(() => {
526
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
527
+ `${newPrefix}\n\n${initialText.slice(0, -1)}`,
528
+ ])
529
+ })
530
+
531
+ editor.send({
532
+ type: 'history.undo',
533
+ })
534
+
535
+ await vi.waitFor(() => {
536
+ expect(getTersePt(editor.getSnapshot().context)).toEqual([
537
+ `${newPrefix}\n\n${initialText}`,
538
+ ])
539
+ })
540
+ })
541
+ })
@@ -0,0 +1,125 @@
1
+ Feature: Undo/Redo
2
+
3
+ Background:
4
+ Given a global keymap
5
+
6
+ Scenario: Undoing writing two words
7
+ Given the text ""
8
+ When "foo" is typed
9
+ And " bar" is typed
10
+ And undo is performed
11
+ Then the text is "foo"
12
+
13
+ Scenario: Selection change does not affect the undo stack
14
+ Given the text ""
15
+ When "foo" is typed
16
+ And "{ArrowLeft}" is pressed
17
+ And "bar" is typed
18
+ Then the text is "fobaro"
19
+ When undo is performed
20
+ Then the text is "foo"
21
+
22
+ Scenario: Undoing annotation
23
+ Given the text "foo"
24
+ When "foo" is selected
25
+ And "link" is toggled
26
+ And undo is performed
27
+ Then the text is "foo"
28
+ And "foo" has no marks
29
+
30
+ Scenario: Undoing the deletion of the last char of annotated text
31
+ Given the text "foo"
32
+ And a "comment" "c1" around "foo"
33
+ When "{ArrowRight}" is pressed
34
+ And "{Backspace}" is pressed
35
+ And undo is performed
36
+ Then the text is "foo"
37
+ And "foo" has marks "c1"
38
+
39
+ Scenario: Redoing the deletion of the last char of annotated text
40
+ Given the text "foo"
41
+ And a "comment" "c1" around "foo"
42
+ When "{ArrowRight}" is pressed
43
+ And "{Backspace}" is pressed
44
+ And undo is performed
45
+ When redo is performed
46
+ Then the text is "fo"
47
+ And "fo" has marks "c1"
48
+
49
+ Scenario: Undoing inserting text after annotated text
50
+ Given the text "foo"
51
+ And a "comment" "c1" around "foo"
52
+ When "{ArrowRight}" is pressed
53
+ And "{Space}" is pressed
54
+ Then the text is "foo, "
55
+ And "foo" has marks "c1"
56
+ And " " has no marks
57
+ When undo is performed
58
+ Then the text is "foo"
59
+ And "foo" has marks "c1"
60
+
61
+ Scenario: Undoing and redoing inserting text after annotated text
62
+ Given the text "foo"
63
+ And a "comment" "c1" around "foo"
64
+ When "{ArrowRight}" is pressed
65
+ And "{Space}" is pressed
66
+ And undo is performed
67
+ Then the text is "foo"
68
+ And "foo" has marks "c1"
69
+ When redo is performed
70
+ Then the text is "foo, "
71
+ And "foo" has marks "c1"
72
+ And " " has no marks
73
+
74
+ Scenario: Undoing the deletion of block with annotation at the end
75
+ Given the text "foo bar"
76
+ And a "comment" "c1" around "bar"
77
+ When "foo bar" is selected
78
+ And "{Backspace}" is pressed
79
+ And undo is performed
80
+ Then the text is "foo ,bar"
81
+ And "bar" has marks "c1"
82
+
83
+ Scenario: Undoing deletion of annotated block
84
+ Given the text "foo"
85
+ And a "comment" "c1" around "foo"
86
+ When "{Backspace}" is pressed
87
+ And undo is performed
88
+ Then the text is "foo"
89
+ And "foo" has marks "c1"
90
+
91
+ Scenario: Undoing annotation across text blocks
92
+ Given the text "foo"
93
+ When "{Enter}" is pressed
94
+ And "bar" is typed
95
+ And "foobar" is selected
96
+ And "link" is toggled
97
+ And undo is performed
98
+ Then the text is "foo|bar"
99
+ And "foo" has no marks
100
+ And "bar" has no marks
101
+
102
+ Scenario: Undoing action step
103
+ Given the text "-"
104
+ When ">" is typed
105
+ Then the text is "→"
106
+ When undo is performed
107
+ Then the text is "->"
108
+
109
+ Scenario: Consecutive undo after selection change
110
+ Given the text "-"
111
+ When ">" is typed
112
+ And undo is performed
113
+ And "{ArrowLeft}" is pressed
114
+ And undo is performed
115
+ Then the text is "-"
116
+
117
+ Scenario: Undo after transform on expanded selection
118
+ Given the text "(cf"
119
+ When "f" is selected
120
+ And ")" is inserted
121
+ Then the text is "©"
122
+ When undo is performed
123
+ Then the text is "(c)"
124
+ When undo is performed
125
+ Then the text is "(cf"