@portabletext/editor 2.21.2 → 3.0.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 (79) hide show
  1. package/lib/_chunks-dts/index.d.ts +50 -210
  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} +29 -5
  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 +323 -320
  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 +3 -3
  19. package/lib/utils/index.js +97 -9
  20. package/lib/utils/index.js.map +1 -1
  21. package/package.json +7 -9
  22. package/src/behaviors/behavior.perform-event.ts +7 -7
  23. package/src/editor/PortableTextEditor.tsx +0 -19
  24. package/src/editor/create-editor.ts +0 -3
  25. package/src/editor/editor-machine.ts +0 -10
  26. package/src/editor/event-to-change.tsx +5 -1
  27. package/src/editor/plugins/create-with-event-listeners.ts +0 -4
  28. package/src/editor/plugins/createWithObjectKeys.ts +2 -1
  29. package/src/editor/plugins/createWithPatches.ts +3 -3
  30. package/src/editor/plugins/createWithPlaceholderBlock.ts +2 -1
  31. package/src/editor/plugins/createWithPortableTextMarkModel.ts +2 -1
  32. package/src/editor/plugins/with-plugins.ts +10 -14
  33. package/src/editor/relay-machine.ts +0 -4
  34. package/src/editor/sync-machine.ts +2 -2
  35. package/src/editor.ts +0 -4
  36. package/src/history/behavior.operation.history.redo.ts +67 -0
  37. package/src/history/behavior.operation.history.undo.ts +71 -0
  38. package/src/history/event.history.undo.test.tsx +672 -0
  39. package/src/history/history.preserving-keys.test.tsx +112 -0
  40. package/src/history/remote-patches.ts +20 -0
  41. package/src/history/slate-plugin.history.ts +146 -0
  42. package/src/history/slate-plugin.redoing.ts +21 -0
  43. package/src/history/slate-plugin.undoing.ts +21 -0
  44. package/src/history/slate-plugin.without-history.ts +23 -0
  45. package/src/history/transform-operation.ts +245 -0
  46. package/src/history/undo-redo-collaboration.test.tsx +541 -0
  47. package/src/history/undo-redo.feature +125 -0
  48. package/src/history/undo-redo.test.tsx +195 -0
  49. package/src/history/undo-step.ts +148 -0
  50. package/src/index.ts +0 -1
  51. package/src/internal-utils/applyPatch.ts +46 -1
  52. package/src/operations/behavior.operations.ts +2 -4
  53. package/src/plugins/index.ts +0 -3
  54. package/src/selectors/index.ts +0 -3
  55. package/src/test/vitest/step-definitions.tsx +88 -8
  56. package/src/test/vitest/test-editor.tsx +1 -1
  57. package/lib/_chunks-es/selector.get-selection-text.js +0 -92
  58. package/lib/_chunks-es/selector.get-selection-text.js.map +0 -1
  59. package/lib/_chunks-es/selector.get-text-before.js +0 -36
  60. package/lib/_chunks-es/selector.get-text-before.js.map +0 -1
  61. package/lib/_chunks-es/util.get-text-block-text.js.map +0 -1
  62. package/lib/_chunks-es/util.is-empty-text-block.js +0 -40
  63. package/lib/_chunks-es/util.is-empty-text-block.js.map +0 -1
  64. package/lib/_chunks-es/util.merge-text-blocks.js +0 -101
  65. package/lib/_chunks-es/util.merge-text-blocks.js.map +0 -1
  66. package/src/editor/plugins/createWithMaxBlocks.ts +0 -53
  67. package/src/editor/plugins/createWithUndoRedo.ts +0 -628
  68. package/src/editor/with-undo-step.ts +0 -37
  69. package/src/editor/withUndoRedo.ts +0 -34
  70. package/src/editor-event-listener.tsx +0 -28
  71. package/src/plugins/plugin.decorator-shortcut.ts +0 -238
  72. package/src/plugins/plugin.markdown.test.tsx +0 -42
  73. package/src/plugins/plugin.markdown.tsx +0 -131
  74. package/src/plugins/plugin.one-line.tsx +0 -123
  75. package/src/selectors/selector.get-list-state.test.ts +0 -189
  76. package/src/selectors/selector.get-list-state.ts +0 -96
  77. package/src/selectors/selector.get-selected-slice.ts +0 -13
  78. package/src/selectors/selector.get-trimmed-selection.test.ts +0 -657
  79. package/src/selectors/selector.get-trimmed-selection.ts +0 -189
