@getpara/graz-connector 2.0.0-dev.11 → 2.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.
@@ -1,491 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { ParaGrazConnector, ParaGrazConfig, ParaOfflineSigner } from '../src/connector';
3
- import type { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
4
- import ParaWeb from '@getpara/react-sdk';
5
- import { renderModal } from '../src/connectorModal.js';
6
-
7
- vi.mock('styled-components', () => ({
8
- default: {
9
- div: () => 'div',
10
- button: () => 'button',
11
- a: () => 'a',
12
- span: () => 'span',
13
- input: () => 'input',
14
- form: () => 'form',
15
- },
16
- createGlobalStyle: vi.fn(),
17
- css: vi.fn(),
18
- keyframes: vi.fn(),
19
- ThemeProvider: vi.fn(),
20
- }));
21
-
22
- const mockParaWebInstance = {
23
- isFullyLoggedIn: vi.fn().mockResolvedValue(false),
24
- getWalletsByType: vi.fn().mockReturnValue([]),
25
- logout: vi.fn().mockResolvedValue(undefined),
26
- };
27
-
28
- vi.mock('@getpara/react-sdk', () => ({
29
- __esModule: true,
30
- default: vi.fn(() => mockParaWebInstance),
31
- Environment: { BETA: 'BETA', DEVELOPMENT: 'DEV', PRODUCTION: 'PROD' },
32
- ParaModal: vi.fn(props => ({ type: 'div', props })),
33
- }));
34
-
35
- import type { AccountData, AminoSignResponse, StdSignDoc } from '@keplr-wallet/types';
36
- import type { DirectSignResponse } from '@cosmjs/proto-signing';
37
- import Long from 'long';
38
-
39
- function createProtoSignerMock() {
40
- const directRes: DirectSignResponse = {
41
- signed: {
42
- bodyBytes: new Uint8Array(),
43
- authInfoBytes: new Uint8Array(),
44
- chainId: 'chain-1',
45
- accountNumber: Long.fromString('0'),
46
- },
47
- signature: {
48
- pub_key: { type: 'tendermint/PubKeySecp256k1', value: 'pk' },
49
- signature: 'proto-sig',
50
- },
51
- };
52
- return {
53
- getAccounts: vi.fn().mockResolvedValue([account as unknown as AccountData]),
54
- signDirect: vi.fn().mockResolvedValue(directRes),
55
- };
56
- }
57
-
58
- function createAminoSignerMock() {
59
- const aminoRes: AminoSignResponse = {
60
- signed: {
61
- chain_id: 'chain-1',
62
- account_number: '0',
63
- sequence: '0',
64
- fee: { amount: [], gas: '0' },
65
- msgs: [],
66
- memo: '',
67
- },
68
- signature: {
69
- pub_key: { type: 'tendermint/PubKeySecp256k1', value: 'pk' },
70
- signature: 'amino-sig',
71
- },
72
- };
73
- return {
74
- getAccounts: vi.fn().mockResolvedValue([account as unknown as AccountData]),
75
- signAmino: vi.fn().mockResolvedValue(aminoRes),
76
- };
77
- }
78
-
79
- vi.mock('@getpara/cosmjs-v0-integration', () => ({
80
- __esModule: true,
81
- ParaAminoSigner: vi.fn(() => createAminoSignerMock()),
82
- ParaProtoSigner: vi.fn(() => createProtoSignerMock()),
83
- }));
84
-
85
- vi.mock('long', () => ({ __esModule: true, default: { fromString: vi.fn().mockReturnValue({ toString: () => '1' }) } }));
86
- vi.mock('@cosmjs/encoding', () => ({
87
- __esModule: true,
88
- fromBech32: vi.fn().mockReturnValue({ data: new Uint8Array([1, 2, 3]) }),
89
- }));
90
- vi.mock('../src/connectorModal.js', () => ({ __esModule: true, renderModal: vi.fn() }));
91
-
92
- const account: AccountData = { address: 'test1qqq', algo: 'secp256k1', pubkey: new Uint8Array([9]) };
93
- const wallet = { id: 'wallet-1', type: 'COSMOS' } as const;
94
- const chains = [{ chainId: 'chain-1', bech32Config: { bech32PrefixAccAddr: 'pref' } }] as any;
95
-
96
- function cfg(p: Partial<ParaGrazConfig> = {}): ParaGrazConfig {
97
- return { paraWeb: mockParaWebInstance as any, ...p };
98
- }
99
- function newConnector(conf: Partial<ParaGrazConfig> = {}, cs = chains) {
100
- return new ParaGrazConnector(cfg(conf), cs);
101
- }
102
-
103
- beforeEach(() => {
104
- vi.clearAllMocks();
105
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
106
- mockParaWebInstance.getWalletsByType.mockReturnValue([]);
107
- });
108
-
109
- afterEach(() => {
110
- vi.useRealTimers();
111
- });
112
-
113
- describe('ParaGrazConnector', () => {
114
- describe('constructor', () => {
115
- it('constructs with valid config', () => {
116
- expect(() => newConnector()).not.toThrow();
117
- });
118
-
119
- it('constructor validation errors', () => {
120
- expect(() => newConnector({ paraWeb: undefined as any })).toThrow('Para connector: missing paraWeb instance in config');
121
- });
122
- });
123
-
124
- describe('getBech32Prefix', () => {
125
- it('returns correct prefix for known chain', () => {
126
- expect(newConnector().getBech32Prefix('chain-1')).toBe('pref');
127
- });
128
- it('returns default prefix for unknown chain', () => {
129
- expect(newConnector().getBech32Prefix('x')).toBe('cosmos');
130
- });
131
- it('returns default prefix when chains is null', () => {
132
- expect(new ParaGrazConnector(cfg(), null).getBech32Prefix('chain-1')).toBe('cosmos');
133
- });
134
- });
135
-
136
- describe('private methods', () => {
137
- describe('waitForLogin', () => {
138
- it('returns immediately when already logged in', async () => {
139
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValueOnce(true);
140
- await newConnector()['waitForLogin']();
141
- });
142
-
143
- it('throws error when modal is closed', async () => {
144
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
145
- const c = newConnector();
146
- c['isModalClosed'] = true;
147
- await expect(c['waitForLogin']()).rejects.toThrow('User closed modal');
148
- });
149
-
150
- it('throws error on timeout', async () => {
151
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
152
- const c = newConnector();
153
- c['isModalClosed'] = false;
154
- vi.spyOn(global, 'setTimeout').mockImplementation((cb: any) => {
155
- cb();
156
- return 0 as any;
157
- });
158
- await expect(c['waitForLogin'](1)).rejects.toThrow('Timed out waiting for user to log in');
159
- });
160
- });
161
-
162
- describe('waitForAccounts', () => {
163
- it('returns immediately when accounts are available', async () => {
164
- mockParaWebInstance.getWalletsByType.mockReturnValueOnce([wallet]);
165
- const res = await newConnector()['waitForAccounts']();
166
- expect(res).toEqual([wallet]);
167
- });
168
-
169
- it('throws error on timeout', async () => {
170
- mockParaWebInstance.getWalletsByType.mockReturnValue([]);
171
- const c = newConnector();
172
- vi.spyOn(global, 'setTimeout').mockImplementation((cb: any) => {
173
- cb();
174
- return 0 as any;
175
- });
176
- await expect(c['waitForAccounts'](1)).rejects.toThrow('Timed out waiting for accounts to load');
177
- });
178
- });
179
-
180
- describe('hasCosmosWallet', () => {
181
- it('returns false when not logged in', async () => {
182
- expect(await newConnector()['hasCosmosWallet']()).toBe(false);
183
- });
184
- it('returns false when logged in but no COSMOS wallets', async () => {
185
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
186
- mockParaWebInstance.getWalletsByType.mockReturnValue([]);
187
- expect(await newConnector()['hasCosmosWallet']()).toBe(false);
188
- });
189
- it('returns true when logged in with COSMOS wallets', async () => {
190
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
191
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
192
- expect(await newConnector()['hasCosmosWallet']()).toBe(true);
193
- });
194
- });
195
-
196
- describe('ensureConnected', () => {
197
- it('throws when not connected', async () => {
198
- await expect(newConnector()['ensureConnected']()).rejects.toThrow();
199
- });
200
- it('does not throw when connected', async () => {
201
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
202
- await newConnector()['ensureConnected']();
203
- });
204
- });
205
- });
206
-
207
- describe('enable', () => {
208
- it('early exit when wallet already present', async () => {
209
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
210
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
211
- await newConnector().enable('chain-1');
212
- expect(renderModal).not.toHaveBeenCalled();
213
- });
214
-
215
- it('opens modal then resolves on login + wallets', async () => {
216
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValueOnce(false).mockResolvedValue(true);
217
- mockParaWebInstance.getWalletsByType.mockReturnValueOnce([]).mockReturnValue([wallet]);
218
- vi.spyOn(global, 'setTimeout').mockImplementation((cb: any) => {
219
- cb();
220
- return 0 as any;
221
- });
222
- await newConnector().enable('chain-1');
223
- expect(renderModal).toHaveBeenCalled();
224
- });
225
-
226
- it('handles array of chain IDs', async () => {
227
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
228
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
229
- await newConnector().enable(['chain-1', 'chain-2']);
230
- });
231
-
232
- it('handles login failure and cleans up', async () => {
233
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
234
- const c = newConnector();
235
- vi.spyOn(c as any, 'waitForLogin').mockRejectedValueOnce(new Error('Login error'));
236
- await expect(c.enable('chain-1')).rejects.toThrow('Login error');
237
- expect(c['isModalClosed']).toBe(true);
238
- });
239
-
240
- it('onClose callback sets isModalClosed', async () => {
241
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
242
- const c = newConnector();
243
- vi.spyOn(c as any, 'waitForLogin').mockResolvedValue(undefined);
244
- vi.spyOn(c as any, 'waitForAccounts').mockResolvedValue([wallet]);
245
-
246
- await c.enable('chain-1');
247
- const onClose = (renderModal as any).mock.calls[0][2] as () => void;
248
- c['isModalClosed'] = false;
249
- onClose();
250
- expect(c['isModalClosed']).toBe(true);
251
- });
252
- });
253
-
254
- describe('public methods', () => {
255
- describe('getFirstWallet', () => {
256
- it('returns first wallet from waitForAccounts', async () => {
257
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
258
- expect(await newConnector().getFirstWallet()).toBe(wallet);
259
- });
260
- });
261
-
262
- describe('getKey', () => {
263
- it('returns key for wallet', async () => {
264
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
265
- const res = await newConnector().getKey('chain-1');
266
- expect(res.bech32Address).toBe('test1qqq');
267
- });
268
- it('throws error when no account', async () => {
269
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
270
- const ParaProtoSigner = (await import('@getpara/cosmjs-v0-integration')).ParaProtoSigner as any;
271
- ParaProtoSigner.mockImplementationOnce(() => ({ getAccounts: vi.fn().mockResolvedValue([]) }));
272
- await expect(newConnector().getKey('chain-1')).rejects.toThrow('has no accounts');
273
- });
274
- });
275
-
276
- describe('getParaWebClient and getConfig', () => {
277
- it('getParaWebClient returns client', () => {
278
- expect(newConnector().getParaWebClient()).toBe(mockParaWebInstance);
279
- });
280
- it('getConfig returns config', () => {
281
- const cfgObj = cfg();
282
- expect(new ParaGrazConnector(cfgObj).getConfig()).toBe(cfgObj);
283
- });
284
- });
285
-
286
- describe('getOfflineSigner methods', () => {
287
- it('getOfflineSignerOnlyAmino returns signer & can sign', async () => {
288
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
289
- const aminoSigner: any = newConnector().getOfflineSignerOnlyAmino('chain-1');
290
-
291
- await expect(
292
- aminoSigner.signAmino('test1qqq', {
293
- chain_id: 'chain-1',
294
- account_number: '0',
295
- sequence: '0',
296
- fee: { amount: [], gas: '0' },
297
- msgs: [],
298
- memo: '',
299
- } as StdSignDoc),
300
- ).resolves.toBeDefined();
301
- });
302
-
303
- it('getOfflineSignerOnlyAmino throws when no wallets available', async () => {
304
- mockParaWebInstance.getWalletsByType.mockReturnValue([]);
305
- const connector = newConnector();
306
- expect(() => connector.getOfflineSignerOnlyAmino('chain-1')).toThrow('No wallets found');
307
- });
308
-
309
- it('getOfflineSigner returns combined signer and can sign directly', async () => {
310
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
311
- const signer = newConnector().getOfflineSigner('chain-1');
312
-
313
- await expect(
314
- signer.signDirect('test1qqq', {
315
- bodyBytes: new Uint8Array(),
316
- authInfoBytes: new Uint8Array(),
317
- chainId: 'chain-1',
318
- accountNumber: Long.fromString('1'),
319
- } as SignDoc),
320
- ).resolves.toBeDefined();
321
- });
322
-
323
- it('getOfflineSignerAuto resolves to amino signer', async () => {
324
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
325
- const signer = await newConnector().getOfflineSignerAuto('chain-1');
326
- expect(signer.getAccounts).toBeDefined();
327
- });
328
- });
329
-
330
- describe('signing methods', () => {
331
- beforeEach(() => {
332
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(true);
333
- mockParaWebInstance.getWalletsByType.mockReturnValue([wallet]);
334
- });
335
-
336
- it('signAmino success path', async () => {
337
- await expect(
338
- newConnector().signAmino('chain-1', 'test1qqq', {
339
- chain_id: 'chain-1',
340
- account_number: '1',
341
- sequence: '1',
342
- fee: { amount: [], gas: '0' },
343
- msgs: [],
344
- memo: '',
345
- }),
346
- ).resolves.toBeDefined();
347
- });
348
-
349
- it('signDirect success path', async () => {
350
- await expect(
351
- newConnector().signDirect('chain-1', 'test1qqq', {
352
- bodyBytes: new Uint8Array([1]),
353
- authInfoBytes: new Uint8Array([1]),
354
- chainId: 'chain-1',
355
- accountNumber: BigInt(1),
356
- } as any),
357
- ).resolves.toBeDefined();
358
- });
359
-
360
- it('signDirect handles null fields', async () => {
361
- await expect(
362
- newConnector().signDirect('chain-1', 'test1qqq', {
363
- bodyBytes: null,
364
- authInfoBytes: null,
365
- chainId: null,
366
- accountNumber: null,
367
- } as any),
368
- ).resolves.toBeDefined();
369
- });
370
-
371
- it('signArbitrary with string data', async () => {
372
- await expect(newConnector().signArbitrary('chain-1', 'test1qqq', 'data')).resolves.toBeDefined();
373
- });
374
- it('signArbitrary with Uint8Array data', async () => {
375
- await expect(newConnector().signArbitrary('chain-1', 'test1qqq', new Uint8Array([1, 2]))).resolves.toBeDefined();
376
- });
377
-
378
- it('signing methods throw when wallet not connected', async () => {
379
- mockParaWebInstance.isFullyLoggedIn.mockResolvedValue(false);
380
- const c = newConnector();
381
- await expect(c.signAmino('chain-1', 'a', {} as any)).rejects.toThrow();
382
- await expect(c.signDirect('chain-1', 'a', {} as any)).rejects.toThrow();
383
- await expect(c.signArbitrary('chain-1', 'a', 'x')).rejects.toThrow();
384
- });
385
- });
386
-
387
- it('disconnect calls logout', async () => {
388
- await newConnector().disconnect();
389
- expect(mockParaWebInstance.logout).toHaveBeenCalled();
390
- });
391
- });
392
- });
393
-
394
- describe('ParaOfflineSigner', () => {
395
- const mockChainId = 'chain-1';
396
- let mockConnector: any;
397
- let offlineSigner: ParaOfflineSigner;
398
-
399
- beforeEach(() => {
400
- mockConnector = {
401
- getParaWebClient: vi.fn().mockReturnValue(mockParaWebInstance),
402
- getBech32Prefix: vi.fn().mockReturnValue('cosmos'),
403
- getFirstWallet: vi.fn().mockResolvedValue(wallet),
404
- getKey: vi.fn().mockResolvedValue({
405
- bech32Address: 'test1qqq',
406
- algo: 'secp256k1',
407
- pubKey: new Uint8Array([1, 2, 3]),
408
- }),
409
- };
410
- offlineSigner = new ParaOfflineSigner(mockChainId, mockConnector);
411
- });
412
-
413
- describe('private methods', () => {
414
- it('para getter returns client', () => {
415
- expect(offlineSigner['para']).toBe(mockParaWebInstance);
416
- expect(mockConnector.getParaWebClient).toHaveBeenCalled();
417
- });
418
-
419
- it('prefix getter returns bech32 prefix', () => {
420
- expect(offlineSigner['prefix']).toBe('cosmos');
421
- expect(mockConnector.getBech32Prefix).toHaveBeenCalledWith(mockChainId);
422
- });
423
-
424
- it('wallet method returns first wallet', async () => {
425
- const result = await offlineSigner['wallet']();
426
- expect(result).toBe(wallet);
427
- expect(mockConnector.getFirstWallet).toHaveBeenCalled();
428
- });
429
- });
430
-
431
- describe('getAccounts', () => {
432
- it('returns accounts', async () => {
433
- expect(await offlineSigner.getAccounts()).toEqual([
434
- {
435
- address: 'test1qqq',
436
- algo: 'secp256k1',
437
- pubkey: new Uint8Array([1, 2, 3]),
438
- },
439
- ]);
440
- });
441
- });
442
-
443
- describe('signDirect validation', () => {
444
- it('throws when chain ID does not match', async () => {
445
- await expect(
446
- offlineSigner.signDirect('test1qqq', {
447
- bodyBytes: new Uint8Array(),
448
- authInfoBytes: new Uint8Array(),
449
- chainId: 'wrong',
450
- accountNumber: Long.fromString('1'),
451
- } as SignDoc),
452
- ).rejects.toThrow('Chain ID does not match');
453
- });
454
-
455
- it('throws when signer address does not match', async () => {
456
- vi.spyOn(offlineSigner, 'getAccounts').mockResolvedValueOnce([
457
- { address: 'test1qqq', algo: 'secp256k1', pubkey: new Uint8Array([1, 2, 3]) },
458
- ] as any);
459
- await expect(
460
- offlineSigner.signDirect('other', {
461
- bodyBytes: new Uint8Array(),
462
- authInfoBytes: new Uint8Array(),
463
- chainId: mockChainId,
464
- accountNumber: Long.fromString('1'),
465
- } as SignDoc),
466
- ).rejects.toThrow('Signer address does not match');
467
- });
468
-
469
- it('signDirect success path', async () => {
470
- await expect(
471
- offlineSigner.signDirect('test1qqq', {
472
- bodyBytes: new Uint8Array(),
473
- authInfoBytes: new Uint8Array(),
474
- chainId: mockChainId,
475
- accountNumber: Long.fromString('1'),
476
- } as SignDoc),
477
- ).resolves.toBeDefined();
478
- });
479
-
480
- it('signDirect handles string accountNumber', async () => {
481
- await expect(
482
- offlineSigner.signDirect('test1qqq', {
483
- bodyBytes: new Uint8Array(),
484
- authInfoBytes: new Uint8Array(),
485
- chainId: mockChainId,
486
- accountNumber: '42' as any,
487
- }),
488
- ).resolves.toBeDefined();
489
- });
490
- });
491
- });
@@ -1,148 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
2
- import React from 'react';
3
- import type ParaWeb from '@getpara/react-sdk';
4
-
5
- const mockRootRender = vi.fn();
6
- const mockRootUnmount = vi.fn();
7
- const mockRoot = { render: mockRootRender, unmount: mockRootUnmount };
8
-
9
- const mockCreateRoot = vi.fn().mockReturnValue(mockRoot);
10
- const mockLegacyRender = vi.fn();
11
- const mockLegacyUnmount = vi.fn();
12
-
13
- vi.mock('react-dom/client', () => ({
14
- __esModule: true,
15
- default: { createRoot: mockCreateRoot },
16
- createRoot: mockCreateRoot,
17
- }));
18
-
19
- vi.mock('react-dom', () => ({
20
- __esModule: true,
21
- default: { render: mockLegacyRender, unmountComponentAtNode: mockLegacyUnmount },
22
- render: mockLegacyRender,
23
- unmountComponentAtNode: mockLegacyUnmount,
24
- }));
25
-
26
- vi.mock('@getpara/react-sdk', () => ({
27
- __esModule: true,
28
- default: vi.fn(),
29
- ParaModal: vi.fn(props => React.createElement('div', props)),
30
- }));
31
-
32
- const originalCreateElement = document.createElement.bind(document);
33
- let mockContainer!: HTMLDivElement;
34
-
35
- function stubDom() {
36
- mockContainer = originalCreateElement('div');
37
- mockContainer.remove = vi.fn();
38
-
39
- document.createElement = vi.fn().mockReturnValue(mockContainer) as any;
40
- document.getElementById = vi.fn().mockReturnValue(null) as any;
41
- document.body.appendChild = vi.fn();
42
- }
43
-
44
- function restoreDom() {
45
- document.createElement = originalCreateElement;
46
- }
47
-
48
- import { renderModal as renderModalR18 } from '../src/connectorModal';
49
-
50
- beforeEach(() => {
51
- document.body.innerHTML = '';
52
- vi.clearAllMocks();
53
- stubDom();
54
- });
55
-
56
- afterEach(() => {
57
- restoreDom();
58
- });
59
-
60
- describe('connectorModal', () => {
61
- const para: ParaWeb = {} as ParaWeb;
62
-
63
- describe('React 18 path', () => {
64
- it('creates a container and renders with createRoot', async () => {
65
- await renderModalR18(para, {}, vi.fn());
66
-
67
- expect(document.createElement).toHaveBeenCalledWith('div');
68
- expect(document.body.appendChild).toHaveBeenCalledWith(mockContainer);
69
- expect(mockCreateRoot).toHaveBeenCalledWith(mockContainer);
70
- expect(mockRootRender).toHaveBeenCalledTimes(1);
71
-
72
- const el = mockRootRender.mock.calls[0][0];
73
- expect(el.props.isOpen).toBe(true);
74
- expect(el.props.para).toBe(para);
75
- expect(el.props.appName).toBe('Test App');
76
- });
77
-
78
- it('handles undefined props', async () => {
79
- await renderModalR18(para, undefined, vi.fn());
80
-
81
- const el = mockRootRender.mock.calls[0][0];
82
- expect(el.props.isOpen).toBe(true);
83
- expect(el.props.para).toBe(para);
84
- });
85
-
86
- it('calls custom onClose and performs cleanup', async () => {
87
- const custom = vi.fn();
88
- const outer = vi.fn();
89
-
90
- await renderModalR18(para, { onClose: custom }, outer);
91
- mockRootRender.mock.calls[0][0].props.onClose();
92
-
93
- expect(outer).toHaveBeenCalledTimes(1);
94
- expect(custom).toHaveBeenCalledTimes(1);
95
- expect(mockRootUnmount).toHaveBeenCalledTimes(1);
96
- expect(mockContainer.remove).toHaveBeenCalled();
97
- });
98
-
99
- it('reuses existing container', async () => {
100
- await renderModalR18(para, {}, vi.fn());
101
-
102
- vi.clearAllMocks();
103
- (document.getElementById as Mock).mockReturnValueOnce(mockContainer);
104
-
105
- await renderModalR18(para, {}, vi.fn());
106
-
107
- expect(document.createElement).not.toHaveBeenCalled();
108
- expect(document.body.appendChild).not.toHaveBeenCalled();
109
- expect(mockRootRender).toHaveBeenCalledTimes(1);
110
- });
111
- });
112
-
113
- describe('Legacy React path', () => {
114
- let renderModalLegacy: typeof renderModalR18;
115
-
116
- beforeEach(async () => {
117
- vi.resetModules();
118
- stubDom();
119
- const client = await import('react-dom/client');
120
- delete (client as any).createRoot;
121
- if (client.default) delete (client.default as any).createRoot;
122
- ({ renderModal: renderModalLegacy } = await import('../src/connectorModal'));
123
- });
124
-
125
- it('falls back to legacy render when createRoot is unavailable', async () => {
126
- await renderModalLegacy(para, {}, vi.fn());
127
-
128
- expect(mockCreateRoot).not.toHaveBeenCalled();
129
- expect(mockLegacyRender).toHaveBeenCalledTimes(1);
130
-
131
- const [el, container] = mockLegacyRender.mock.calls[0];
132
- expect(container).toBe(mockContainer);
133
- expect(el.props.para).toBe(para);
134
- expect(el.props.appName).toBe('Legacy App');
135
- });
136
-
137
- it('performs cleanup with legacy unmount', async () => {
138
- const outer = vi.fn();
139
-
140
- await renderModalLegacy(para, {}, outer);
141
- mockLegacyRender.mock.calls[0][0].props.onClose();
142
-
143
- expect(outer).toHaveBeenCalledTimes(1);
144
- expect(mockLegacyUnmount).toHaveBeenCalledWith(mockContainer);
145
- expect(mockContainer.remove).toHaveBeenCalled();
146
- });
147
- });
148
- });
package/scripts/build.mjs DELETED
@@ -1,36 +0,0 @@
1
- import * as esbuild from 'esbuild';
2
- import * as fs from 'fs/promises';
3
- import { fileURLToPath } from 'url';
4
- import { dirname, resolve } from 'path';
5
- import { glob } from 'glob';
6
-
7
- const entryPoints = await glob('src/**/*.{ts,tsx,js,jsx}');
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const distDir = resolve(__dirname, '../dist');
11
-
12
- await fs.mkdir(distDir, { recursive: true });
13
- await fs.writeFile(`${distDir}/package.json`, JSON.stringify({ type: 'module', sideEffects: false }, null, 2));
14
-
15
- /** @type {import('esbuild').BuildOptions} */
16
- await esbuild.build({
17
- banner: {
18
- js: '"use client";', // Required for Next 13 App Router
19
- },
20
- bundle: false,
21
- write: true,
22
- format: 'esm',
23
- loader: {
24
- '.png': 'dataurl',
25
- '.svg': 'dataurl',
26
- '.json': 'text',
27
- },
28
- platform: 'browser',
29
- entryPoints,
30
- outdir: distDir,
31
- allowOverwrite: true,
32
- splitting: true, // Required for tree shaking
33
- minify: false,
34
- target: ['es2015'],
35
- packages: 'external',
36
- });