@wordpress/editor 14.50.0 → 14.51.1-next.v.202607070741.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 (197) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/build/components/collab-sidebar/format.cjs +83 -0
  3. package/build/components/collab-sidebar/format.cjs.map +7 -0
  4. package/build/components/collab-sidebar/hooks.cjs +117 -16
  5. package/build/components/collab-sidebar/hooks.cjs.map +2 -2
  6. package/build/components/collab-sidebar/index.cjs +18 -2
  7. package/build/components/collab-sidebar/index.cjs.map +2 -2
  8. package/build/components/collab-sidebar/note-highlight-styles.cjs +67 -0
  9. package/build/components/collab-sidebar/note-highlight-styles.cjs.map +7 -0
  10. package/build/components/collab-sidebar/utils.cjs +139 -0
  11. package/build/components/collab-sidebar/utils.cjs.map +2 -2
  12. package/build/components/collaborators-presence/use-collaborator-notifications.cjs +55 -30
  13. package/build/components/collaborators-presence/use-collaborator-notifications.cjs.map +2 -2
  14. package/build/components/commands/index.cjs +99 -3
  15. package/build/components/commands/index.cjs.map +2 -2
  16. package/build/components/document-bar/index.cjs +28 -34
  17. package/build/components/document-bar/index.cjs.map +2 -2
  18. package/build/components/entities-saved-states/index.cjs +1 -4
  19. package/build/components/entities-saved-states/index.cjs.map +2 -2
  20. package/build/components/media-categories/index.cjs +92 -6
  21. package/build/components/media-categories/index.cjs.map +2 -2
  22. package/build/components/page-attributes/parent.cjs +0 -1
  23. package/build/components/page-attributes/parent.cjs.map +2 -2
  24. package/build/components/post-author/combobox.cjs +0 -1
  25. package/build/components/post-author/combobox.cjs.map +2 -2
  26. package/build/components/post-author/select.cjs +0 -1
  27. package/build/components/post-author/select.cjs.map +2 -2
  28. package/build/components/post-preview-button/index.cjs +29 -2
  29. package/build/components/post-preview-button/index.cjs.map +2 -2
  30. package/build/components/post-publish-panel/maybe-upload-media.cjs +15 -26
  31. package/build/components/post-publish-panel/maybe-upload-media.cjs.map +3 -3
  32. package/build/components/post-revisions-preview/revisions-slider.cjs +3 -1
  33. package/build/components/post-revisions-preview/revisions-slider.cjs.map +2 -2
  34. package/build/components/post-taxonomies/flat-term-selector.cjs +0 -1
  35. package/build/components/post-taxonomies/flat-term-selector.cjs.map +2 -2
  36. package/build/components/post-taxonomies/hierarchical-term-selector.cjs +0 -2
  37. package/build/components/post-taxonomies/hierarchical-term-selector.cjs.map +2 -2
  38. package/build/components/post-template/classic-theme.cjs +0 -1
  39. package/build/components/post-template/classic-theme.cjs.map +2 -2
  40. package/build/components/preferences-modal/index.cjs +54 -28
  41. package/build/components/preferences-modal/index.cjs.map +2 -2
  42. package/build/components/preview-dropdown/index.cjs +3 -3
  43. package/build/components/preview-dropdown/index.cjs.map +2 -2
  44. package/build/components/provider/use-block-editor-settings.cjs +23 -3
  45. package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
  46. package/build/components/sidebar/dataform-post-summary.cjs +5 -1
  47. package/build/components/sidebar/dataform-post-summary.cjs.map +3 -3
  48. package/build/components/sidebar/index.cjs +29 -38
  49. package/build/components/sidebar/index.cjs.map +3 -3
  50. package/build/components/sidebar/post-summary.cjs +0 -13
  51. package/build/components/sidebar/post-summary.cjs.map +3 -3
  52. package/build/dataviews/store/private-actions.cjs +24 -19
  53. package/build/dataviews/store/private-actions.cjs.map +2 -2
  54. package/build/store/private-actions.cjs +5 -2
  55. package/build/store/private-actions.cjs.map +2 -2
  56. package/build/utils/media-sideload-from-url/index.cjs +63 -0
  57. package/build/utils/media-sideload-from-url/index.cjs.map +7 -0
  58. package/build-module/components/collab-sidebar/format.mjs +57 -0
  59. package/build-module/components/collab-sidebar/format.mjs.map +7 -0
  60. package/build-module/components/collab-sidebar/hooks.mjs +121 -16
  61. package/build-module/components/collab-sidebar/hooks.mjs.map +2 -2
  62. package/build-module/components/collab-sidebar/index.mjs +21 -5
  63. package/build-module/components/collab-sidebar/index.mjs.map +2 -2
  64. package/build-module/components/collab-sidebar/note-highlight-styles.mjs +41 -0
  65. package/build-module/components/collab-sidebar/note-highlight-styles.mjs.map +7 -0
  66. package/build-module/components/collab-sidebar/utils.mjs +133 -0
  67. package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
  68. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs +55 -30
  69. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs.map +2 -2
  70. package/build-module/components/commands/index.mjs +99 -3
  71. package/build-module/components/commands/index.mjs.map +2 -2
  72. package/build-module/components/document-bar/index.mjs +28 -34
  73. package/build-module/components/document-bar/index.mjs.map +2 -2
  74. package/build-module/components/entities-saved-states/index.mjs +1 -4
  75. package/build-module/components/entities-saved-states/index.mjs.map +2 -2
  76. package/build-module/components/media-categories/index.mjs +93 -7
  77. package/build-module/components/media-categories/index.mjs.map +2 -2
  78. package/build-module/components/page-attributes/parent.mjs +0 -1
  79. package/build-module/components/page-attributes/parent.mjs.map +2 -2
  80. package/build-module/components/post-author/combobox.mjs +0 -1
  81. package/build-module/components/post-author/combobox.mjs.map +2 -2
  82. package/build-module/components/post-author/select.mjs +0 -1
  83. package/build-module/components/post-author/select.mjs.map +2 -2
  84. package/build-module/components/post-preview-button/index.mjs +29 -2
  85. package/build-module/components/post-preview-button/index.mjs.map +2 -2
  86. package/build-module/components/post-publish-panel/maybe-upload-media.mjs +19 -27
  87. package/build-module/components/post-publish-panel/maybe-upload-media.mjs.map +2 -2
  88. package/build-module/components/post-revisions-preview/revisions-slider.mjs +3 -1
  89. package/build-module/components/post-revisions-preview/revisions-slider.mjs.map +2 -2
  90. package/build-module/components/post-taxonomies/flat-term-selector.mjs +0 -1
  91. package/build-module/components/post-taxonomies/flat-term-selector.mjs.map +2 -2
  92. package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs +0 -2
  93. package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs.map +2 -2
  94. package/build-module/components/post-template/classic-theme.mjs +0 -1
  95. package/build-module/components/post-template/classic-theme.mjs.map +2 -2
  96. package/build-module/components/preferences-modal/index.mjs +54 -28
  97. package/build-module/components/preferences-modal/index.mjs.map +2 -2
  98. package/build-module/components/preview-dropdown/index.mjs +3 -3
  99. package/build-module/components/preview-dropdown/index.mjs.map +2 -2
  100. package/build-module/components/provider/use-block-editor-settings.mjs +23 -3
  101. package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
  102. package/build-module/components/sidebar/dataform-post-summary.mjs +6 -2
  103. package/build-module/components/sidebar/dataform-post-summary.mjs.map +2 -2
  104. package/build-module/components/sidebar/index.mjs +29 -42
  105. package/build-module/components/sidebar/index.mjs.map +2 -2
  106. package/build-module/components/sidebar/post-summary.mjs +0 -13
  107. package/build-module/components/sidebar/post-summary.mjs.map +2 -2
  108. package/build-module/dataviews/store/private-actions.mjs +26 -19
  109. package/build-module/dataviews/store/private-actions.mjs.map +2 -2
  110. package/build-module/store/private-actions.mjs +5 -2
  111. package/build-module/store/private-actions.mjs.map +2 -2
  112. package/build-module/utils/media-sideload-from-url/index.mjs +32 -0
  113. package/build-module/utils/media-sideload-from-url/index.mjs.map +7 -0
  114. package/build-style/style-rtl.css +65 -33
  115. package/build-style/style.css +65 -33
  116. package/build-types/components/collab-sidebar/format.d.ts +17 -0
  117. package/build-types/components/collab-sidebar/format.d.ts.map +1 -0
  118. package/build-types/components/collab-sidebar/hooks.d.ts.map +1 -1
  119. package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
  120. package/build-types/components/collab-sidebar/note-highlight-styles.d.ts +31 -0
  121. package/build-types/components/collab-sidebar/note-highlight-styles.d.ts.map +1 -0
  122. package/build-types/components/collab-sidebar/utils.d.ts +82 -2
  123. package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
  124. package/build-types/components/collaborators-presence/use-collaborator-notifications.d.ts.map +1 -1
  125. package/build-types/components/commands/index.d.ts.map +1 -1
  126. package/build-types/components/document-bar/index.d.ts.map +1 -1
  127. package/build-types/components/entities-saved-states/index.d.ts.map +1 -1
  128. package/build-types/components/media-categories/index.d.ts +12 -3
  129. package/build-types/components/media-categories/index.d.ts.map +1 -1
  130. package/build-types/components/page-attributes/parent.d.ts.map +1 -1
  131. package/build-types/components/post-author/combobox.d.ts.map +1 -1
  132. package/build-types/components/post-author/select.d.ts.map +1 -1
  133. package/build-types/components/post-preview-button/index.d.ts.map +1 -1
  134. package/build-types/components/post-publish-panel/maybe-upload-media.d.ts.map +1 -1
  135. package/build-types/components/post-revisions-preview/revisions-slider.d.ts.map +1 -1
  136. package/build-types/components/post-taxonomies/flat-term-selector.d.ts.map +1 -1
  137. package/build-types/components/post-taxonomies/hierarchical-term-selector.d.ts.map +1 -1
  138. package/build-types/components/post-template/classic-theme.d.ts.map +1 -1
  139. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  140. package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -1
  141. package/build-types/components/sidebar/index.d.ts.map +1 -1
  142. package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
  143. package/build-types/dataviews/store/private-actions.d.ts.map +1 -1
  144. package/build-types/store/private-actions.d.ts.map +1 -1
  145. package/build-types/store/selectors.d.ts.map +1 -1
  146. package/build-types/utils/get-template-part-icon.d.ts.map +1 -1
  147. package/build-types/utils/media-sideload-from-url/index.d.ts +18 -0
  148. package/build-types/utils/media-sideload-from-url/index.d.ts.map +1 -0
  149. package/package.json +48 -46
  150. package/src/components/collab-sidebar/format.js +72 -0
  151. package/src/components/collab-sidebar/hooks.js +203 -22
  152. package/src/components/collab-sidebar/index.js +19 -5
  153. package/src/components/collab-sidebar/note-highlight-styles.js +83 -0
  154. package/src/components/collab-sidebar/style.scss +9 -0
  155. package/src/components/collab-sidebar/test/note-highlight-styles.js +106 -0
  156. package/src/components/collab-sidebar/test/utils.js +471 -0
  157. package/src/components/collab-sidebar/utils.js +225 -2
  158. package/src/components/collaborators-presence/test/use-collaborator-notifications.ts +265 -262
  159. package/src/components/collaborators-presence/use-collaborator-notifications.ts +70 -34
  160. package/src/components/commands/index.js +142 -0
  161. package/src/components/document-bar/index.js +0 -9
  162. package/src/components/document-bar/style.scss +0 -17
  163. package/src/components/entities-saved-states/index.js +1 -5
  164. package/src/components/header/style.scss +7 -3
  165. package/src/components/media-categories/index.js +160 -6
  166. package/src/components/media-categories/test/index.js +202 -0
  167. package/src/components/page-attributes/parent.js +0 -1
  168. package/src/components/post-author/combobox.js +0 -1
  169. package/src/components/post-author/select.js +0 -1
  170. package/src/components/post-preview-button/index.js +68 -2
  171. package/src/components/post-publish-panel/maybe-upload-media.js +25 -30
  172. package/src/components/post-publish-panel/style.scss +1 -1
  173. package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +11 -127
  174. package/src/components/post-revisions-preview/revisions-slider.js +4 -1
  175. package/src/components/post-revisions-preview/test/block-diff.js +2 -2
  176. package/src/components/post-taxonomies/flat-term-selector.js +0 -1
  177. package/src/components/post-taxonomies/hierarchical-term-selector.js +0 -2
  178. package/src/components/post-template/classic-theme.js +0 -1
  179. package/src/components/preferences-modal/index.js +40 -22
  180. package/src/components/preferences-modal/test/index.js +83 -0
  181. package/src/components/preview-dropdown/index.js +3 -3
  182. package/src/components/provider/use-block-editor-settings.js +42 -1
  183. package/src/components/sidebar/dataform-post-summary.js +13 -1
  184. package/src/components/sidebar/index.js +32 -47
  185. package/src/components/sidebar/post-summary.js +0 -17
  186. package/src/dataviews/store/private-actions.ts +30 -26
  187. package/src/store/private-actions.js +6 -2
  188. package/src/utils/media-sideload-from-url/index.js +54 -0
  189. package/src/utils/media-sideload-from-url/test/index.js +162 -0
  190. package/build/components/post-publish-panel/media-util.cjs +0 -72
  191. package/build/components/post-publish-panel/media-util.cjs.map +0 -7
  192. package/build-module/components/post-publish-panel/media-util.mjs +0 -46
  193. package/build-module/components/post-publish-panel/media-util.mjs.map +0 -7
  194. package/build-types/components/post-publish-panel/media-util.d.ts +0 -20
  195. package/build-types/components/post-publish-panel/media-util.d.ts.map +0 -1
  196. package/src/components/post-publish-panel/media-util.js +0 -87
  197. package/src/components/post-publish-panel/test/media-util.js +0 -118