@@ -0,0 +1,195 @@
1
+ import {Before} from 'racejar'
2
+ import {Feature} from 'racejar/vitest'
3
+ import {defineSchema} from '..'
4
+ import {defineBehavior, forward, raise} from '../behaviors'
5
+ import {BehaviorPlugin} from '../plugins/plugin.behavior'
6
+ import {getFocusTextBlock, isSelectionExpanded} from '../selectors'
7
+ import {parameterTypes} from '../test'
8
+ import {createTestEditor, stepDefinitions, type Context} from '../test/vitest'
9
+ import undoRedoFeature from './undo-redo.feature?raw'
10
+
11
+ Feature({
12
+ featureText: undoRedoFeature,
13
+ stepDefinitions,
14
+ parameterTypes,
15
+ hooks: [
16
+ Before(async (context: Context) => {
17
+ const {editor, locator} = await createTestEditor({
18
+ schemaDefinition: defineSchema({
19
+ annotations: [{name: 'link'}, {name: 'comment'}],
20
+ }),
21
+ children: (
22
+ <>
23
+ <ArrowTransformPlugin />
24
+ <CopyrightTransformPlugin />
25
+ </>
26
+ ),
27
+ })
28
+
29
+ context.locator = locator
30
+ context.editor = editor
31
+ }),
32
+ ],
33
+ })
34
+
35
+ /**
36
+ * Native behavior to transform `>` into `→`.
37
+ * Ony for testing purposes.
38
+ */
39
+ function ArrowTransformPlugin() {
40
+ return (
41
+ <BehaviorPlugin
42
+ behaviors={[
43
+ defineBehavior({
44
+ on: 'insert.text',
45
+ guard: ({snapshot, event}) => {
46
+ if (event.text !== '>' || isSelectionExpanded(snapshot)) {
47
+ return false
48
+ }
49
+
50
+ const focusTextBlock = getFocusTextBlock(snapshot)
51
+
52
+ if (!focusTextBlock) {
53
+ return false
54
+ }
55
+
56
+ return {focusTextBlock}
57
+ },
58
+ actions: [
59
+ ({event}) => [forward(event)],
60
+ ({snapshot}, {focusTextBlock}) => [
61
+ raise({
62
+ type: 'select',
63
+ at: {
64
+ anchor: {
65
+ path: focusTextBlock.path,
66
+ offset: 0,
67
+ },
68
+ focus: {
69
+ path: focusTextBlock.path,
70
+ offset: 2,
71
+ },
72
+ },
73
+ }),
74
+ raise({
75
+ type: 'delete',
76
+ at: {
77
+ anchor: {
78
+ path: focusTextBlock.path,
79
+ offset: 0,
80
+ },
81
+ focus: {
82
+ path: focusTextBlock.path,
83
+ offset: 2,
84
+ },
85
+ },
86
+ }),
87
+ raise({
88
+ type: 'insert.child',
89
+ child: {
90
+ _type: snapshot.context.schema.span.name,
91
+ text: '→',
92
+ marks: [],
93
+ },
94
+ }),
95
+ raise({
96
+ type: 'select',
97
+ at: {
98
+ anchor: {
99
+ path: focusTextBlock.path,
100
+ offset: 2,
101
+ },
102
+ focus: {
103
+ path: focusTextBlock.path,
104
+ offset: 2,
105
+ },
106
+ },
107
+ }),
108
+ ],
109
+ ],
110
+ }),
111
+ ]}
112
+ />
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Native behavior to transform `(c)` into `©`.
118
+ * Ony for testing purposes.
119
+ */
120
+ function CopyrightTransformPlugin() {
121
+ return (
122
+ <BehaviorPlugin
123
+ behaviors={[
124
+ defineBehavior({
125
+ on: 'insert.text',
126
+ guard: ({snapshot, event}) => {
127
+ if (event.text !== ')' || isSelectionExpanded(snapshot)) {
128
+ return false
129
+ }
130
+
131
+ const focusTextBlock = getFocusTextBlock(snapshot)
132
+
133
+ if (!focusTextBlock) {
134
+ return false
135
+ }
136
+
137
+ return {focusTextBlock}
138
+ },
139
+ actions: [
140
+ ({event}) => [forward(event)],
141
+ ({snapshot}, {focusTextBlock}) => [
142
+ raise({
143
+ type: 'select',
144
+ at: {
145
+ anchor: {
146
+ path: focusTextBlock.path,
147
+ offset: 0,
148
+ },
149
+ focus: {
150
+ path: focusTextBlock.path,
151
+ offset: 3,
152
+ },
153
+ },
154
+ }),
155
+ raise({
156
+ type: 'delete',
157
+ at: {
158
+ anchor: {
159
+ path: focusTextBlock.path,
160
+ offset: 0,
161
+ },
162
+ focus: {
163
+ path: focusTextBlock.path,
164
+ offset: 3,
165
+ },
166
+ },
167
+ }),
168
+ raise({
169
+ type: 'insert.child',
170
+ child: {
171
+ _type: snapshot.context.schema.span.name,
172
+ text: '©',
173
+ marks: [],
174
+ },
175
+ }),
176
+ raise({
177
+ type: 'select',
178
+ at: {
179
+ anchor: {
180
+ path: focusTextBlock.path,
181
+ offset: 3,
182
+ },
183
+ focus: {
184
+ path: focusTextBlock.path,
185
+ offset: 3,
186
+ },
187
+ },
188
+ }),
189
+ ],
190
+ ],
191
+ }),
192
+ ]}
193
+ />
194
+ )
195
+ }
@@ -0,0 +1,148 @@
1
+ import {Path, type Editor, type Operation} from 'slate'
2
+ import {isNormalizingNode} from '../editor/with-normalizing-node'
3
+ import {defaultKeyGenerator} from '../utils/key-generator'
4
+
5
+ const CURRENT_UNDO_STEP_ID: WeakMap<Editor, {undoStepId: string} | undefined> =
6
+ new WeakMap()
7
+
8
+ export function getCurrentUndoStepId(editor: Editor) {
9
+ return CURRENT_UNDO_STEP_ID.get(editor)?.undoStepId
10
+ }
11
+
12
+ export function createUndoStepId(editor: Editor) {
13
+ CURRENT_UNDO_STEP_ID.set(editor, {
14
+ undoStepId: defaultKeyGenerator(),
15
+ })
16
+ }
17
+
18
+ export function clearUndoStepId(editor: Editor) {
19
+ CURRENT_UNDO_STEP_ID.set(editor, undefined)
20
+ }
21
+
22
+ type UndoStep = {
23
+ operations: Array<Operation>
24
+ timestamp: Date
25
+ }
26
+
27
+ export function createUndoSteps({
28
+ steps,
29
+ op,
30
+ editor,
31
+ currentUndoStepId,
32
+ previousUndoStepId,
33
+ }: {
34
+ steps: Array<UndoStep>
35
+ op: Operation
36
+ editor: Editor
37
+ currentUndoStepId: string | undefined
38
+ previousUndoStepId: string | undefined
39
+ }): Array<UndoStep> {
40
+ const lastStep = steps.at(-1)
41
+
42
+ if (!lastStep) {
43
+ return createNewStep(steps, op, editor)
44
+ }
45
+
46
+ if (editor.operations.length > 0) {
47
+ // The editor has operations in progress
48
+
49
+ if (currentUndoStepId === previousUndoStepId || isNormalizingNode(editor)) {
50
+ return mergeIntoLastStep(steps, lastStep, op)
51
+ }
52
+
53
+ return createNewStep(steps, op, editor)
54
+ }
55
+
56
+ if (
57
+ op.type === 'set_selection' &&
58
+ currentUndoStepId === undefined &&
59
+ previousUndoStepId !== undefined
60
+ ) {
61
+ // Selecting without undo step ID
62
+ return mergeIntoLastStep(steps, lastStep, op)
63
+ }
64
+
65
+ if (
66
+ op.type === 'set_selection' &&
67
+ currentUndoStepId !== undefined &&
68
+ previousUndoStepId !== undefined &&
69
+ previousUndoStepId !== currentUndoStepId
70
+ ) {
71
+ // Selecting with different undo step ID
72
+ return mergeIntoLastStep(steps, lastStep, op)
73
+ }
74
+
75
+ // Handle case when both IDs are undefined
76
+ if (currentUndoStepId === undefined && previousUndoStepId === undefined) {
77
+ if (op.type === 'set_selection') {
78
+ return mergeIntoLastStep(steps, lastStep, op)
79
+ }
80
+
81
+ const lastOp = lastStep.operations.at(-1)
82
+
83
+ if (
84
+ lastOp &&
85
+ op.type === 'insert_text' &&
86
+ lastOp.type === 'insert_text' &&
87
+ op.offset === lastOp.offset + lastOp.text.length &&
88
+ Path.equals(op.path, lastOp.path) &&
89
+ op.text !== ' '
90
+ ) {
91
+ return mergeIntoLastStep(steps, lastStep, op)
92
+ }
93
+
94
+ if (
95
+ lastOp &&
96
+ op.type === 'remove_text' &&
97
+ lastOp.type === 'remove_text' &&
98
+ op.offset + op.text.length === lastOp.offset &&
99
+ Path.equals(op.path, lastOp.path)
100
+ ) {
101
+ return mergeIntoLastStep(steps, lastStep, op)
102
+ }
103
+
104
+ return createNewStep(steps, op, editor)
105
+ }
106
+
107
+ return createNewStep(steps, op, editor)
108
+ }
109
+
110
+ function createNewStep(
111
+ steps: Array<UndoStep>,
112
+ op: Operation,
113
+ editor: Editor,
114
+ ): Array<UndoStep> {
115
+ const operations =
116
+ editor.selection === null
117
+ ? [op]
118
+ : [
119
+ {
120
+ type: 'set_selection' as const,
121
+ properties: {...editor.selection},
122
+ newProperties: {...editor.selection},
123
+ },
124
+ op,
125
+ ]
126
+
127
+ return [
128
+ ...steps,
129
+ {
130
+ operations,
131
+ timestamp: new Date(),
132
+ },
133
+ ]
134
+ }
135
+
136
+ function mergeIntoLastStep(
137
+ steps: Array<UndoStep>,
138
+ lastStep: UndoStep,
139
+ op: Operation,
140
+ ): Array<UndoStep> {
141
+ return [
142
+ ...steps.slice(0, -1),
143
+ {
144
+ timestamp: lastStep.timestamp,
145
+ operations: [...lastStep.operations, op],
146
+ },
147
+ ]
148
+ }
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ export type {
7
7
  PortableTextSpan,
8
8
  } from '@sanity/types'
9
9
  export type {Editor, EditorConfig, EditorEvent} from './editor'
10
- export {EditorEventListener} from './editor-event-listener'
11
10
  export {PortableTextEditable} from './editor/Editable'
12
11
  export type {PortableTextEditableProps} from './editor/Editable'
13
12
  export type {PatchesEvent} from './editor/editor-machine'
@@ -16,7 +16,7 @@ import {
16
16
  parsePatch,
17
17
  } from '@sanity/diff-match-patch'
18
18
  import type {Path, PortableTextBlock, PortableTextChild} from '@sanity/types'
19
- import {Element, Node, Text, Transforms, type Descendant} from 'slate'
19
+ import {Editor, Element, Node, Text, Transforms, type Descendant} from 'slate'
20
20
  import type {EditorSchema} from '../editor/editor-schema'
21
21
  import {KEY_TO_SLATE_ELEMENT} from '../editor/weakMaps'
22
22
  import type {PortableTextSlateEditor} from '../types/editor'
@@ -202,6 +202,51 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
202
202
 
203
203
  const isTextBlock = editor.isTextBlock(block.node)
204
204
 
205
+ if (patch.path.length === 1) {
206
+ const updatedBlock = applyAll(block.node, [
207
+ {
208
+ ...patch,
209
+ path: patch.path.slice(1),
210
+ },
211
+ ])
212
+
213
+ if (editor.isTextBlock(block.node) && Element.isElement(updatedBlock)) {
214
+ Transforms.setNodes(editor, updatedBlock, {at: [block.index]})
215
+
216
+ const previousSelection = editor.selection
217
+
218
+ // Remove the previous children
219
+ for (const [_, childPath] of Editor.nodes(editor, {
220
+ at: [block.index],
221
+ reverse: true,
222
+ mode: 'lowest',
223
+ })) {
224
+ Transforms.removeNodes(editor, {at: childPath})
225
+ }
226
+
227
+ // Insert the new children
228
+ Transforms.insertNodes(editor, updatedBlock.children, {
229
+ at: [block.index, 0],
230
+ })
231
+
232
+ // Restore the selection
233
+ if (previousSelection) {
234
+ // Update the selection on the editor object
235
+ Transforms.setSelection(editor, previousSelection)
236
+ // Actively select the previous selection
237
+ Transforms.select(editor, previousSelection)
238
+ }
239
+
240
+ return true
241
+ } else {
242
+ Transforms.setNodes(editor, updatedBlock as Partial<Node>, {
243
+ at: [block.index],
244
+ })
245
+
246
+ return true
247
+ }
248
+ }
249
+
205
250
  if (isTextBlock && patch.path[1] !== 'children') {
206
251
  const updatedBlock = applyAll(block.node, [
207
252
  {
@@ -5,10 +5,8 @@ import type {
5
5
  } from '../behaviors/behavior.types.event'
6
6
  import type {EditorContext} from '../editor/editor-snapshot'
7
7
  import {removeDecoratorOperationImplementation} from '../editor/plugins/createWithPortableTextMarkModel'
8
- import {
9
- historyRedoOperationImplementation,
10
- historyUndoOperationImplementation,
11
- } from '../editor/plugins/createWithUndoRedo'
8
+ import {historyRedoOperationImplementation} from '../history/behavior.operation.history.redo'
9
+ import {historyUndoOperationImplementation} from '../history/behavior.operation.history.undo'
12
10
  import type {OmitFromUnion, PickFromUnion} from '../type-utils'
13
11
  import type {PortableTextSlateEditor} from '../types/editor'
14
12
  import {addAnnotationOperationImplementation} from './behavior.operation.annotation.add'
@@ -1,6 +1,3 @@
1
1
  export {BehaviorPlugin} from './plugin.behavior'
2
- export {DecoratorShortcutPlugin} from './plugin.decorator-shortcut'
3
2
  export {EditorRefPlugin} from './plugin.editor-ref'
4
3
  export {EventListenerPlugin} from './plugin.event-listener'
5
- export {MarkdownPlugin, type MarkdownPluginConfig} from './plugin.markdown'
6
- export {OneLinePlugin} from './plugin.one-line'
@@ -16,7 +16,6 @@ export {getFocusListBlock} from './selector.get-focus-list-block'
16
16
  export {getFocusSpan} from './selector.get-focus-span'
17
17
  export {getFocusTextBlock} from './selector.get-focus-text-block'
18
18
  export {getLastBlock} from './selector.get-last-block'
19
- export {getListIndex} from './selector.get-list-state'
20
19
  export {getMarkState, type MarkState} from './selector.get-mark-state'
21
20
  export {getNextBlock} from './selector.get-next-block'
22
21
  export {getNextInlineObject} from './selector.get-next-inline-object'
@@ -27,7 +26,6 @@ export {getPreviousInlineObject} from './selector.get-previous-inline-object'
27
26
  export {getPreviousInlineObjects} from './selector.get-previous-inline-objects'
28
27
  export {getPreviousSpan} from './selector.get-previous-span'
29
28
  export {getSelectedBlocks} from './selector.get-selected-blocks'
30
- export {getSelectedSlice} from './selector.get-selected-slice'
31
29
  export {getSelectedSpans} from './selector.get-selected-spans'
32
30
  export {getSelectedTextBlocks} from './selector.get-selected-text-blocks'
33
31
  export {getSelectedValue} from './selector.get-selected-value'
@@ -41,7 +39,6 @@ export {getSelectionStartPoint} from './selector.get-selection-start-point'
41
39
  export {getSelectionText} from './selector.get-selection-text'
42
40
  export {getBlockTextAfter} from './selector.get-text-after'
43
41
  export {getBlockTextBefore} from './selector.get-text-before'
44
- export {getTrimmedSelection} from './selector.get-trimmed-selection'
45
42
  export {getValue} from './selector.get-value'
46
43
  export {isActiveAnnotation} from './selector.is-active-annotation'
47
44
  export {isActiveDecorator} from './selector.is-active-decorator'
@@ -190,7 +190,7 @@ export const stepDefinitions = [
190
190
  await userEvent.type(context.locator, text)
191
191
  }),
192
192
  When(
193
- '{string} is typed by Editor B',
193
+ '{string} is typed in Editor B',
194
194
  async (context: Context, text: string) => {
195
195
  await userEvent.type(context.locatorB, text)
196
196
  },
@@ -222,23 +222,74 @@ export const stepDefinitions = [
222
222
  */
223
223
  When(
224
224
  '{button} is pressed',
225
- async (_: Context, button: Parameter['button']) => {
225
+ async (context: Context, button: Parameter['button']) => {
226
+ const previousSelection = context.editor.getSnapshot().context.selection
226
227
  await userEvent.keyboard(button)
227
- await new Promise((resolve) => setTimeout(resolve, 100))
228
+
229
+ await vi.waitFor(() => {
230
+ const currentSelection = context.editor.getSnapshot().context.selection
231
+
232
+ if (currentSelection) {
233
+ expect(currentSelection).not.toBe(previousSelection)
234
+ }
235
+ })
236
+ },
237
+ ),
238
+ When(
239
+ '{button} is pressed in Editor B',
240
+ async (context: Context, button: Parameter['button']) => {
241
+ const previousSelection = context.editorB.getSnapshot().context.selection
242
+ await userEvent.keyboard(button)
243
+
244
+ await vi.waitFor(() => {
245
+ const currentSelection = context.editorB.getSnapshot().context.selection
246
+
247
+ if (currentSelection) {
248
+ expect(currentSelection).not.toBe(previousSelection)
249
+ }
250
+ })
228
251
  },
229
252
  ),
230
253
  When(
231
254
  '{button} is pressed {int} times',
232
- async (_: Context, button: Parameter['button'], times: number) => {
255
+ async (context: Context, button: Parameter['button'], times: number) => {
233
256
  for (let i = 0; i < times; i++) {
257
+ const previousSelection = context.editor.getSnapshot().context.selection
234
258
  await userEvent.keyboard(button)
235
- await new Promise((resolve) => setTimeout(resolve, 100))
259
+
260
+ await vi.waitFor(() => {
261
+ const currentSelection =
262
+ context.editor.getSnapshot().context.selection
263
+
264
+ if (currentSelection) {
265
+ expect(currentSelection).not.toBe(previousSelection)
266
+ }
267
+ })
268
+ }
269
+ },
270
+ ),
271
+ When(
272
+ '{button} is pressed {int} times in Editor B',
273
+ async (context: Context, button: Parameter['button'], times: number) => {
274
+ for (let i = 0; i < times; i++) {
275
+ const previousSelection =
276
+ context.editorB.getSnapshot().context.selection
277
+ await userEvent.keyboard(button)
278
+
279
+ await vi.waitFor(() => {
280
+ const currentSelection =
281
+ context.editorB.getSnapshot().context.selection
282
+
283
+ if (currentSelection) {
284
+ expect(currentSelection).not.toBe(previousSelection)
285
+ }
286
+ })
236
287
  }
237
288
  },
238
289
  ),
239
290
  When(
240
291
  '{shortcut} is pressed',
241
- async (_: Context, shortcut: Parameter['shortcut']) => {
292
+ async (context: Context, shortcut: Parameter['shortcut']) => {
242
293
  const shortcuts: Record<Parameter['shortcut'], string> = {
243
294
  'deleteWord.backward': IS_MAC
244
295
  ? '{Alt>}{Backspace}{/Alt}'
@@ -248,8 +299,16 @@ export const stepDefinitions = [
248
299
  : '{Control>}{Delete}{/Control}',
249
300
  }
250
301
 
302
+ const previousSelection = context.editor.getSnapshot().context.selection
251
303
  await userEvent.keyboard(shortcuts[shortcut])
252
- await new Promise((resolve) => setTimeout(resolve, 100))
304
+
305
+ await vi.waitFor(() => {
306
+ const currentSelection = context.editor.getSnapshot().context.selection
307
+
308
+ if (currentSelection) {
309
+ expect(currentSelection).not.toBe(previousSelection)
310
+ }
311
+ })
253
312
  },
254
313
  ),
255
314
 
@@ -277,6 +336,27 @@ export const stepDefinitions = [
277
336
  })
278
337
  },
279
338
  ),
339
+ When(
340
+ 'the caret is put before {string} in Editor B',
341
+ async (context: Context, text: string) => {
342
+ await vi.waitFor(() => {
343
+ const selection = getSelectionBeforeText(
344
+ context.editorB.getSnapshot().context,
345
+ text,
346
+ )
347
+ expect(selection).not.toBeNull()
348
+
349
+ context.editorB.send({
350
+ type: 'select',
351
+ at: selection,
352
+ })
353
+
354
+ expect(context.editorB.getSnapshot().context.selection).toEqual(
355
+ selection,
356
+ )
357
+ })
358
+ },
359
+ ),
280
360
  Then(
281
361
  'the caret is before {string}',
282
362
  async (context: Context, text: string) => {
@@ -315,7 +395,7 @@ export const stepDefinitions = [
315
395
  },
316
396
  ),
317
397
  When(
318
- 'the caret is put after {string} by Editor B',
398
+ 'the caret is put after {string} in Editor B',
319
399
  async (context: Context, text: string) => {
320
400
  await vi.waitFor(() => {
321
401
  const selection = getSelectionAfterText(
@@ -134,7 +134,7 @@ export async function createTestEditors(
134
134
  ...patch,
135
135
  origin: 'remote',
136
136
  })),
137
- snapshot: event.snapshot,
137
+ snapshot: event.value,
138
138
  })
139
139
  editorBRef.current?.send({
140
140
  type: 'update value',