@shopify/ui-extensions-server-kit 0.0.0-experimental-20260902222805

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 (202) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +75 -0
  3. package/dist/ExtensionServerClient/ExtensionServerClient.d.ts +27 -0
  4. package/dist/ExtensionServerClient/ExtensionServerClient.test.d.ts +9 -0
  5. package/dist/ExtensionServerClient/index.d.ts +2 -0
  6. package/dist/ExtensionServerClient/server-types.d.ts +66 -0
  7. package/dist/ExtensionServerClient/types.d.ts +124 -0
  8. package/dist/_virtual/jsx-runtime.cjs.js +1 -0
  9. package/dist/_virtual/jsx-runtime.cjs2.js +1 -0
  10. package/dist/_virtual/jsx-runtime.es.js +5 -0
  11. package/dist/_virtual/jsx-runtime.es2.js +4 -0
  12. package/dist/_virtual/react-jsx-runtime.development.cjs.js +1 -0
  13. package/dist/_virtual/react-jsx-runtime.development.es.js +4 -0
  14. package/dist/_virtual/react-jsx-runtime.production.min.cjs.js +1 -0
  15. package/dist/_virtual/react-jsx-runtime.production.min.es.js +4 -0
  16. package/dist/context/ExtensionServerProvider.d.ts +3 -0
  17. package/dist/context/ExtensionServerProvider.test.d.ts +1 -0
  18. package/dist/context/constants.d.ts +2 -0
  19. package/dist/context/index.d.ts +3 -0
  20. package/dist/context/types.d.ts +12 -0
  21. package/dist/hooks/index.d.ts +3 -0
  22. package/dist/hooks/useExtensionServerContext.d.ts +1 -0
  23. package/dist/hooks/useExtensionServerState.d.ts +1 -0
  24. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  25. package/dist/i18n.d.ts +68 -0
  26. package/dist/i18n.test.d.ts +1 -0
  27. package/dist/index.cjs.js +1 -0
  28. package/dist/index.cjs2.js +1 -0
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.es.js +46 -0
  31. package/dist/index.es2.js +8 -0
  32. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.cjs.js +22 -0
  33. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.es.js +604 -0
  34. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.cjs.js +9 -0
  35. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.es.js +28 -0
  36. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.cjs.js +1 -0
  37. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.es.js +10 -0
  38. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.cjs.js +1 -0
  39. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.es.js +136 -0
  40. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.cjs.js +1 -0
  41. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.es.js +4 -0
  42. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.cjs.js +1 -0
  43. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.es.js +30 -0
  44. package/dist/packages/ui-extensions-server-kit/src/context/constants.cjs.js +1 -0
  45. package/dist/packages/ui-extensions-server-kit/src/context/constants.es.js +13 -0
  46. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.cjs.js +1 -0
  47. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.es.js +7 -0
  48. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.cjs.js +1 -0
  49. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.es.js +9 -0
  50. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.cjs.js +1 -0
  51. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.es.js +5 -0
  52. package/dist/packages/ui-extensions-server-kit/src/i18n.cjs.js +1 -0
  53. package/dist/packages/ui-extensions-server-kit/src/i18n.es.js +60 -0
  54. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.cjs.js +1 -0
  55. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.es.js +37 -0
  56. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.cjs.js +1 -0
  57. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.es.js +7 -0
  58. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.cjs.js +1 -0
  59. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.es.js +57 -0
  60. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.cjs.js +1 -0
  61. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.es.js +25 -0
  62. package/dist/packages/ui-extensions-server-kit/src/testing/app.cjs.js +1 -0
  63. package/dist/packages/ui-extensions-server-kit/src/testing/app.es.js +16 -0
  64. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.cjs.js +1 -0
  65. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.es.js +66 -0
  66. package/dist/packages/ui-extensions-server-kit/src/types.cjs.js +1 -0
  67. package/dist/packages/ui-extensions-server-kit/src/types.es.js +4 -0
  68. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.cjs.js +1 -0
  69. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.es.js +7 -0
  70. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.cjs.js +1 -0
  71. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.es.js +7 -0
  72. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.cjs.js +1 -0
  73. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.es.js +7 -0
  74. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.cjs.js +1 -0
  75. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.es.js +5 -0
  76. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.cjs.js +1 -0
  77. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.es.js +14 -0
  78. package/dist/packages/ui-extensions-server-kit/src/utilities/set.cjs.js +1 -0
  79. package/dist/packages/ui-extensions-server-kit/src/utilities/set.es.js +18 -0
  80. package/dist/state/actions/actions.d.ts +6 -0
  81. package/dist/state/actions/index.d.ts +2 -0
  82. package/dist/state/actions/types.d.ts +21 -0
  83. package/dist/state/index.d.ts +2 -0
  84. package/dist/state/reducers/constants.d.ts +2 -0
  85. package/dist/state/reducers/extensionServerReducer.d.ts +3 -0
  86. package/dist/state/reducers/extensionServerReducer.test.d.ts +1 -0
  87. package/dist/state/reducers/index.d.ts +3 -0
  88. package/dist/state/reducers/types.d.ts +6 -0
  89. package/dist/testing/MockExtensionServerProvider.d.ts +8 -0
  90. package/dist/testing/app.d.ts +2 -0
  91. package/dist/testing/extensions.d.ts +6 -0
  92. package/dist/testing/index.d.ts +3 -0
  93. package/dist/types.d.ts +196 -0
  94. package/dist/utilities/assetToString.d.ts +2 -0
  95. package/dist/utilities/assetToString.test.d.ts +1 -0
  96. package/dist/utilities/index.d.ts +6 -0
  97. package/dist/utilities/isUIExtension.d.ts +1 -0
  98. package/dist/utilities/isValidSurface.d.ts +2 -0
  99. package/dist/utilities/noop.d.ts +1 -0
  100. package/dist/utilities/replaceUpdated.d.ts +1 -0
  101. package/dist/utilities/replaceUpdated.test.d.ts +1 -0
  102. package/dist/utilities/set.d.ts +4 -0
  103. package/dist/utilities/set.test.d.ts +1 -0
  104. package/index.d.ts +1 -0
  105. package/index.js +1 -0
  106. package/index.mjs +1 -0
  107. package/node_modules/@testing-library/react/LICENSE +20 -0
  108. package/node_modules/@testing-library/react/README.md +692 -0
  109. package/node_modules/@testing-library/react/dist/@testing-library/react.cjs.js +537 -0
  110. package/node_modules/@testing-library/react/dist/@testing-library/react.esm.js +497 -0
  111. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.cjs.js +501 -0
  112. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.esm.js +461 -0
  113. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js +502 -0
  114. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js.map +1 -0
  115. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js +2 -0
  116. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js.map +1 -0
  117. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js +538 -0
  118. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js.map +1 -0
  119. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js +2 -0
  120. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js.map +1 -0
  121. package/node_modules/@testing-library/react/dist/act-compat.js +80 -0
  122. package/node_modules/@testing-library/react/dist/config.js +33 -0
  123. package/node_modules/@testing-library/react/dist/fire-event.js +70 -0
  124. package/node_modules/@testing-library/react/dist/index.js +52 -0
  125. package/node_modules/@testing-library/react/dist/pure.js +356 -0
  126. package/node_modules/@testing-library/react/dont-cleanup-after-each.js +1 -0
  127. package/node_modules/@testing-library/react/package.json +116 -0
  128. package/node_modules/@testing-library/react/pure.d.ts +1 -0
  129. package/node_modules/@testing-library/react/pure.js +2 -0
  130. package/node_modules/@testing-library/react/types/index.d.ts +287 -0
  131. package/node_modules/@testing-library/react/types/pure.d.ts +1 -0
  132. package/node_modules/@types/react/LICENSE +21 -0
  133. package/node_modules/@types/react/README.md +15 -0
  134. package/node_modules/@types/react/canary.d.ts +166 -0
  135. package/node_modules/@types/react/experimental.d.ts +127 -0
  136. package/node_modules/@types/react/global.d.ts +160 -0
  137. package/node_modules/@types/react/index.d.ts +4543 -0
  138. package/node_modules/@types/react/jsx-dev-runtime.d.ts +45 -0
  139. package/node_modules/@types/react/jsx-runtime.d.ts +36 -0
  140. package/node_modules/@types/react/package.json +206 -0
  141. package/node_modules/@types/react/ts5.0/canary.d.ts +166 -0
  142. package/node_modules/@types/react/ts5.0/experimental.d.ts +127 -0
  143. package/node_modules/@types/react/ts5.0/global.d.ts +160 -0
  144. package/node_modules/@types/react/ts5.0/index.d.ts +4530 -0
  145. package/node_modules/@types/react/ts5.0/jsx-dev-runtime.d.ts +44 -0
  146. package/node_modules/@types/react/ts5.0/jsx-runtime.d.ts +35 -0
  147. package/node_modules/@vitejs/plugin-react/LICENSE +21 -0
  148. package/node_modules/@vitejs/plugin-react/README.md +159 -0
  149. package/node_modules/@vitejs/plugin-react/dist/index.d.ts +75 -0
  150. package/node_modules/@vitejs/plugin-react/dist/index.js +401 -0
  151. package/node_modules/@vitejs/plugin-react/dist/refresh-runtime.js +663 -0
  152. package/node_modules/@vitejs/plugin-react/package.json +77 -0
  153. package/node_modules/@vitejs/plugin-react/types/preamble.d.ts +1 -0
  154. package/package.json +64 -0
  155. package/project.json +40 -0
  156. package/scripts/create-entry-files.ts +44 -0
  157. package/src/ExtensionServerClient/ExtensionServerClient.test.ts +1238 -0
  158. package/src/ExtensionServerClient/ExtensionServerClient.ts +320 -0
  159. package/src/ExtensionServerClient/index.ts +2 -0
  160. package/src/ExtensionServerClient/server-types.ts +65 -0
  161. package/src/ExtensionServerClient/types.ts +159 -0
  162. package/src/context/ExtensionServerProvider.test.tsx +318 -0
  163. package/src/context/ExtensionServerProvider.tsx +48 -0
  164. package/src/context/constants.ts +16 -0
  165. package/src/context/index.ts +3 -0
  166. package/src/context/types.ts +14 -0
  167. package/src/hooks/index.ts +3 -0
  168. package/src/hooks/useExtensionServerContext.ts +4 -0
  169. package/src/hooks/useExtensionServerState.ts +6 -0
  170. package/src/hooks/useIsomorphicLayoutEffect.ts +6 -0
  171. package/src/i18n.test.ts +417 -0
  172. package/src/i18n.ts +205 -0
  173. package/src/index.ts +7 -0
  174. package/src/state/actions/actions.ts +36 -0
  175. package/src/state/actions/index.ts +2 -0
  176. package/src/state/actions/types.ts +26 -0
  177. package/src/state/index.ts +2 -0
  178. package/src/state/reducers/constants.ts +6 -0
  179. package/src/state/reducers/extensionServerReducer.test.ts +165 -0
  180. package/src/state/reducers/extensionServerReducer.ts +87 -0
  181. package/src/state/reducers/index.ts +3 -0
  182. package/src/state/reducers/types.ts +7 -0
  183. package/src/testing/MockExtensionServerProvider.tsx +36 -0
  184. package/src/testing/app.ts +15 -0
  185. package/src/testing/extensions.ts +71 -0
  186. package/src/testing/index.ts +3 -0
  187. package/src/types.ts +204 -0
  188. package/src/utilities/assetToString.test.ts +16 -0
  189. package/src/utilities/assetToString.ts +7 -0
  190. package/src/utilities/index.ts +6 -0
  191. package/src/utilities/isUIExtension.ts +8 -0
  192. package/src/utilities/isValidSurface.ts +6 -0
  193. package/src/utilities/noop.ts +1 -0
  194. package/src/utilities/replaceUpdated.test.ts +26 -0
  195. package/src/utilities/replaceUpdated.ts +15 -0
  196. package/src/utilities/set.test.ts +19 -0
  197. package/src/utilities/set.ts +35 -0
  198. package/testing.d.ts +1 -0
  199. package/testing.js +1 -0
  200. package/testing.mjs +1 -0
  201. package/tests/setup.ts +3 -0
  202. package/vite.config.mts +57 -0