@@ -14,145 +14,163 @@ import { useDispatch, useSelect } from '@wordpress/data';
14
14
  import { useCollaboratorNotifications } from '../use-collaborator-notifications';
15
15
 
16
16
  // --- Mocks ---
17
-
18
- const mockCreateNotice = jest.fn();
19
- let mockOnJoinCallback: Function | null = null;
20
- let mockOnLeaveCallback: Function | null = null;
21
- let mockOnPostSaveCallback: Function | null = null;
22
- let lastJoinPostId: unknown;
23
- let lastLeavePostId: unknown;
24
- let lastSavePostId: unknown;
25
- let mockEditorState = {
26
- postStatus: 'draft',
27
- isCollaborationEnabled: true,
28
- showCollaborationNotifications: true,
29
- };
17
+ //
18
+ // These mocks isolate the hook from `@wordpress/data`, the editor store, and
19
+ // the core-data private APIs it unlocks, so tests can drive the join/leave/
20
+ // save callbacks directly instead of simulating real awareness events.
30
21
 
31
22
  jest.mock( '@wordpress/data', () => ( {
32
23
  useSelect: jest.fn(),
33
24
  useDispatch: jest.fn(),
34
25
  } ) );
35
26
 
36
- jest.mock( '@wordpress/notices', () => ( {
37
- store: 'core/notices',
38
- } ) );
39
-
40
- jest.mock( '@wordpress/preferences', () => ( {
41
- store: 'core/preferences',
42
- } ) );
43
-
44
- // Mock the editor store to prevent deep import chain (blocks, rich-text, etc.)
45
- jest.mock( '../../../store', () => ( {
46
- store: 'core/editor',
47
- } ) );
48
-
49
- // Mock the private APIs and unlock mechanism
50
- jest.mock( '@wordpress/core-data', () => ( {
51
- privateApis: {},
52
- } ) );
27
+ jest.mock( '@wordpress/notices', () => ( { store: 'core/notices' } ) );
28
+
29
+ jest.mock( '@wordpress/preferences', () => ( { store: 'core/preferences' } ) );
30
+
31
+ // Avoids pulling in the full editor store (blocks, rich-text, etc.).
32
+ jest.mock( '../../../store', () => ( { store: 'core/editor' } ) );
33
+
34
+ jest.mock( '@wordpress/core-data', () => ( { privateApis: {} } ) );
35
+
36
+ // Captures the callbacks and postIds the hook registers with each of the
37
+ // three core-data subscriptions it unlocks. Must be prefixed with `mock`
38
+ // so jest's module factory hoisting allows referencing it below.
39
+ const mockRegistered: {
40
+ join: Function | null;
41
+ leave: Function | null;
42
+ save: Function | null;
43
+ joinPostId: unknown;
44
+ leavePostId: unknown;
45
+ savePostId: unknown;
46
+ } = {
47
+ join: null,
48
+ leave: null,
49
+ save: null,
50
+ joinPostId: undefined,
51
+ leavePostId: undefined,
52
+ savePostId: undefined,
53
+ };
53
54
 
