@aaronshaf/confluence-cli 0.1.15

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/package.json +73 -0
  4. package/src/cli/commands/attachments.ts +113 -0
  5. package/src/cli/commands/clone.ts +188 -0
  6. package/src/cli/commands/comments.ts +56 -0
  7. package/src/cli/commands/create.ts +58 -0
  8. package/src/cli/commands/delete.ts +46 -0
  9. package/src/cli/commands/doctor.ts +161 -0
  10. package/src/cli/commands/duplicate-check.ts +89 -0
  11. package/src/cli/commands/file-rename.ts +113 -0
  12. package/src/cli/commands/folder-hierarchy.ts +241 -0
  13. package/src/cli/commands/info.ts +56 -0
  14. package/src/cli/commands/labels.ts +53 -0
  15. package/src/cli/commands/move.ts +23 -0
  16. package/src/cli/commands/open.ts +145 -0
  17. package/src/cli/commands/pull.ts +241 -0
  18. package/src/cli/commands/push-errors.ts +40 -0
  19. package/src/cli/commands/push.ts +699 -0
  20. package/src/cli/commands/search.ts +62 -0
  21. package/src/cli/commands/setup.ts +124 -0
  22. package/src/cli/commands/spaces.ts +42 -0
  23. package/src/cli/commands/status.ts +88 -0
  24. package/src/cli/commands/tree.ts +190 -0
  25. package/src/cli/help.ts +425 -0
  26. package/src/cli/index.ts +413 -0
  27. package/src/cli/utils/browser.ts +34 -0
  28. package/src/cli/utils/progress-reporter.ts +49 -0
  29. package/src/cli.ts +6 -0
  30. package/src/lib/config.ts +156 -0
  31. package/src/lib/confluence-client/attachment-operations.ts +221 -0
  32. package/src/lib/confluence-client/client.ts +653 -0
  33. package/src/lib/confluence-client/comment-operations.ts +60 -0
  34. package/src/lib/confluence-client/folder-operations.ts +203 -0
  35. package/src/lib/confluence-client/index.ts +47 -0
  36. package/src/lib/confluence-client/label-operations.ts +102 -0
  37. package/src/lib/confluence-client/page-operations.ts +270 -0
  38. package/src/lib/confluence-client/search-operations.ts +60 -0
  39. package/src/lib/confluence-client/types.ts +329 -0
  40. package/src/lib/confluence-client/user-operations.ts +58 -0
  41. package/src/lib/dependency-sorter.ts +233 -0
  42. package/src/lib/errors.ts +237 -0
  43. package/src/lib/file-scanner.ts +195 -0
  44. package/src/lib/formatters.ts +314 -0
  45. package/src/lib/health-check.ts +204 -0
  46. package/src/lib/markdown/converter.ts +427 -0
  47. package/src/lib/markdown/frontmatter.ts +116 -0
  48. package/src/lib/markdown/html-converter.ts +398 -0
  49. package/src/lib/markdown/index.ts +21 -0
  50. package/src/lib/markdown/link-converter.ts +189 -0
  51. package/src/lib/markdown/reference-updater.ts +251 -0
  52. package/src/lib/markdown/slugify.ts +32 -0
  53. package/src/lib/page-state.ts +195 -0
  54. package/src/lib/resolve-page-target.ts +33 -0
  55. package/src/lib/space-config.ts +264 -0
  56. package/src/lib/sync/cleanup.ts +50 -0
  57. package/src/lib/sync/folder-path.ts +61 -0
  58. package/src/lib/sync/index.ts +2 -0
  59. package/src/lib/sync/link-resolution-pass.ts +139 -0
  60. package/src/lib/sync/sync-engine.ts +681 -0
  61. package/src/lib/sync/sync-specific.ts +221 -0
  62. package/src/lib/sync/types.ts +42 -0
  63. package/src/test/attachments.test.ts +68 -0
  64. package/src/test/clone.test.ts +373 -0
  65. package/src/test/comments.test.ts +53 -0
  66. package/src/test/config.test.ts +209 -0
  67. package/src/test/confluence-client.test.ts +535 -0
  68. package/src/test/delete.test.ts +39 -0
  69. package/src/test/dependency-sorter.test.ts +384 -0
  70. package/src/test/errors.test.ts +199 -0
  71. package/src/test/file-rename.test.ts +305 -0
  72. package/src/test/file-scanner.test.ts +331 -0
  73. package/src/test/folder-hierarchy.test.ts +337 -0
  74. package/src/test/formatters.test.ts +213 -0
  75. package/src/test/html-converter.test.ts +399 -0
  76. package/src/test/info.test.ts +56 -0
  77. package/src/test/labels.test.ts +70 -0
  78. package/src/test/link-conversion-integration.test.ts +189 -0
  79. package/src/test/link-converter.test.ts +413 -0
  80. package/src/test/link-resolution-pass.test.ts +368 -0
  81. package/src/test/markdown.test.ts +443 -0
  82. package/src/test/mocks/handlers.ts +228 -0
  83. package/src/test/move.test.ts +53 -0
  84. package/src/test/msw-schema-validation.ts +151 -0
  85. package/src/test/page-state.test.ts +542 -0
  86. package/src/test/push.test.ts +551 -0
  87. package/src/test/reference-updater.test.ts +293 -0
  88. package/src/test/resolve-page-target.test.ts +55 -0
  89. package/src/test/search.test.ts +64 -0
  90. package/src/test/setup-msw.ts +75 -0
  91. package/src/test/space-config.test.ts +516 -0
  92. package/src/test/spaces.test.ts +53 -0
  93. package/src/test/sync-engine.test.ts +486 -0
  94. package/src/types/turndown-plugin-gfm.d.ts +9 -0