@@ -0,0 +1,1238 @@
1
+ import {ExtensionServerClient} from './ExtensionServerClient'
2
+ import {DeepPartial} from '../types'
3
+ import {mockApp} from '../testing'
4
+
5
+ import {Localization} from 'i18n.js'
6
+ import {beforeEach, expect, test, vi, describe} from 'vitest'
7
+
8
+ // Mock React's act function because jest-websocket-mock tries to use it
9
+ vi.mock('react-dom/test-utils', () => ({
10
+ act: async (callback: () => Promise<void> | void) => {
11
+ return callback()
12
+ },
13
+ }))
14
+
15
+ // Create a custom mock WebSocket implementation to avoid using jest-websocket-mock
16
+ class MockWebSocketServer {
17
+ clients: MockWebSocket[] = []
18
+ messages: any[] = []
19
+
20
+ connect(socket: MockWebSocket) {
21
+ // OPEN state
22
+ this.clients.push(socket)
23
+ socket.readyState = 1
24
+ socket.onopen?.({} as Event)
25
+ }
26
+
27
+ send(data: any) {
28
+ this.messages.push(data)
29
+ this.clients.forEach((client) => {
30
+ const event = new MessageEvent('message', {
31
+ data: typeof data === 'string' ? data : JSON.stringify(data),
32
+ })
33
+ client.onmessage?.(event)
34
+ })
35
+ }
36
+
37
+ close() {
38
+ // CLOSED state
39
+ this.clients.forEach((client) => {
40
+ client.readyState = 3
41
+ client.onclose?.({} as CloseEvent)
42
+ })
43
+ this.clients = []
44
+ this.messages = []
45
+ }
46
+ }
47
+
48
+ class MockWebSocket implements Partial<WebSocket> {
49
+ url: string
50
+ readyState = 0
51
+ onopen: ((ev: Event) => any) | null = null
52
+ onmessage: ((ev: MessageEvent) => any) | null = null
53
+ onclose: ((ev: CloseEvent) => any) | null = null
54
+ server: MockWebSocketServer
55
+ private eventListeners: Record<string, Set<EventListener>> = {
56
+ open: new Set(),
57
+ message: new Set(),
58
+ close: new Set(),
59
+ error: new Set(),
60
+ }
61
+
62
+ constructor(url: string, server: MockWebSocketServer) {
63
+ this.url = url
64
+ this.server = server
65
+ }
66
+
67
+ addEventListener(type: string, listener: EventListener): void {
68
+ if (!this.eventListeners[type]) {
69
+ this.eventListeners[type] = new Set()
70
+ }
71
+ this.eventListeners[type].add(listener)
72
+
73
+ // Map standard event handlers to addEventListener
74
+ if (type === 'open' && this.onopen === null) {
75
+ this.onopen = (event) => {
76
+ this.eventListeners.open.forEach((listener) => listener(event))
77
+ }
78
+ } else if (type === 'message' && this.onmessage === null) {
79
+ this.onmessage = (event) => {
80
+ this.eventListeners.message.forEach((listener) => listener(event))
81
+ }
82
+ } else if (type === 'close' && this.onclose === null) {
83
+ this.onclose = (event) => {
84
+ this.eventListeners.close.forEach((listener) => listener(event))
85
+ }
86
+ }
87
+ }
88
+
89
+ removeEventListener(type: string, listener: EventListener): void {
90
+ if (this.eventListeners[type]) {
91
+ this.eventListeners[type].delete(listener)
92
+ }
93
+ }
94
+
95
+ dispatchEvent(event: Event): boolean {
96
+ const type = event.type
97
+
98
+ if (type === 'open' && this.onopen) {
99
+ this.onopen(event)
100
+ } else if (type === 'message' && this.onmessage) {
101
+ this.onmessage(event as MessageEvent)
102
+ } else if (type === 'close' && this.onclose) {
103
+ this.onclose(event as CloseEvent)
104
+ }
105
+
106
+ if (this.eventListeners[type]) {
107
+ this.eventListeners[type].forEach((listener) => listener(event))
108
+ }
109
+
110
+ return true
111
+ }
112
+
113
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
114
+ this.server.messages.push(data)
115
+ }
116
+
117
+ close() {
118
+ this.readyState = 3
119
+ this.onclose?.({} as CloseEvent)
120
+ }
121
+ }
122
+
123
+ // Update the connection interface to include automaticConnect
124
+ declare module './ExtensionServerClient' {
125
+ namespace ExtensionServer {
126
+ interface ConnectionOptions {
127
+ url?: string
128
+ automaticConnect?: boolean
129
+ }
130
+ }
131
+ }
132
+
133
+ // Test constants
134
+ const TEST_CONNECTION_URL = 'ws://example-host.com:8000/extensions/'
135
+
136
+ const defaultOptions = {
137
+ connection: {
138
+ url: TEST_CONNECTION_URL,
139
+ automaticConnect: true,
140
+ },
141
+ }
142
+
143
+ describe('ExtensionServerClient', () => {
144
+ let mockSocketServer: MockWebSocketServer
145
+ let mockSocket: MockWebSocket
146
+
147
+ // Create a WebSocket factory function that returns our mock
148
+ const createMockWebSocket = (url: string) => {
149
+ mockSocket = new MockWebSocket(url, mockSocketServer)
150
+ return mockSocket
151
+ }
152
+
153
+ beforeEach(() => {
154
+ // Set up mock socket server
155
+ mockSocketServer = new MockWebSocketServer()
156
+
157
+ // Mock the global WebSocket to use our implementation
158
+ vi.spyOn(globalThis, 'WebSocket').mockImplementation(function (urlParam: any) {
159
+ // Handle both string URLs and URL objects by extracting the string representation
160
+ const urlString = typeof urlParam === 'string' ? urlParam : urlParam.toString()
161
+ return createMockWebSocket(urlString) as unknown as WebSocket
162
+ })
163
+ })
164
+
165
+ describe('initialization', () => {
166
+ test('connects to the target websocket', async () => {
167
+ // Create client
168
+ const client = new ExtensionServerClient(defaultOptions)
169
+
170
+ // Connect the mock socket to simulate WebSocket connection
171
+ mockSocketServer.connect(mockSocket)
172
+
173
+ // Verify connection is established
174
+ expect(client.connection).toBeDefined()
175
+ expect(mockSocketServer.clients.length).toBe(1)
176
+ })
177
+
178
+ test('does not connect to the target websocket if "automaticConnect" is false', async () => {
179
+ // Create client with automaticConnect: false
180
+ const client = new ExtensionServerClient({
181
+ connection: {
182
+ url: TEST_CONNECTION_URL,
183
+ automaticConnect: false,
184
+ },
185
+ })
186
+
187
+ // Verify connection is not established
188
+ expect(client.connection).toBeUndefined()
189
+ expect(mockSocketServer.clients.length).toBe(0)
190
+ })
191
+ })
192
+
193
+ describe('on()', () => {
194
+ test('sends data with extensions filtered by surface option on "connected" event', async () => {
195
+ // Create client
196
+ const client = new ExtensionServerClient({
197
+ connection: {
198
+ url: TEST_CONNECTION_URL,
199
+ automaticConnect: true,
200
+ },
201
+ surface: 'admin',
202
+ })
203
+
204
+ // Mock connection
205
+ mockSocketServer.connect(mockSocket)
206
+
207
+ // Create spy for the connected event
208
+ const connectSpy = vi.fn()
209
+ client.on('connected', connectSpy)
210
+
211
+ // Send connected event with mock data
212
+ const data = {
213
+ app: mockApp(),
214
+ extensions: [
215
+ {uuid: '123', surface: 'admin'},
216
+ {uuid: '456', surface: 'checkout'},
217
+ {uuid: '789', surface: '', extensionPoints: [{surface: 'admin'}]},
218
+ ],
219
+ }
220
+
221
+ // Send the event
222
+ mockSocketServer.send({event: 'connected', data})
223
+
224
+ // Verify correct data is filtered and passed to the callback
225
+ expect(connectSpy).toHaveBeenCalledTimes(1)
226
+ expect(connectSpy).toHaveBeenCalledWith(
227
+ expect.objectContaining({
228
+ extensions: expect.arrayContaining([
229
+ expect.objectContaining({uuid: '123', surface: 'admin'}),
230
+ expect.objectContaining({uuid: '789'}),
231
+ ]),
232
+ }),
233
+ )
234
+ // Verify checkout extension is filtered out
235
+ const calledWith = connectSpy.mock.calls[0][0]
236
+ const extensionIds = calledWith.extensions.map((ext: any) => ext.uuid)
237
+ expect(extensionIds).not.toContain('456')
238
+ })
239
+
240
+ test('sends data with all extensions when surface option is not valid on "connected" event', async () => {
241
+ // Create client with invalid surface
242
+ const client = new ExtensionServerClient({
243
+ connection: {
244
+ url: TEST_CONNECTION_URL,
245
+ automaticConnect: true,
246
+ },
247
+ surface: 'abc' as any,
248
+ })
249
+
250
+ // Mock connection
251
+ mockSocketServer.connect(mockSocket)
252
+
253
+ // Create spy for the connected event
254
+ const connectSpy = vi.fn()
255
+ client.on('connected', connectSpy)
256
+
257
+ // Send connected event with mock data
258
+ const data = {
259
+ app: mockApp(),
260
+ extensions: [
261
+ {uuid: '123', surface: 'admin'},
262
+ {uuid: '456', surface: 'checkout'},
263
+ ],
264
+ }
265
+
266
+ // Send the event
267
+ mockSocketServer.send({event: 'connected', data})
268
+
269
+ // Verify all extensions are passed to the callback
270
+ expect(connectSpy).toHaveBeenCalledTimes(1)
271
+ expect(connectSpy).toHaveBeenCalledWith(
272
+ expect.objectContaining({
273
+ extensions: expect.arrayContaining([
274
+ expect.objectContaining({uuid: '123'}),
275
+ expect.objectContaining({uuid: '456'}),
276
+ ]),
277
+ }),
278
+ )
279
+ })
280
+
281
+ test('sends data with translatable props as-is for UI extensions when locales option is not provided on "connected" event', async () => {
282
+ // Create client
283
+ const client = new ExtensionServerClient(defaultOptions)
284
+
285
+ // Mock connection
286
+ mockSocketServer.connect(mockSocket)
287
+
288
+ // Create spy
289
+ const connectSpy = vi.fn()
290
+ client.on('connected', connectSpy)
291
+
292
+ // Define localization data
293
+ const localization: Localization = {
294
+ defaultLocale: 'en',
295
+ translations: {
296
+ ja: {
297
+ welcome: 'いらっしゃいませ!',
298
+ },
299
+ en: {
300
+ welcome: 'Welcome!',
301
+ },
302
+ fr: {
303
+ welcome: 'Bienvenue!',
304
+ },
305
+ },
306
+ lastUpdated: 1684164163736,
307
+ }
308
+
309
+ // Mock data
310
+ const data = {
311
+ app: mockApp(),
312
+ extensions: [
313
+ {
314
+ uuid: '123',
315
+ type: 'ui_extension',
316
+ localization,
317
+ extensionPoints: [{localization}],
318
+ },
319
+ {uuid: '456', type: 'ui_extension', localization: null, extensionPoints: [{localization: null}]},
320
+ {uuid: '789', type: 'product_subscription'},
321
+ ],
322
+ }
323
+
324
+ // Send event
325
+ mockSocketServer.send({event: 'connected', data})
326
+
327
+ // Verify props are passed as-is
328
+ expect(connectSpy).toHaveBeenCalledTimes(1)
329
+ expect(connectSpy).toHaveBeenCalledWith(
330
+ expect.objectContaining({
331
+ extensions: expect.arrayContaining(data.extensions),
332
+ }),
333
+ )
334
+ })
335
+
336
+ test('sends data as-is on "connected" event', async () => {
337
+ // Create client
338
+ const client = new ExtensionServerClient(defaultOptions)
339
+
340
+ // Mock connection
341
+ mockSocketServer.connect(mockSocket)
342
+
343
+ // Create spy
344
+ const connectSpy = vi.fn()
345
+ client.on('connected', connectSpy)
346
+
347
+ // Mock data
348
+ const data = {
349
+ app: mockApp(),
350
+ extensions: [
351
+ {uuid: '123', type: 'ui_extension', name: 'Extension 123'},
352
+ {uuid: '456', type: 'checkout_ui_extension', name: 'Extension 456'},
353
+ {uuid: '789', type: 'product_subscription', name: 'Extension 789'},
354
+ ],
355
+ }
356
+
357
+ // Send event
358
+ mockSocketServer.send({event: 'connected', data})
359
+
360
+ // Verify props are passed as-is
361
+ expect(connectSpy).toHaveBeenCalledTimes(1)
362
+ expect(connectSpy).toHaveBeenCalledWith(
363
+ expect.objectContaining({
364
+ extensions: expect.arrayContaining(data.extensions),
365
+ }),
366
+ )
367
+ })
368
+
369
+ test('sends data with translated props for UI extensions when locales option is provided on "connected" event', async () => {
370
+ // Create client with locales using direct type assertion to DeepPartial
371
+ const client = new ExtensionServerClient({
372
+ ...defaultOptions,
373
+ locales: {user: 'ja', shop: 'fr'} as unknown as DeepPartial<any>,
374
+ })
375
+
376
+ // Mock connection
377
+ mockSocketServer.connect(mockSocket)
378
+
379
+ // Create spy for the connected event
380
+ const connectSpy = vi.fn()
381
+ client.on('connected', connectSpy)
382
+
383
+ // Define localization data
384
+ const localization: Localization = {
385
+ defaultLocale: 'en',
386
+ translations: {
387
+ ja: {
388
+ welcome: 'いらっしゃいませ!',
389
+ description: '拡張子の説明',
390
+ },
391
+ en: {
392
+ welcome: 'Welcome!',
393
+ description: 'Extension description',
394
+ },
395
+ fr: {
396
+ welcome: 'Bienvenue!',
397
+ description: "Description de l'extension",
398
+ },
399
+ },
400
+ lastUpdated: 1684164163736,
401
+ }
402
+
403
+ // Create mock data - using type assertion to avoid DeepPartial errors
404
+ interface MockExtension {
405
+ uuid: string
406
+ type: string
407
+ name: string
408
+ description?: string
409
+ localization?: any
410
+ extensionPoints?: {
411
+ localization?: any
412
+ target?: string
413
+ surface?: string
414
+ name?: string
415
+ }[]
416
+ surface?: string
417
+ }
418
+
419
+ // Using a typed array to avoid DeepPartial issues
420
+ const mockExtensions: MockExtension[] = [
421
+ {
422
+ uuid: '123',
423
+ type: 'ui_extension',
424
+ name: 't:welcome',
425
+ description: 't:description',
426
+ localization,
427
+ extensionPoints: [{localization, target: 'admin.test', surface: 'admin'}],
428
+ },
429
+ {
430
+ uuid: '456',
431
+ type: 'ui_extension',
432
+ name: 'Fixed name t:',
433
+ localization: null,
434
+ extensionPoints: [{localization: null, name: 'Fixed name t:', target: 'admin.test', surface: 'admin'}],
435
+ },
436
+ {uuid: '789', type: 'product_subscription', name: 'Extension 789'},
437
+ ]
438
+
439
+ // Send the connected event
440
+ mockSocketServer.send({
441
+ event: 'connected',
442
+ data: {
443
+ app: mockApp(),
444
+ extensions: mockExtensions,
445
+ store: 'test-store',
446
+ },
447
+ })
448
+
449
+ // Verify event was handled
450
+ expect(connectSpy).toHaveBeenCalledTimes(1)
451
+
452
+ // Assert on specific properties without using complex matchers that trigger type errors
453
+ const connectedData = connectSpy.mock.calls[0][0]
454
+ expect(connectedData).toBeDefined()
455
+ expect(connectedData.extensions).toHaveLength(3)
456
+
457
+ // Find the translated extension
458
+ const translatedExt = connectedData.extensions.find((ext: any) => ext.uuid === '123')
459
+ expect(translatedExt).toBeDefined()
460
+
461
+ // Check translation worked properly - name should be translated
462
+ expect(translatedExt.name).not.toBe('t:welcome')
463
+ // Description should be translated
464
+ expect(translatedExt.description).not.toBe('t:description')
465
+
466
+ // Verify extension points were also translated
467
+ const extensionPoint = translatedExt.extensionPoints[0]
468
+ expect(extensionPoint.localization).toBeDefined()
469
+ expect(extensionPoint.localization.extensionLocale).toBe('ja')
470
+ })
471
+
472
+ test('listens to persist events', async () => {
473
+ // Create client
474
+ const client = new ExtensionServerClient(defaultOptions)
475
+
476
+ // Mock connection
477
+ mockSocketServer.connect(mockSocket)
478
+
479
+ // Create spy
480
+ const updateSpy = vi.fn()
481
+ client.on('update', updateSpy)
482
+
483
+ // Mock data
484
+ const data = {
485
+ app: mockApp(),
486
+ }
487
+
488
+ // Send event
489
+ mockSocketServer.send({event: 'update', data})
490
+
491
+ // Verify data is passed to the callback
492
+ expect(updateSpy).toHaveBeenCalledTimes(1)
493
+ expect(updateSpy).toHaveBeenCalledWith(data)
494
+ })
495
+
496
+ test('unsubscribes from persist events', async () => {
497
+ // Create client
498
+ const client = new ExtensionServerClient(defaultOptions)
499
+
500
+ // Mock connection
501
+ mockSocketServer.connect(mockSocket)
502
+
503
+ // Set up spy and get unsubscribe function
504
+ const updateSpy = vi.fn()
505
+ const unsubscribe = client.on('update', updateSpy)
506
+
507
+ // Unsubscribe
508
+ unsubscribe()
509
+
510
+ // Send event
511
+ mockSocketServer.send({
512
+ event: 'update',
513
+ data: {
514
+ app: mockApp(),
515
+ },
516
+ })
517
+
518
+ // Verify event wasn't handled
519
+ expect(updateSpy).not.toHaveBeenCalled()
520
+ })
521
+
522
+ test('listens to dispatch events', async () => {
523
+ // Create client
524
+ const client = new ExtensionServerClient(defaultOptions)
525
+
526
+ // Mock connection
527
+ mockSocketServer.connect(mockSocket)
528
+
529
+ // Set up spy
530
+ const unfocusSpy = vi.fn()
531
+ client.on('unfocus', unfocusSpy)
532
+
533
+ // Send event
534
+ mockSocketServer.send({event: 'dispatch', data: {type: 'unfocus'}})
535
+
536
+ // Verify event was handled
537
+ expect(unfocusSpy).toHaveBeenCalledTimes(1)
538
+ expect(unfocusSpy).toHaveBeenCalledWith(undefined)
539
+ })
540
+
541
+ test('sends data with extensions filtered by surface option on "update" event', async () => {
542
+ // Create client
543
+ const client = new ExtensionServerClient({
544
+ connection: {
545
+ url: TEST_CONNECTION_URL,
546
+ automaticConnect: true,
547
+ },
548
+ surface: 'admin',
549
+ })
550
+
551
+ // Mock connection
552
+ mockSocketServer.connect(mockSocket)
553
+
554
+ // Create spy
555
+ const updateSpy = vi.fn()
556
+ client.on('update', updateSpy)
557
+
558
+ // Mock data
559
+ const data = {
560
+ app: mockApp(),
561
+ extensions: [
562
+ {uuid: '123', surface: 'admin'},
563
+ {uuid: '456', surface: 'checkout'},
564
+ {uuid: '789', surface: '', extensionPoints: [{surface: 'admin'}]},
565
+ ],
566
+ }
567
+
568
+ // Send event
569
+ mockSocketServer.send({event: 'update', data})
570
+
571
+ // Verify correct data is filtered and passed to the callback
572
+ expect(updateSpy).toHaveBeenCalledTimes(1)
573
+ expect(updateSpy).toHaveBeenCalledWith(
574
+ expect.objectContaining({
575
+ extensions: expect.arrayContaining([
576
+ expect.objectContaining({uuid: '123'}),
577
+ expect.objectContaining({uuid: '789'}),
578
+ ]),
579
+ }),
580
+ )
581
+
582
+ // Verify checkout extension is filtered out
583
+ const calledWith = updateSpy.mock.calls[0][0]
584
+ const extensionIds = calledWith.extensions.map((ext: any) => ext.uuid)
585
+ expect(extensionIds).not.toContain('456')
586
+ })
587
+
588
+ test('sends data with all extensions when surface option is not valid on "update" event', async () => {
589
+ // Create client with invalid surface
590
+ const client = new ExtensionServerClient({
591
+ connection: {
592
+ url: TEST_CONNECTION_URL,
593
+ automaticConnect: true,
594
+ },
595
+ surface: 'abc' as any,
596
+ })
597
+
598
+ // Mock connection
599
+ mockSocketServer.connect(mockSocket)
600
+
601
+ // Create spy
602
+ const updateSpy = vi.fn()
603
+ client.on('update', updateSpy)
604
+
605
+ // Mock data
606
+ const data = {
607
+ app: mockApp(),
608
+ extensions: [
609
+ {uuid: '123', surface: 'admin'},
610
+ {uuid: '456', surface: 'checkout'},
611
+ ],
612
+ }
613
+
614
+ // Send event
615
+ mockSocketServer.send({event: 'update', data})
616
+
617
+ // Verify all extensions are included (not filtered)
618
+ expect(updateSpy).toHaveBeenCalledTimes(1)
619
+ expect(updateSpy).toHaveBeenCalledWith(
620
+ expect.objectContaining({
621
+ extensions: expect.arrayContaining(data.extensions),
622
+ }),
623
+ )
624
+ })
625
+
626
+ test('sends data with translatable props as-is when locales option is not provided on "update" event', async () => {
627
+ // Create client without locales
628
+ const client = new ExtensionServerClient(defaultOptions)
629
+
630
+ // Mock connection
631
+ mockSocketServer.connect(mockSocket)
632
+
633
+ // Create spy
634
+ const updateSpy = vi.fn()
635
+ client.on('update', updateSpy)
636
+
637
+ // Define localization data
638
+ const localization: Localization = {
639
+ defaultLocale: 'en',
640
+ translations: {
641
+ ja: {
642
+ welcome: 'いらっしゃいませ!',
643
+ description: '拡張子の説明',
644
+ },
645
+ en: {
646
+ welcome: 'Welcome!',
647
+ description: 'Extension description',
648
+ },
649
+ fr: {
650
+ welcome: 'Bienvenue!',
651
+ description: "Description de l'extension",
652
+ },
653
+ },
654
+ lastUpdated: 1684164163736,
655
+ }
656
+
657
+ // Mock data
658
+ const data = {
659
+ app: mockApp(),
660
+ extensions: [
661
+ {
662
+ uuid: '123',
663
+ type: 'ui_extension',
664
+ name: 't:welcome',
665
+ description: 't:description',
666
+ localization,
667
+ extensionPoints: [{localization}],
668
+ },
669
+ {
670
+ uuid: '456',
671
+ type: 'ui_extension',
672
+ name: 'Extension 456',
673
+ localization: null,
674
+ extensionPoints: [{localization: null}],
675
+ },
676
+ {uuid: '789', type: 'product_subscription'},
677
+ ],
678
+ }
679
+
680
+ // Send event
681
+ mockSocketServer.send({event: 'update', data})
682
+
683
+ // Verify props are passed as-is
684
+ expect(updateSpy).toHaveBeenCalledTimes(1)
685
+ expect(updateSpy).toHaveBeenCalledWith(
686
+ expect.objectContaining({
687
+ extensions: expect.arrayContaining(data.extensions),
688
+ }),
689
+ )
690
+ })
691
+
692
+ test('sends data with translated props when locales option is provided on "update" event', async () => {
693
+ // Create client with locales using type assertion
694
+ const client = new ExtensionServerClient({
695
+ ...defaultOptions,
696
+ locales: {user: 'ja', shop: 'fr'} as unknown as DeepPartial<any>,
697
+ })
698
+
699
+ // Mock connection
700
+ mockSocketServer.connect(mockSocket)
701
+
702
+ // Create spy
703
+ const updateSpy = vi.fn()
704
+ client.on('update', updateSpy)
705
+
706
+ // Define localization data
707
+ const localization: Localization = {
708
+ defaultLocale: 'en',
709
+ translations: {
710
+ ja: {
711
+ welcome: 'いらっしゃいませ!',
712
+ description: '拡張子の説明',
713
+ },
714
+ en: {
715
+ welcome: 'Welcome!',
716
+ description: 'Extension description',
717
+ },
718
+ fr: {
719
+ welcome: 'Bienvenue!',
720
+ description: "Description de l'extension",
721
+ },
722
+ },
723
+ lastUpdated: 1684164163736,
724
+ }
725
+
726
+ // Create mock data - using type assertion to avoid DeepPartial errors
727
+ interface MockExtension {
728
+ uuid: string
729
+ type: string
730
+ name: string
731
+ description?: string
732
+ localization?: any
733
+ extensionPoints?: {
734
+ localization?: any
735
+ target?: string
736
+ surface?: string
737
+ name?: string
738
+ }[]
739
+ }
740
+
741
+ // Using a typed array to avoid DeepPartial issues
742
+ const mockExtensions: MockExtension[] = [
743
+ {
744
+ uuid: '123',
745
+ type: 'ui_extension',
746
+ name: 't:welcome',
747
+ description: 't:description',
748
+ localization,
749
+ extensionPoints: [{localization, target: 'admin.test', surface: 'admin'}],
750
+ },
751
+ {
752
+ uuid: '456',
753
+ type: 'ui_extension',
754
+ name: 'Extension 456',
755
+ description: 'This is a test extension',
756
+ localization: null,
757
+ extensionPoints: [{localization: null, target: 'admin.test', surface: 'admin'}],
758
+ },
759
+ {uuid: '789', type: 'product_subscription', name: 'Extension 789'},
760
+ ]
761
+
762
+ // Send the update event
763
+ mockSocketServer.send({
764
+ event: 'update',
765
+ data: {
766
+ app: mockApp(),
767
+ extensions: mockExtensions,
768
+ store: 'test-store',
769
+ },
770
+ })
771
+
772
+ // Verify event was handled
773
+ expect(updateSpy).toHaveBeenCalledTimes(1)
774
+
775
+ // Assert on specific properties without using complex matchers that trigger type errors
776
+ const updatedData = updateSpy.mock.calls[0][0]
777
+ expect(updatedData).toBeDefined()
778
+ expect(updatedData.extensions).toHaveLength(3)
779
+
780
+ // Find the translated extension
781
+ const translatedExt = updatedData.extensions.find((ext: any) => ext.uuid === '123')
782
+ expect(translatedExt).toBeDefined()
783
+
784
+ // Check translation worked properly - name should be translated
785
+ expect(translatedExt.name).not.toBe('t:welcome')
786
+ // Description should be translated
787
+ expect(translatedExt.description).not.toBe('t:description')
788
+ })
789
+
790
+ test('sends data with translated props when locales option is provided on subsequent "update" events', async () => {
791
+ // Create client with locales using type assertion
792
+ const client = new ExtensionServerClient({
793
+ ...defaultOptions,
794
+ locales: {user: 'ja', shop: 'fr'} as unknown as DeepPartial<any>,
795
+ })
796
+
797
+ // Mock connection
798
+ mockSocketServer.connect(mockSocket)
799
+
800
+ // Create spy
801
+ const updateSpy = vi.fn()
802
+ client.on('update', updateSpy)
803
+
804
+ // Define localization data
805
+ const localization: Localization = {
806
+ defaultLocale: 'en',
807
+ translations: {
808
+ ja: {
809
+ welcome: 'いらっしゃいませ!',
810
+ description: '拡張子の説明',
811
+ },
812
+ en: {
813
+ welcome: 'Welcome!',
814
+ description: 'Extension description',
815
+ },
816
+ fr: {
817
+ welcome: 'Bienvenue!',
818
+ description: "Description de l'extension",
819
+ },
820
+ },
821
+ lastUpdated: 1684164163736,
822
+ }
823
+
824
+ // Create mock data - using type assertion to avoid DeepPartial errors
825
+ interface MockExtension {
826
+ uuid: string
827
+ type: string
828
+ name: string
829
+ description?: string
830
+ localization?: any
831
+ extensionPoints?: {
832
+ localization?: any
833
+ target?: string
834
+ surface?: string
835
+ name?: string
836
+ }[]
837
+ }
838
+
839
+ // Using a typed array to avoid DeepPartial issues
840
+ const mockExtensions: MockExtension[] = [
841
+ {
842
+ uuid: '123',
843
+ type: 'ui_extension',
844
+ name: 't:welcome',
845
+ description: 't:description',
846
+ localization,
847
+ extensionPoints: [{localization, target: 'admin.test', surface: 'admin'}],
848
+ },
849
+ {
850
+ uuid: '456',
851
+ type: 'ui_extension',
852
+ name: 'Extension 456',
853
+ description: 'This is a test extension',
854
+ localization: null,
855
+ extensionPoints: [{localization: null, target: 'admin.test', surface: 'admin'}],
856
+ },
857
+ {uuid: '789', type: 'product_subscription', name: 'Extension 789'},
858
+ ]
859
+
860
+ // Send the update event twice
861
+ mockSocketServer.send({
862
+ event: 'update',
863
+ data: {
864
+ app: mockApp(),
865
+ extensions: mockExtensions,
866
+ store: 'test-store',
867
+ },
868
+ })
869
+
870
+ mockSocketServer.send({
871
+ event: 'update',
872
+ data: {
873
+ app: mockApp(),
874
+ extensions: mockExtensions,
875
+ store: 'test-store',
876
+ },
877
+ })
878
+
879
+ // Verify event was handled twice
880
+ expect(updateSpy).toHaveBeenCalledTimes(2)
881
+
882
+ // Check the second call - make sure translations still work
883
+ const secondCallData = updateSpy.mock.calls[1][0]
884
+ expect(secondCallData).toBeDefined()
885
+ expect(secondCallData.extensions).toHaveLength(3)
886
+
887
+ // Find the translated extension
888
+ const translatedExt = secondCallData.extensions.find((ext: any) => ext.uuid === '123')
889
+ expect(translatedExt).toBeDefined()
890
+
891
+ // Check translation worked properly - name should be translated
892
+ expect(translatedExt.name).not.toBe('t:welcome')
893
+ // Description should be translated
894
+ expect(translatedExt.description).not.toBe('t:description')
895
+ })
896
+
897
+ test('handles localized extension props correctly', async () => {
898
+ // Create mock extension with translations
899
+ interface MockExtension {
900
+ uuid: string
901
+ type: string
902
+ name: string
903
+ description?: string
904
+ localization?: any
905
+ extensionPoints?: {
906
+ localization?: any
907
+ target?: string
908
+ surface?: string
909
+ name?: string
910
+ }[]
911
+ }
912
+
913
+ const mockExtension: MockExtension = {
914
+ uuid: '123',
915
+ type: 'ui_extension',
916
+ name: 't:extension.name',
917
+ description: 't:extension.description',
918
+ localization: {
919
+ translations: JSON.stringify({
920
+ 'extension.name': 'Extension Name',
921
+ 'extension.description': 'Extension Description',
922
+ }),
923
+ default: 'en',
924
+ },
925
+ extensionPoints: [
926
+ {
927
+ target: 'admin.product.item.action',
928
+ name: 't:extension.point.name',
929
+ },
930
+ ],
931
+ }
932
+
933
+ // Create client with locales option
934
+ const client = new ExtensionServerClient({
935
+ connection: {
936
+ url: TEST_CONNECTION_URL,
937
+ automaticConnect: true,
938
+ },
939
+ locales: {user: 'en', shop: 'en'} as unknown as DeepPartial<any>,
940
+ })
941
+
942
+ // Mock connection
943
+ mockSocketServer.connect(mockSocket)
944
+
945
+ // Create spy for translating extension
946
+ const translateSpy = vi.spyOn(client as any, '_getLocalizedValue')
947
+ translateSpy.mockImplementation((translations, key) => {
948
+ if (key === 't:extension.name') return 'Extension Name'
949
+ if (key === 't:extension.description') return 'Extension Description'
950
+ return key
951
+ })
952
+
953
+ // Create spy for the connected event
954
+ const connectSpy = vi.fn()
955
+ client.on('connected', connectSpy)
956
+
957
+ // Send connected event with mock data
958
+ const data = {
959
+ app: mockApp(),
960
+ extensions: [mockExtension],
961
+ }
962
+
963
+ // Send the event
964
+ mockSocketServer.send({event: 'connected', data})
965
+
966
+ // Verify translated props
967
+ expect(connectSpy).toHaveBeenCalledTimes(1)
968
+ const callData = connectSpy.mock.calls[0][0]
969
+ const extension = callData.extensions[0]
970
+
971
+ // Check that the translation worked
972
+ expect(extension.name).toBe('Extension Name')
973
+ expect(extension.description).toBe('Extension Description')
974
+
975
+ // Clean up
976
+ translateSpy.mockRestore()
977
+ })
978
+ })
979
+
980
+ describe('emit()', () => {
981
+ test('emits an event', async () => {
982
+ // Create client
983
+ const client = new ExtensionServerClient(defaultOptions)
984
+
985
+ // Mock connection
986
+ mockSocketServer.connect(mockSocket)
987
+
988
+ // Emit an event
989
+ client.emit('unfocus')
990
+
991
+ // Verify the correct message was sent
992
+ expect(mockSocketServer.messages.length).toBe(1)
993
+
994
+ // Parse the JSON if it's a string
995
+ const message =
996
+ typeof mockSocketServer.messages[0] === 'string'
997
+ ? JSON.parse(mockSocketServer.messages[0])
998
+ : mockSocketServer.messages[0]
999
+
1000
+ expect(message).toMatchObject({
1001
+ event: 'dispatch',
1002
+ data: {type: 'unfocus'},
1003
+ })
1004
+ })
1005
+
1006
+ test('warns if trying to "emit" a persist event', async () => {
1007
+ // Spy on console.warn
1008
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
1009
+
1010
+ // Create client
1011
+ const client = new ExtensionServerClient(defaultOptions)
1012
+
1013
+ // Mock connection
1014
+ mockSocketServer.connect(mockSocket)
1015
+
1016
+ // Try to emit an invalid event type
1017
+ client.emit('update' as any)
1018
+
1019
+ // Verify warning was shown
1020
+ expect(warnSpy).toHaveBeenCalled()
1021
+
1022
+ // Restore console.warn
1023
+ warnSpy.mockRestore()
1024
+ })
1025
+ })
1026
+
1027
+ describe('persist()', () => {
1028
+ test('persists a mutation', async () => {
1029
+ // Create client
1030
+ const client = new ExtensionServerClient(defaultOptions)
1031
+
1032
+ // Mock connection
1033
+ mockSocketServer.connect(mockSocket)
1034
+
1035
+ // Persist data
1036
+ const extensionData = {extensions: [{uuid: '123'}]} as any
1037
+ client.persist('update', extensionData)
1038
+
1039
+ // Verify the correct message was sent
1040
+ expect(mockSocketServer.messages.length).toBe(1)
1041
+
1042
+ // Parse the JSON if it's a string
1043
+ const message =
1044
+ typeof mockSocketServer.messages[0] === 'string'
1045
+ ? JSON.parse(mockSocketServer.messages[0])
1046
+ : mockSocketServer.messages[0]
1047
+
1048
+ expect(message).toMatchObject({
1049
+ event: 'update',
1050
+ data: extensionData,
1051
+ })
1052
+ })
1053
+
1054
+ test('warns if trying to "persist" a dispatch event', async () => {
1055
+ // Spy on console.warn
1056
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
1057
+
1058
+ // Create client
1059
+ const client = new ExtensionServerClient(defaultOptions)
1060
+
1061
+ // Mock connection
1062
+ mockSocketServer.connect(mockSocket)
1063
+
1064
+ // Try to persist an invalid event type
1065
+ client.persist('unfocus' as any, {})
1066
+
1067
+ // Verify warning was shown
1068
+ expect(warnSpy).toHaveBeenCalled()
1069
+
1070
+ // Restore console.warn
1071
+ warnSpy.mockRestore()
1072
+ })
1073
+
1074
+ test('remove translated props from the UI extensions payload when locales are provided in the client options', async () => {
1075
+ // Create client with locales using type assertion
1076
+ const client = new ExtensionServerClient({
1077
+ ...defaultOptions,
1078
+ locales: {user: 'ja', shop: 'fr'} as unknown as DeepPartial<any>,
1079
+ })
1080
+
1081
+ // Mock connection
1082
+ mockSocketServer.connect(mockSocket)
1083
+
1084
+ // Create mock data with translated fields
1085
+ const extensionData = {
1086
+ extensions: [
1087
+ {
1088
+ uuid: '123',
1089
+ type: 'ui_extension',
1090
+ name: 'いらっしゃいませ!',
1091
+ description: '拡張子の説明',
1092
+ localization: {},
1093
+ extensionPoints: [{localization: {}, name: 'いらっしゃいませ!', description: '拡張子の説明'}],
1094
+ },
1095
+ ],
1096
+ } as any
1097
+
1098
+ // Persist the data
1099
+ client.persist('update', extensionData)
1100
+
1101
+ // Verify the message was sent and normalized (translation fields removed)
1102
+ expect(mockSocketServer.messages.length).toBe(1)
1103
+
1104
+ // Parse the JSON if it's a string
1105
+ const message =
1106
+ typeof mockSocketServer.messages[0] === 'string'
1107
+ ? JSON.parse(mockSocketServer.messages[0])
1108
+ : mockSocketServer.messages[0]
1109
+
1110
+ // Check translation fields were removed
1111
+ expect(message).toMatchObject({
1112
+ event: 'update',
1113
+ data: {
1114
+ extensions: [{uuid: '123', type: 'ui_extension', extensionPoints: [{}]}],
1115
+ },
1116
+ })
1117
+ })
1118
+
1119
+ test('leave translatable props as-is in the UI extensions payload when locales are not provided in the client options', async () => {
1120
+ // Create client without locales
1121
+ const client = new ExtensionServerClient(defaultOptions)
1122
+
1123
+ // Mock connection
1124
+ mockSocketServer.connect(mockSocket)
1125
+
1126
+ // Create data with localization fields
1127
+ const extensionData = {
1128
+ extensions: [{uuid: '123', type: 'ui_extension', localization: {}, extensionPoints: [{localization: {}}]}],
1129
+ } as any
1130
+
1131
+ // Persist the data
1132
+ client.persist('update', extensionData)
1133
+
1134
+ // Verify the message was sent without changes
1135
+ expect(mockSocketServer.messages.length).toBe(1)
1136
+
1137
+ // Parse the JSON if it's a string
1138
+ const message =
1139
+ typeof mockSocketServer.messages[0] === 'string'
1140
+ ? JSON.parse(mockSocketServer.messages[0])
1141
+ : mockSocketServer.messages[0]
1142
+
1143
+ // Check fields were preserved
1144
+ expect(message).toMatchObject({
1145
+ event: 'update',
1146
+ data: extensionData,
1147
+ })
1148
+ })
1149
+ })
1150
+
1151
+ describe('connect()', () => {
1152
+ test('updates the client options', () => {
1153
+ // Create client without initial options
1154
+ const client = new ExtensionServerClient()
1155
+
1156
+ // Then connect with options
1157
+ client.connect({connection: {automaticConnect: false}})
1158
+
1159
+ // Verify options were updated
1160
+ expect(client.options.connection).toMatchObject({
1161
+ automaticConnect: false,
1162
+ })
1163
+ })
1164
+
1165
+ test('does not attempt to connect if the URL is undefined', () => {
1166
+ // Create client
1167
+ const client = new ExtensionServerClient()
1168
+
1169
+ // Try to connect without URL
1170
+ client.connect()
1171
+
1172
+ // Verify no connection was made
1173
+ expect(client.connection).toBeUndefined()
1174
+ })
1175
+
1176
+ test('does not attempt to connect if the URL is empty', () => {
1177
+ // Create client with empty URL
1178
+ const client = new ExtensionServerClient({connection: {url: ''}})
1179
+
1180
+ // Try to connect
1181
+ client.connect()
1182
+
1183
+ // Verify no connection was made
1184
+ expect(client.connection).toBeUndefined()
1185
+ })
1186
+
1187
+ test('re-use existing connection if connect options have not changed', async () => {
1188
+ // Create client
1189
+ const client = new ExtensionServerClient(defaultOptions)
1190
+
1191
+ // Mock connection
1192
+ mockSocketServer.connect(mockSocket)
1193
+
1194
+ // Get initial connection
1195
+ const initialConnection = client.connection
1196
+ expect(initialConnection).toBeDefined()
1197
+
1198
+ // Try to connect with the same URL
1199
+ client.connect({connection: {url: TEST_CONNECTION_URL}})
1200
+
1201
+ // Connection should be the same object (reused)
1202
+ expect(client.connection).toBe(initialConnection)
1203
+
1204
+ // Only one connection should exist
1205
+ expect(mockSocketServer.clients.length).toBe(1)
1206
+ })
1207
+
1208
+ test('creates a new connection if the URL has changed', async () => {
1209
+ // Create client
1210
+ const client = new ExtensionServerClient(defaultOptions)
1211
+
1212
+ // Connect the mock socket
1213
+ mockSocketServer.connect(mockSocket)
1214
+
1215
+ // Store the original connection
1216
+ const originalConnection = client.connection
1217
+
1218
+ // Create a WebSocket spy to track new instances
1219
+ const webSocketSpy = vi.spyOn(globalThis, 'WebSocket')
1220
+ const initialCallCount = webSocketSpy.mock.calls.length
1221
+
1222
+ // Change the URL
1223
+ const newUrl = 'ws://new-host.com:9000/extensions/'
1224
+ client.connect({
1225
+ connection: {
1226
+ url: newUrl,
1227
+ },
1228
+ })
1229
+
1230
+ // Verify a new WebSocket was created
1231
+ expect(webSocketSpy).toHaveBeenCalledTimes(initialCallCount + 1)
1232
+ expect(webSocketSpy).toHaveBeenLastCalledWith(newUrl, [])
1233
+
1234
+ // Verify connection is different
1235
+ expect(client.connection).not.toBe(originalConnection)
1236
+ })
1237
+ })
1238
+ })