54
55
  jest.mock( '../../../lock-unlock', () => ( {
55
56
  unlock: jest.fn( ( value: unknown ) => ( {
56
57
  ...( value as object ),
57
- useOnCollaboratorJoin: jest.fn(
58
- ( postId: unknown, _postType: unknown, callback: Function ) => {
59
- lastJoinPostId = postId;
60
- mockOnJoinCallback = callback;
61
- }
62
- ),
63
- useOnCollaboratorLeave: jest.fn(
64
- ( postId: unknown, _postType: unknown, callback: Function ) => {
65
- lastLeavePostId = postId;
66
- mockOnLeaveCallback = callback;
67
- }
68
- ),
69
- useOnPostSave: jest.fn(
70
- ( postId: unknown, _postType: unknown, callback: Function ) => {
71
- lastSavePostId = postId;
72
- mockOnPostSaveCallback = callback;
73
- }
74
- ),
58
+ useOnCollaboratorJoin: (
59
+ postId: unknown,
60
+ _t: unknown,
61
+ cb: Function
62
+ ) => {
63
+ mockRegistered.joinPostId = postId;
64
+ mockRegistered.join = cb;
65
+ },
66
+ useOnCollaboratorLeave: (
67
+ postId: unknown,
68
+ _t: unknown,
69
+ cb: Function
70
+ ) => {
71
+ mockRegistered.leavePostId = postId;
72
+ mockRegistered.leave = cb;
73
+ },
74
+ useOnPostSave: ( postId: unknown, _t: unknown, cb: Function ) => {
75
+ mockRegistered.savePostId = postId;
76
+ mockRegistered.save = cb;
77
+ },
75
78
  } ) ),
76
79
  } ) );