@@ -0,0 +1,293 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { updateReferencesAfterRename } from '../lib/markdown/reference-updater.js';
6
+
7
+ describe('updateReferencesAfterRename', () => {
8
+ let testDir: string;
9
+
10
+ beforeEach(() => {
11
+ // Create a temporary directory for each test
12
+ testDir = mkdtempSync(join(tmpdir(), 'cn-test-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Clean up test directory
17
+ if (testDir) {
18
+ rmSync(testDir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ test('updates single reference in one file', () => {
23
+ // Create files
24
+ writeFileSync(
25
+ join(testDir, 'page1.md'),
26
+ `---
27
+ title: Page 1
28
+ ---
29
+
30
+ See [Page 2](./page2.md) for more info.
31
+ `,
32
+ );
33
+
34
+ writeFileSync(
35
+ join(testDir, 'page2.md'),
36
+ `---
37
+ title: Page 2
38
+ ---
39
+
40
+ Content here.
41
+ `,
42
+ );
43
+
44
+ // Rename page2.md to page2-renamed.md
45
+ const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
46
+
47
+ // Verify results
48
+ expect(results.length).toBe(1);
49
+ expect(results[0].filePath).toBe('page1.md');
50
+ expect(results[0].updatedCount).toBe(1);
51
+
52
+ // Verify file content was updated
53
+ const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
54
+ expect(content).toContain('[Page 2](./page2-renamed.md)');
55
+ expect(content).not.toContain('[Page 2](./page2.md)');
56
+ });
57
+
58
+ test('updates multiple references in one file', () => {
59
+ writeFileSync(
60
+ join(testDir, 'page1.md'),
61
+ `---
62
+ title: Page 1
63
+ ---
64
+
65
+ See [Page 2](./page2.md) for more info.
66
+ Also check out [this link](./page2.md) again.
67
+ And [another reference](./page2.md).
68
+ `,
69
+ );
70
+
71
+ writeFileSync(join(testDir, 'page2.md'), '# Page 2');
72
+
73
+ const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
74
+
75
+ expect(results.length).toBe(1);
76
+ expect(results[0].updatedCount).toBe(3);
77
+
78
+ const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
79
+ const matches = content.match(/\(\.\/page2-renamed\.md\)/g);
80
+ expect(matches?.length).toBe(3);
81
+ });
82
+
83
+ test('updates references in multiple files', () => {
84
+ writeFileSync(join(testDir, 'page1.md'), 'Check [Page 3](./page3.md).');
85
+ writeFileSync(join(testDir, 'page2.md'), 'See [Page 3](./page3.md).');
86
+ writeFileSync(join(testDir, 'page3.md'), '# Page 3');
87
+
88
+ const results = updateReferencesAfterRename(testDir, 'page3.md', 'page3-renamed.md');
89
+
90
+ expect(results.length).toBe(2);
91
+ expect(results.map((r) => r.filePath).sort()).toEqual(['page1.md', 'page2.md']);
92
+
93
+ const content1 = readFileSync(join(testDir, 'page1.md'), 'utf-8');
94
+ const content2 = readFileSync(join(testDir, 'page2.md'), 'utf-8');
95
+ expect(content1).toContain('./page3-renamed.md');
96
+ expect(content2).toContain('./page3-renamed.md');
97
+ });
98
+
99
+ test('handles nested directories', () => {
100
+ mkdirSync(join(testDir, 'architecture'));
101
+
102
+ writeFileSync(join(testDir, 'home.md'), 'See [Overview](./architecture/overview.md).');
103
+ writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
104
+
105
+ const results = updateReferencesAfterRename(
106
+ testDir,
107
+ 'architecture/overview.md',
108
+ 'architecture/overview-renamed.md',
109
+ );
110
+
111
+ expect(results.length).toBe(1);
112
+ expect(results[0].filePath).toBe('home.md');
113
+
114
+ const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
115
+ expect(content).toContain('./architecture/overview-renamed.md');
116
+ });
117
+
118
+ test('handles relative paths from subdirectories', () => {
119
+ mkdirSync(join(testDir, 'docs'));
120
+ mkdirSync(join(testDir, 'architecture'));
121
+
122
+ writeFileSync(join(testDir, 'docs', 'guide.md'), 'See [Overview](../architecture/overview.md).');
123
+ writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
124
+
125
+ const results = updateReferencesAfterRename(
126
+ testDir,
127
+ 'architecture/overview.md',
128
+ 'architecture/overview-renamed.md',
129
+ );
130
+
131
+ expect(results.length).toBe(1);
132
+ expect(results[0].filePath).toBe('docs/guide.md');
133
+
134
+ const content = readFileSync(join(testDir, 'docs', 'guide.md'), 'utf-8');
135
+ expect(content).toContain('../architecture/overview-renamed.md');
136
+ });
137
+
138
+ test('returns empty array when no references found', () => {
139
+ writeFileSync(join(testDir, 'page1.md'), 'No references here.');
140
+ writeFileSync(join(testDir, 'page2.md'), '# Page 2');
141
+
142
+ const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
143
+
144
+ expect(results.length).toBe(0);
145
+ });
146
+
147
+ test('does not update the renamed file itself', () => {
148
+ writeFileSync(join(testDir, 'page1.md'), 'See [myself](./page1.md).');
149
+
150
+ const results = updateReferencesAfterRename(testDir, 'page1.md', 'page1-renamed.md');
151
+
152
+ expect(results.length).toBe(0);
153
+ });
154
+
155
+ test('preserves frontmatter when updating references', () => {
156
+ writeFileSync(
157
+ join(testDir, 'page1.md'),
158
+ `---
159
+ page_id: "123"
160
+ title: "Page 1"
161
+ version: 5
162
+ ---
163
+
164
+ See [Page 2](./page2.md).
165
+ `,
166
+ );
167
+
168
+ writeFileSync(join(testDir, 'page2.md'), '# Page 2');
169
+
170
+ updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
171
+
172
+ const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
173
+ // gray-matter converts double quotes to single quotes, which is valid YAML
174
+ expect(content).toMatch(/page_id:\s+['"]123['"]/);
175
+ expect(content).toMatch(/title:\s+['"]?Page 1['"]?/);
176
+ expect(content).toContain('version: 5');
177
+ expect(content).toContain('./page2-renamed.md');
178
+ });
179
+
180
+ test('handles files with special characters in link text', () => {
181
+ writeFileSync(join(testDir, 'page1.md'), 'See [Special (Page) [2]](./page2.md).');
182
+ writeFileSync(join(testDir, 'page2.md'), '# Page 2');
183
+
184
+ const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
185
+
186
+ expect(results.length).toBe(1);
187
+
188
+ const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
189
+ expect(content).toContain('[Special (Page) [2]](./page2-renamed.md)');
190
+ });
191
+
192
+ test('ignores external links', () => {
193
+ writeFileSync(
194
+ join(testDir, 'page1.md'),
195
+ `See [External](https://example.com/page2.md).
196
+ Also [Local](./page2.md).`,
197
+ );
198
+
199
+ writeFileSync(join(testDir, 'page2.md'), '# Page 2');
200
+
201
+ const _results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
202
+
203
+ const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
204
+ expect(content).toContain('https://example.com/page2.md'); // Should not change
205
+ expect(content).toContain('./page2-renamed.md'); // Should change
206
+ });
207
+
208
+ test('handles move to subdirectory', () => {
209
+ mkdirSync(join(testDir, 'architecture'));
210
+
211
+ writeFileSync(join(testDir, 'home.md'), 'See [Overview](./overview.md).');
212
+ writeFileSync(join(testDir, 'overview.md'), '# Overview');
213
+
214
+ const results = updateReferencesAfterRename(testDir, 'overview.md', 'architecture/overview.md');
215
+
216
+ expect(results.length).toBe(1);
217
+
218
+ const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
219
+ expect(content).toContain('./architecture/overview.md');
220
+ });
221
+
222
+ test('handles move from subdirectory to root', () => {
223
+ mkdirSync(join(testDir, 'architecture'));
224
+
225
+ writeFileSync(join(testDir, 'home.md'), 'See [Overview](./architecture/overview.md).');
226
+ writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
227
+
228
+ const results = updateReferencesAfterRename(testDir, 'architecture/overview.md', 'overview.md');
229
+
230
+ expect(results.length).toBe(1);
231
+
232
+ const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
233
+ expect(content).toContain('./overview.md');
234
+ });
235
+
236
+ test('updates links without ./ prefix', () => {
237
+ mkdirSync(join(testDir, 'development'));
238
+
239
+ // Link without ./ prefix (common in markdown)
240
+ writeFileSync(
241
+ join(testDir, 'README.md'),
242
+ `---
243
+ title: README
244
+ ---
245
+
246
+ See [I18n Guide](development/i18n-guidelines.md) for details.
247
+ `,
248
+ );
249
+ writeFileSync(join(testDir, 'development', 'i18n-guidelines.md'), '# I18n');
250
+
251
+ const results = updateReferencesAfterRename(
252
+ testDir,
253
+ 'development/i18n-guidelines.md',
254
+ 'development/internationalization-guidelines.md',
255
+ );
256
+
257
+ expect(results.length).toBe(1);
258
+ expect(results[0].filePath).toBe('README.md');
259
+ expect(results[0].updatedCount).toBe(1);
260
+
261
+ const content = readFileSync(join(testDir, 'README.md'), 'utf-8');
262
+ expect(content).toContain('development/internationalization-guidelines.md');
263
+ expect(content).not.toContain('development/i18n-guidelines.md');
264
+ });
265
+
266
+ test('updates both prefixed and non-prefixed links in same file', () => {
267
+ mkdirSync(join(testDir, 'docs'));
268
+
269
+ writeFileSync(
270
+ join(testDir, 'index.md'),
271
+ `---
272
+ title: Index
273
+ ---
274
+
275
+ See [Guide](docs/guide.md) and also [Guide Again](./docs/guide.md).
276
+ `,
277
+ );
278
+ writeFileSync(join(testDir, 'docs', 'guide.md'), '# Guide');
279
+
280
+ const results = updateReferencesAfterRename(testDir, 'docs/guide.md', 'docs/user-guide.md');
281
+
282
+ expect(results.length).toBe(1);
283
+ expect(results[0].updatedCount).toBe(2);
284
+
285
+ const content = readFileSync(join(testDir, 'index.md'), 'utf-8');
286
+ // Verify both links were updated
287
+ expect(content).not.toContain('docs/guide.md');
288
+ // Verify prefix style is preserved: non-prefixed stays non-prefixed
289
+ expect(content).toContain('[Guide](docs/user-guide.md)');
290
+ // Verify prefix style is preserved: prefixed stays prefixed
291
+ expect(content).toContain('[Guide Again](./docs/user-guide.md)');
292
+ });
293
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { resolvePageTarget } from '../lib/resolve-page-target.js';
5
+
6
+ const TMP = '/tmp/cn-test-resolve';
7
+
8
+ describe('resolvePageTarget', () => {
9
+ test('returns numeric string directly', () => {
10
+ expect(resolvePageTarget('123456')).toBe('123456');
11
+ });
12
+
13
+ test('returns large numeric string directly', () => {
14
+ expect(resolvePageTarget('987654321012')).toBe('987654321012');
15
+ });
16
+
17
+ test('extracts page_id from .md file frontmatter', () => {
18
+ mkdirSync(TMP, { recursive: true });
19
+ const file = join(TMP, 'page.md');
20
+ writeFileSync(
21
+ file,
22
+ `---\npage_id: "99999"\ntitle: Test\nsynced_at: "2024-01-01T00:00:00Z"\n---\n\nContent here.\n`,
23
+ );
24
+ expect(resolvePageTarget(file)).toBe('99999');
25
+ rmSync(file);
26
+ });
27
+
28
+ test('extracts page_id from path containing /', () => {
29
+ mkdirSync(TMP, { recursive: true });
30
+ const file = join(TMP, 'sub.md');
31
+ writeFileSync(file, `---\npage_id: "77777"\ntitle: Sub\nsynced_at: "2024-01-01T00:00:00Z"\n---\n`);
32
+ expect(resolvePageTarget(file)).toBe('77777');
33
+ rmSync(file);
34
+ });
35
+
36
+ test('throws when .md file does not exist', () => {
37
+ expect(() => resolvePageTarget('/nonexistent/page.md')).toThrow('File not found');
38
+ });
39
+
40
+ test('throws when .md file has no page_id in frontmatter', () => {
41
+ mkdirSync(TMP, { recursive: true });
42
+ const file = join(TMP, 'no-id.md');
43
+ writeFileSync(file, `---\ntitle: No ID\n---\n\nContent.\n`);
44
+ expect(() => resolvePageTarget(file)).toThrow('No page_id found');
45
+ rmSync(file);
46
+ });
47
+
48
+ test('throws for non-numeric non-path string', () => {
49
+ expect(() => resolvePageTarget('my-page-slug')).toThrow('Invalid page target');
50
+ });
51
+
52
+ test('throws for empty string', () => {
53
+ expect(() => resolvePageTarget('')).toThrow();
54
+ });
55
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { HttpResponse, http } from 'msw';
3
+ import { ConfluenceClient } from '../lib/confluence-client/client.js';
4
+ import { server } from './setup-msw.js';
5
+
6
+ const testConfig = {
7
+ confluenceUrl: 'https://test.atlassian.net',
8
+ email: 'test@example.com',
9
+ apiToken: 'test-token',
10
+ };
11
+
12
+ describe('ConfluenceClient - search', () => {
13
+ test('returns search results', async () => {
14
+ const client = new ConfluenceClient(testConfig);
15
+ const response = await client.search('type=page AND text~"test"');
16
+ expect(response.results).toBeArray();
17
+ });
18
+
19
+ test('returns empty results for folder-type CQL', async () => {
20
+ const client = new ConfluenceClient(testConfig);
21
+ const response = await client.search('type=folder AND space="TEST"');
22
+ expect(response.results).toBeArray();
23
+ expect(response.results.length).toBe(0);
24
+ });
25
+
26
+ test('narrows results with --space flag via CQL', async () => {
27
+ let capturedCql = '';
28
+ server.use(
29
+ http.get('*/wiki/rest/api/search', ({ request }) => {
30
+ const url = new URL(request.url);
31
+ capturedCql = url.searchParams.get('cql') || '';
32
+ return HttpResponse.json({ results: [], totalSize: 0 });
33
+ }),
34
+ );
35
+
36
+ const client = new ConfluenceClient(testConfig);
37
+ await client.search('type=page AND text~"api" AND space="DOCS"');
38
+ expect(capturedCql).toContain('space="DOCS"');
39
+ });
40
+
41
+ test('handles empty results gracefully', async () => {
42
+ server.use(
43
+ http.get('*/wiki/rest/api/search', () => {
44
+ return HttpResponse.json({ results: [], totalSize: 0 });
45
+ }),
46
+ );
47
+
48
+ const client = new ConfluenceClient(testConfig);
49
+ const response = await client.search('type=page AND text~"nonexistent"');
50
+ expect(response.results).toBeArray();
51
+ expect(response.results.length).toBe(0);
52
+ });
53
+
54
+ test('throws on API error', async () => {
55
+ server.use(
56
+ http.get('*/wiki/rest/api/search', () => {
57
+ return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
58
+ }),
59
+ );
60
+
61
+ const client = new ConfluenceClient(testConfig);
62
+ await expect(client.search('type=page AND text~"test"')).rejects.toThrow();
63
+ });
64
+ });
@@ -0,0 +1,75 @@
1
+ import { afterAll, afterEach, beforeAll } from 'bun:test';
2
+ import { setupServer } from 'msw/node';
3
+ import { handlers } from './mocks/handlers.js';
4
+
5
+ /**
6
+ * MSW (Mock Service Worker) Setup for Bun Test Environment
7
+ *
8
+ * This file configures a shared MSW server for all test files. It is preloaded
9
+ * via bunfig.toml to ensure global hooks run before any test code executes.
10
+ *
11
+ * ## Why preload is required:
12
+ *
13
+ * MSW works by intercepting global.fetch before any code runs. If test files
14
+ * directly manipulate global.fetch, they can break MSW's internal state,
15
+ * causing all subsequent tests to fail.
16
+ *
17
+ * Preloading this file ensures:
18
+ * 1. MSW's server.listen() intercepts fetch first
19
+ * 2. All test files share the same MSW instance
20
+ * 3. Test isolation via server.resetHandlers() in afterEach
21
+ *
22
+ * ## Configuration:
23
+ *
24
+ * See bunfig.toml [test] section for preload configuration.
25
+ *
26
+ * ## Usage in tests:
27
+ *
28
+ * Import server from this file and use server.use() to add test-specific handlers.
29
+ * Handlers added with server.use() are automatically reset after each test.
30
+ *
31
+ * @example
32
+ * import { server } from './setup-msw';
33
+ * import { http, HttpResponse } from 'msw';
34
+ *
35
+ * test('my test', async () => {
36
+ * server.use(
37
+ * http.get(endpoint, () => HttpResponse.json(data))
38
+ * );
39
+ * });
40
+ */
41
+
42
+ // Shared MSW server for all tests
43
+ // Tests add handlers using server.use() which are automatically reset after each test
44
+ export const server = setupServer(...handlers);
45
+
46
+ // Track if server has been started to prevent multiple calls to server.listen()
47
+ let serverStarted = false;
48
+
49
+ // Global hooks - MSW server is active for all tests
50
+ beforeAll(async () => {
51
+ // Ensure we're in a test environment with fetch available
52
+ if (typeof fetch === 'undefined') {
53
+ console.warn('fetch is not defined in MSW setup - skipping MSW server start');
54
+ return;
55
+ }
56
+
57
+ // Only start the server once, even if multiple test files import this module
58
+ if (!serverStarted) {
59
+ server.listen({
60
+ // Use 'warn' mode to log unhandled requests but don't fail tests
61
+ onUnhandledRequest: 'warn',
62
+ });
63
+ serverStarted = true;
64
+ }
65
+ });
66
+
67
+ afterEach(() => {
68
+ // Reset handlers added with server.use() back to the original handlers
69
+ // This prevents handler accumulation which causes performance issues and conflicts
70
+ server.resetHandlers();
71
+ });
72
+
73
+ afterAll(() => {
74
+ server.close();
75
+ });