77
80
 
78
- // --- Helpers ---
81
+ // --- Fixtures ---
79
82
 
80
83
  const BASE_ENTERED_AT = 1704067200000;
81
84
 
82
- function makeCollaborator( overrides: Record< string, unknown > = {} ) {
85
+ function makeCollaborator(
86
+ id: number,
87
+ name: string,
88
+ enteredAt: number,
89
+ overrides: Record< string, unknown > = {}
90
+ ) {
83
91
  return {
84
- clientId: 1,
92
+ clientId: id,
85
93
  isMe: false,
86
94
  isConnected: true,
87
95
  collaboratorInfo: {
88
- id: 100,
89
- name: 'Alice',
90
- slug: 'alice',
96
+ id,
97
+ name,
98
+ slug: name.toLowerCase(),
91
99
  avatar_urls: {},
92
100
  browserType: 'Chrome',
93
- enteredAt: BASE_ENTERED_AT + 1000,
101
+ enteredAt,
94
102
  },
95
103
  ...overrides,
96
104
  };
97
105
  }
98
106
 
99
- function makeMe( overrides: Record< string, unknown > = {} ) {
100
- return makeCollaborator( {
101
- clientId: 999,
102
- isMe: true,
103
- collaboratorInfo: {
104
- id: 1,
105
- name: 'Me',
106
- slug: 'me',
107
- avatar_urls: {},
108
- browserType: 'Chrome',
109
- enteredAt: BASE_ENTERED_AT + 5000, // joined later than Alice
110
- },
111
- ...overrides,
112
- } );
113
- }
107
+ // "me" is the current user; Alice entered before "me", Bob after.
108
+ const me = makeCollaborator( 1, 'Me', BASE_ENTERED_AT + 5000, {
109
+ isMe: true,
110
+ } );
111
+ const alice = makeCollaborator( 100, 'Alice', BASE_ENTERED_AT + 1000 );
112
+ const bob = makeCollaborator( 200, 'Bob', BASE_ENTERED_AT + 10000 );
114
113
 
115
114
  // --- Setup ---
116
115
 
117
- function buildMockSelect() {
118
- return ( storeKey: string ) => {
119
- if ( storeKey === 'core/preferences' ) {
120
- return {
121
- get: ( scope: string, name: string ) => {
122
- if (
123
- scope === 'core' &&
124
- name === 'showCollaborationNotifications'
125
- ) {
126
- return mockEditorState.showCollaborationNotifications;
127
- }
128
- return undefined;
129
- },
130
- };
131
- }
116
+ const mockCreateNotice = jest.fn();
117
+
118
+ type PreferenceState = {
119
+ postStatus: string | undefined;
120
+ isCollaborationEnabled: boolean;
121
+ showCollaborationJoinNotifications: boolean;
122
+ showCollaborationLeaveNotifications: boolean;
123
+ showCollaborationPostSaveNotifications: boolean;
124
+ };
125
+
126
+ const DEFAULT_STATE: PreferenceState = {
127
+ postStatus: 'draft',
128
+ isCollaborationEnabled: true,
129
+ showCollaborationJoinNotifications: true,
130
+ showCollaborationLeaveNotifications: true,
131
+ showCollaborationPostSaveNotifications: true,
132
+ };
133
+
134
+ let state: PreferenceState = { ...DEFAULT_STATE };
135
+
136
+ function mockSelect( storeKey: string ) {
137
+ if ( storeKey === 'core/preferences' ) {
132
138
  return {
133
- getCurrentPostAttribute: ( attr: string ) =>
134
- attr === 'status' ? mockEditorState.postStatus : undefined,
135
- isCollaborationEnabledForCurrentPost: () =>
136
- mockEditorState.isCollaborationEnabled,
139
+ get: ( scope: string, name: string ) =>
140
+ scope === 'core' && name in state
141
+ ? ( state as unknown as Record< string, boolean > )[ name ]
142
+ : undefined,
137
143
  };
144
+ }
145
+ return {
146
+ getCurrentPostAttribute: ( attr: string ) =>
147
+ attr === 'status' ? state.postStatus : undefined,
148
+ isCollaborationEnabledForCurrentPost: () =>
149
+ state.isCollaborationEnabled,
138
150
  };
139
151
  }
140
152
 
153
+ /**
154
+ * Renders the hook under test with the given preference/state overrides
155
+ * applied on top of the defaults (everything enabled, status: 'draft').
156
+ *
157
+ * @param overrides Partial preference/state overrides.
158
+ */
159
+ function renderNotifications( overrides: Partial< PreferenceState > = {} ) {
160
+ state = { ...DEFAULT_STATE, ...overrides };
161
+ return renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
162
+ }
163
+
141
164
  beforeEach( () => {
142
- mockOnJoinCallback = null;
143
- mockOnLeaveCallback = null;
144
- mockOnPostSaveCallback = null;
145
- lastJoinPostId = undefined;
146
- lastLeavePostId = undefined;
147
- lastSavePostId = undefined;
148
- mockEditorState = {
149
- postStatus: 'draft',
150
- isCollaborationEnabled: true,
151
- showCollaborationNotifications: true,
152
- };
165
+ mockRegistered.join = null;
166
+ mockRegistered.leave = null;
167
+ mockRegistered.save = null;
168
+ mockRegistered.joinPostId = undefined;
169
+ mockRegistered.leavePostId = undefined;
170
+ mockRegistered.savePostId = undefined;
153
171
  mockCreateNotice.mockClear();
154
172
  ( useSelect as jest.Mock ).mockImplementation( ( selector: Function ) =>
155
- selector( buildMockSelect() )
173
+ selector( mockSelect )
156
174
  );
157
175
  ( useDispatch as jest.Mock ).mockReturnValue( {
158
176
  createNotice: mockCreateNotice,
@@ -163,235 +181,220 @@ beforeEach( () => {
163
181
 
164
182
  describe( 'useCollaboratorNotifications', () => {
165
183
  describe( 'collaborator join notifications', () => {
166
- it( 'fires join notification for a collaborator who joined after current user', () => {
167
- const me = makeMe(); // enteredAt: BASE_ENTERED_AT + 5000
168
- const bobJoinedAfter = makeCollaborator( {
169
- clientId: 2,
170
- collaboratorInfo: {
171
- id: 200,
172
- name: 'Bob',
173
- slug: 'bob',
174
- avatar_urls: {},
175
- browserType: 'Firefox',
176
- enteredAt: BASE_ENTERED_AT + 10000, // joined after me
177
- },
178
- } );
179
-
180
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
184
+ it( 'fires when the collaborator entered after the current user', () => {
185
+ renderNotifications();
181
186
 
182
- // Simulate the core-data hook firing the join callback
183
- mockOnJoinCallback?.( bobJoinedAfter, me );
187
+ mockRegistered.join?.( bob, me );
184
188
 
185
189
  expect( mockCreateNotice ).toHaveBeenCalledWith(
186
190
  'info',
187
191
  'Bob has joined the post.',
188
192
  expect.objectContaining( {
189
193
  id: 'collab-user-entered-200',
194
+ type: 'snackbar',
195
+ isDismissible: false,
190
196
  } )
191
197
  );
192
198
  } );
193
199
 
194
- it( 'skips join notification for collaborators who joined before current user', () => {
195
- const me = makeMe(); // enteredAt: BASE_ENTERED_AT + 5000
196
- const aliceJoinedFirst = makeCollaborator( {
197
- collaboratorInfo: {
198
- id: 100,
199
- name: 'Alice',
200
- slug: 'alice',
201
- avatar_urls: {},
202
- browserType: 'Chrome',
203
- enteredAt: BASE_ENTERED_AT + 1000, // joined earlier than me
204
- },
200
+ it( 'is skipped when the collaborator entered before the current user', () => {
201
+ renderNotifications();
202
+
203
+ mockRegistered.join?.( alice, me );
204
+
205
+ expect( mockCreateNotice ).not.toHaveBeenCalled();
206
+ } );
207
+
208
+ it( 'does not fire when join notifications are disabled', () => {
209
+ renderNotifications( {
210
+ showCollaborationJoinNotifications: false,
205
211
  } );
206
212
 
207
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
213
+ // The hook unsubscribes by passing a null postId...
214
+ expect( mockRegistered.joinPostId ).toBeNull();
208
215
 
209
- // Simulate the core-data hook firing the join callback
210
- mockOnJoinCallback?.( aliceJoinedFirst, me );
216
+ // ...while the other two hooks stay wired to the real post id,
217
+ // proving this preference doesn't accidentally couple with them.
218
+ expect( mockRegistered.leavePostId ).toBe( 123 );
219
+ expect( mockRegistered.savePostId ).toBe( 123 );
220
+
221
+ // ...and the callback itself guards against any queued event.
222
+ mockRegistered.join?.( bob, me );
223
+ expect( mockCreateNotice ).not.toHaveBeenCalled();
224
+ } );
225
+
226
+ it( 'stops firing once join notifications are toggled off mid-session', () => {
227
+ const { rerender } = renderNotifications();
228
+
229
+ state = {
230
+ ...state,
231
+ showCollaborationJoinNotifications: false,
232
+ };
233
+ rerender();
234
+
235
+ mockRegistered.join?.( bob, me );
211
236
 
212
237
  expect( mockCreateNotice ).not.toHaveBeenCalled();
213
238
  } );
214
239
  } );
215
240
 
216
241
  describe( 'collaborator leave notifications', () => {
217
- it( 'fires a leave notification when a collaborator leaves', () => {
218
- const alice = makeCollaborator();
219
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
242
+ it( 'fires when a collaborator leaves', () => {
243
+ renderNotifications();
220
244
 
221
- // Simulate the core-data hook firing the leave callback
222
- mockOnLeaveCallback?.( alice );
245
+ mockRegistered.leave?.( alice );
223
246
 
224
247
  expect( mockCreateNotice ).toHaveBeenCalledWith(
225
248
  'info',
226
249
  'Alice has left the post.',
227
250
  expect.objectContaining( {
251
+ id: 'collab-user-exited-100',
228
252
  type: 'snackbar',
229
253
  isDismissible: false,
230
- id: 'collab-user-exited-100',
231
254
  } )
232
255
  );
233
256
  } );
234
- } );
235
257
 
236
- describe( 'post updated notifications', () => {
237
- it( 'fires a post updated notification when a collaborator saves (draft)', () => {
238
- const alice = makeCollaborator();
239
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
258
+ it( 'does not fire when leave notifications are disabled', () => {
259
+ renderNotifications( {
260
+ showCollaborationLeaveNotifications: false,
261
+ } );
240
262
 
241
- // Simulate the core-data hook firing the save callback
242
- mockOnPostSaveCallback?.(
243
- {
244
- savedAt: Date.now(),
245
- savedByClientId: alice.clientId,
246
- postStatus: undefined,
247
- },
248
- alice,
249
- null
250
- );
263
+ expect( mockRegistered.leavePostId ).toBeNull();
251
264
 
252
- expect( mockCreateNotice ).toHaveBeenCalledWith(
253
- 'info',
254
- 'Draft saved by Alice.',
255
- expect.objectContaining( {
256
- type: 'snackbar',
257
- isDismissible: false,
258
- id: 'collab-post-updated-100',
259
- } )
260
- );
265
+ // The other two hooks stay wired to the real post id.
266
+ expect( mockRegistered.joinPostId ).toBe( 123 );
267
+ expect( mockRegistered.savePostId ).toBe( 123 );
268
+
269
+ mockRegistered.leave?.( alice );
270
+ expect( mockCreateNotice ).not.toHaveBeenCalled();
261
271
  } );
262
272
 
263
- it( 'fires a post updated notification with "Post updated" for published status', () => {
264
- mockEditorState = {
265
- ...mockEditorState,
266
- postStatus: 'publish',
273
+ it( 'stops firing once leave notifications are toggled off mid-session', () => {
274
+ const { rerender } = renderNotifications();
275
+
276
+ state = {
277
+ ...state,
278
+ showCollaborationLeaveNotifications: false,
267
279
  };
268
- const alice = makeCollaborator();
269
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
280
+ rerender();
270
281
 
271
- mockOnPostSaveCallback?.(
272
- {
273
- savedAt: Date.now(),
274
- savedByClientId: alice.clientId,
275
- postStatus: 'publish',
276
- },
277
- alice,
278
- null
279
- );
282
+ mockRegistered.leave?.( alice );
280
283
 
281
- expect( mockCreateNotice ).toHaveBeenCalledWith(
282
- 'info',
283
- 'Post updated by Alice.',
284
- expect.objectContaining( {
285
- id: 'collab-post-updated-100',
286
- } )
287
- );
284
+ expect( mockCreateNotice ).not.toHaveBeenCalled();
288
285
  } );
286
+ } );
289
287
 
290
- it( 'fires a "Post published" notification on first publish (no previous save)', () => {
291
- mockEditorState = {
292
- ...mockEditorState,
288
+ describe( 'post save notifications', () => {
289
+ it.each( [
290
+ {
291
+ description: 'a draft is saved',
293
292
  postStatus: 'draft',
294
- };
295
- const alice = makeCollaborator();
296
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
293
+ saveEventStatus: undefined,
294
+ prevEvent: null,
295
+ expected: 'Draft saved by Alice.',
296
+ },
297
+ {
298
+ description: 'an already-published post is updated',
299
+ postStatus: 'publish',
300
+ saveEventStatus: 'publish',
301
+ prevEvent: null,
302
+ expected: 'Post updated by Alice.',
303
+ },
304
+ {
305
+ description:
306
+ 'a post is first published (no previous save event)',
307
+ postStatus: 'draft',
308
+ saveEventStatus: 'publish',
309
+ prevEvent: null,
310
+ expected: 'Post published by Alice.',
311
+ },
312
+ {
313
+ description:
314
+ 'a post is first published (prevEvent carries the prior draft status)',
315
+ postStatus: 'publish',
316
+ saveEventStatus: 'publish',
317
+ prevEvent: {
318
+ savedAt: 0,
319
+ savedByClientId: 1,
320
+ postStatus: 'draft',
321
+ },
322
+ expected: 'Post published by Alice.',
323
+ },
324
+ ] )(
325
+ 'fires "$expected" when $description',
326
+ ( { postStatus, saveEventStatus, prevEvent, expected } ) => {
327
+ renderNotifications( { postStatus } );
328
+
329
+ mockRegistered.save?.(
330
+ {
331
+ savedAt: Date.now(),
332
+ savedByClientId: alice.clientId,
333
+ postStatus: saveEventStatus,
334
+ },
335
+ alice,
336
+ prevEvent
337
+ );
338
+
339
+ expect( mockCreateNotice ).toHaveBeenCalledWith(
340
+ 'info',
341
+ expected,
342
+ expect.objectContaining( {
343
+ id: 'collab-post-updated-100',
344
+ type: 'snackbar',
345
+ isDismissible: false,
346
+ } )
347
+ );
348
+ }
349
+ );
350
+
351
+ it( 'does not fire when postStatus is unknown', () => {
352
+ renderNotifications( { postStatus: undefined } );
297
353
 
298
- mockOnPostSaveCallback?.(
354
+ mockRegistered.save?.(
299
355
  {
300
356
  savedAt: Date.now(),
301
357
  savedByClientId: alice.clientId,
302
- postStatus: 'publish',
358
+ postStatus: undefined,
303
359
  },
304
360
  alice,
305
361
  null
306
362
  );
307
363
 
308
- expect( mockCreateNotice ).toHaveBeenCalledWith(
309
- 'info',
310
- 'Post published by Alice.',
311
- expect.objectContaining( {
312
- id: 'collab-post-updated-100',
313
- } )
314
- );
364
+ expect( mockCreateNotice ).not.toHaveBeenCalled();
315
365
  } );
316
366
 
317
- it( 'fires a "Post published" notification using prevEvent status for transition detection', () => {
318
- // Redux postStatus may already be 'publish' by the time the
319
- // callback fires. The prevEvent carries the accurate prior status.
320
- mockEditorState = {
321
- ...mockEditorState,
322
- postStatus: 'publish',
323
- };
324
- const alice = makeCollaborator();
325
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
326
-
327
- mockOnPostSaveCallback?.(
328
- {
329
- savedAt: Date.now() + 2000,
330
- savedByClientId: alice.clientId,
331
- postStatus: 'publish',
332
- },
333
- alice,
334
- {
335
- savedAt: Date.now() + 1000,
336
- savedByClientId: alice.clientId,
337
- postStatus: 'draft',
338
- }
339
- );
367
+ it( 'does not fire when post-save notifications are disabled', () => {
368
+ renderNotifications( {
369
+ showCollaborationPostSaveNotifications: false,
370
+ } );
340
371
 
341
- expect( mockCreateNotice ).toHaveBeenCalledWith(
342
- 'info',
343
- 'Post published by Alice.',
344
- expect.objectContaining( {
345
- id: 'collab-post-updated-100',
346
- } )
347
- );
348
- } );
372
+ expect( mockRegistered.savePostId ).toBeNull();
349
373
 
350
- it( 'does not fire a notification when postStatus is undefined', () => {
351
- mockEditorState = {
352
- ...mockEditorState,
353
- postStatus: undefined as any,
354
- };
355
- const alice = makeCollaborator();
356
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
374
+ // The other two hooks stay wired to the real post id.
375
+ expect( mockRegistered.joinPostId ).toBe( 123 );
376
+ expect( mockRegistered.leavePostId ).toBe( 123 );
357
377
 
358
- mockOnPostSaveCallback?.(
378
+ mockRegistered.save?.(
359
379
  {
360
380
  savedAt: Date.now(),
361
381
  savedByClientId: alice.clientId,
362
- postStatus: undefined,
382
+ postStatus: 'publish',
363
383
  },
364
384
  alice,
365
385
  null
366
386
  );
367
-
368
387
  expect( mockCreateNotice ).not.toHaveBeenCalled();
369
388
  } );
370
389
  } );
371
390
 
372
- describe( 'when notifications are disabled', () => {
373
- it( 'passes null postId to hooks when showCollaborationNotifications preference is false', () => {
374
- mockEditorState = {
375
- ...mockEditorState,
376
- showCollaborationNotifications: false,
377
- };
378
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
379
-
380
- expect( lastJoinPostId ).toBeNull();
381
- expect( lastLeavePostId ).toBeNull();
382
- expect( lastSavePostId ).toBeNull();
383
- } );
384
-
385
- it( 'passes null postId to hooks when collaboration is disabled', () => {
386
- mockEditorState = {
387
- ...mockEditorState,
388
- isCollaborationEnabled: false,
389
- };
390
- renderHook( () => useCollaboratorNotifications( 123, 'post' ) );
391
+ describe( 'when collaboration is disabled entirely', () => {
392
+ it( 'unsubscribes all three hooks', () => {
393
+ renderNotifications( { isCollaborationEnabled: false } );
391
394
 
392
- expect( lastJoinPostId ).toBeNull();
393
- expect( lastLeavePostId ).toBeNull();
394
- expect( lastSavePostId ).toBeNull();
395
+ expect( mockRegistered.joinPostId ).toBeNull();
396
+ expect( mockRegistered.leavePostId ).toBeNull();
397
+ expect( mockRegistered.savePostId ).toBeNull();
395
398
  } );
396
399
  } );
397
400
  } );