@fdm-monster/client-next 2.2.1 → 2.2.3

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 (40) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/README.md +19 -0
  3. package/RELEASE_NOTES.MD +22 -0
  4. package/dist/assets/index-BAB7cJ3l.js +105 -0
  5. package/dist/assets/index-BAB7cJ3l.js.map +1 -0
  6. package/dist/assets/index-DfA7W6iO.css +1 -0
  7. package/dist/index.html +2 -2
  8. package/package.json +21 -2
  9. package/screenshots/COVERAGE.md +383 -0
  10. package/screenshots/README.md +431 -0
  11. package/screenshots/fixtures/api-mock.ts +699 -0
  12. package/screenshots/fixtures/data/auth.fixtures.ts +79 -0
  13. package/screenshots/fixtures/data/cameras.fixtures.ts +48 -0
  14. package/screenshots/fixtures/data/files.fixtures.ts +56 -0
  15. package/screenshots/fixtures/data/floors.fixtures.ts +39 -0
  16. package/screenshots/fixtures/data/jobs.fixtures.ts +172 -0
  17. package/screenshots/fixtures/data/printers.fixtures.ts +132 -0
  18. package/screenshots/fixtures/data/settings.fixtures.ts +62 -0
  19. package/screenshots/fixtures/socketio-mock.ts +76 -0
  20. package/screenshots/fixtures/test-fixtures.ts +112 -0
  21. package/screenshots/helpers/dialog.helper.ts +196 -0
  22. package/screenshots/helpers/form.helper.ts +207 -0
  23. package/screenshots/helpers/navigation.helper.ts +191 -0
  24. package/screenshots/playwright.screenshots.config.ts +70 -0
  25. package/screenshots/suites/00-example.screenshots.spec.ts +29 -0
  26. package/screenshots/suites/01-auth.screenshots.spec.ts +130 -0
  27. package/screenshots/suites/02-dashboard.screenshots.spec.ts +106 -0
  28. package/screenshots/suites/03-printer-grid.screenshots.spec.ts +160 -0
  29. package/screenshots/suites/04-printer-list.screenshots.spec.ts +184 -0
  30. package/screenshots/suites/05-camera-grid.screenshots.spec.ts +127 -0
  31. package/screenshots/suites/06-print-jobs.screenshots.spec.ts +139 -0
  32. package/screenshots/suites/07-queue.screenshots.spec.ts +86 -0
  33. package/screenshots/suites/08-files.screenshots.spec.ts +142 -0
  34. package/screenshots/suites/09-settings.screenshots.spec.ts +130 -0
  35. package/screenshots/suites/10-panels-dialogs.screenshots.spec.ts +245 -0
  36. package/screenshots/utils.ts +216 -0
  37. package/vitest.config.ts +8 -0
  38. package/dist/assets/index-BaXVMJVZ.js +0 -105
  39. package/dist/assets/index-BaXVMJVZ.js.map +0 -1
  40. package/dist/assets/index-CU1IeFlc.css +0 -1
@@ -0,0 +1,699 @@
1
+ import { Page, Route } from '@playwright/test';
2
+ import {
3
+ mockLoginRequired,
4
+ mockLoginRequiredTrue,
5
+ mockLoginRequiredWithWizard,
6
+ mockRegistrationEnabled,
7
+ mockLoginResponse,
8
+ mockVerifyResponse,
9
+ mockRefreshResponse,
10
+ } from './data/auth.fixtures';
11
+ import { mockPrinters, mockPrinterEmpty, mockPrinterStates } from './data/printers.fixtures';
12
+ import { mockFloors, mockFloorsEmpty } from './data/floors.fixtures';
13
+ import { mockCameras, mockCamerasEmpty } from './data/cameras.fixtures';
14
+ import { mockFiles, mockFilesEmpty } from './data/files.fixtures';
15
+ import { mockJobs, mockJobsEmpty, mockJobDetails, mockQueue } from './data/jobs.fixtures';
16
+ import {
17
+ mockServerSettings,
18
+ mockUsers,
19
+ mockCurrentUser,
20
+ } from './data/settings.fixtures';
21
+
22
+ /**
23
+ * API mocking helper class for intercepting and mocking API requests
24
+ * Uses Playwright route interception to return mock data
25
+ */
26
+ export class ApiMock {
27
+ constructor(private page: Page) {}
28
+
29
+ /**
30
+ * Helper to fulfill route with JSON response
31
+ */
32
+ private fulfillJson(route: Route, data: any, status = 200) {
33
+ return route.fulfill({
34
+ status,
35
+ contentType: 'application/json',
36
+ body: JSON.stringify(data),
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Mock authentication endpoints
42
+ */
43
+ async mockAuthEndpoints(options?: {
44
+ loginRequired?: boolean;
45
+ wizardIncomplete?: boolean;
46
+ registrationEnabled?: boolean;
47
+ }) {
48
+ // Mock /api/v2/auth/login-required
49
+ await this.page.route('**/api/v2/auth/login-required', (route) => {
50
+ if (options?.wizardIncomplete) {
51
+ return this.fulfillJson(route, mockLoginRequiredWithWizard);
52
+ }
53
+ if (options?.registrationEnabled) {
54
+ return this.fulfillJson(route, mockRegistrationEnabled);
55
+ }
56
+ if (options?.loginRequired) {
57
+ return this.fulfillJson(route, mockLoginRequiredTrue);
58
+ }
59
+ return this.fulfillJson(route, mockLoginRequired);
60
+ });
61
+
62
+ // Mock /api/v2/auth/login
63
+ await this.page.route('**/api/v2/auth/login', (route) => {
64
+ return this.fulfillJson(route, mockLoginResponse);
65
+ });
66
+
67
+ // Mock /api/v2/auth/verify
68
+ await this.page.route('**/api/v2/auth/verify', (route) => {
69
+ return this.fulfillJson(route, mockVerifyResponse);
70
+ });
71
+
72
+ // Mock /api/v2/auth/refresh
73
+ await this.page.route('**/api/v2/auth/refresh', (route) => {
74
+ return this.fulfillJson(route, mockRefreshResponse);
75
+ });
76
+
77
+ // Mock /api/v2/auth/logout
78
+ await this.page.route('**/api/v2/auth/logout', (route) => {
79
+ return this.fulfillJson(route, { success: true });
80
+ });
81
+
82
+ // Mock /api/v2/auth/register
83
+ await this.page.route('**/api/v2/auth/register', (route) => {
84
+ return this.fulfillJson(route, mockLoginResponse, 201);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Mock printer endpoints
90
+ */
91
+ async mockPrinterEndpoints(options?: { empty?: boolean }) {
92
+ const printers = options?.empty ? mockPrinterEmpty : mockPrinters;
93
+
94
+ // Mock GET /api/v2/printer - list printers
95
+ await this.page.route('**/api/v2/printer', (route) => {
96
+ if (route.request().method() === 'GET') {
97
+ return this.fulfillJson(route, printers);
98
+ }
99
+ // POST /api/v2/printer - create printer
100
+ if (route.request().method() === 'POST') {
101
+ const newPrinter = {
102
+ id: printers.length + 1,
103
+ ...JSON.parse(route.request().postData() || '{}'),
104
+ dateAdded: Date.now(),
105
+ };
106
+ return this.fulfillJson(route, newPrinter, 201);
107
+ }
108
+ return route.continue();
109
+ });
110
+
111
+ // Mock GET /api/v2/printer/:id - get single printer
112
+ await this.page.route('**/api/v2/printer/*', (route) => {
113
+ const url = route.request().url();
114
+ const idMatch = url.match(/\/printer\/(\d+)/);
115
+ if (idMatch && route.request().method() === 'GET') {
116
+ const id = Number.parseInt(idMatch[1]);
117
+ const printer = printers.find((p) => p.id === id);
118
+ if (printer) {
119
+ return this.fulfillJson(route, printer);
120
+ }
121
+ return this.fulfillJson(route, { error: 'Printer not found' }, 404);
122
+ }
123
+ return route.continue();
124
+ });
125
+
126
+ // Mock printer state endpoints
127
+ await this.page.route('**/api/v2/printer/*/state', (route) => {
128
+ return this.fulfillJson(route, mockPrinterStates.operational);
129
+ });
130
+
131
+ // Mock printer job control endpoints
132
+ await this.page.route('**/api/v2/printer/*/stop-job', (route) => {
133
+ return this.fulfillJson(route, { success: true });
134
+ });
135
+
136
+ await this.page.route('**/api/v2/printer/*/pause-job', (route) => {
137
+ return this.fulfillJson(route, { success: true });
138
+ });
139
+
140
+ await this.page.route('**/api/v2/printer/*/resume-job', (route) => {
141
+ return this.fulfillJson(route, { success: true });
142
+ });
143
+
144
+ // Mock test connection endpoint
145
+ await this.page.route('**/api/v2/printer/*/test-connection', (route) => {
146
+ return this.fulfillJson(route, { success: true, reachable: true });
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Mock floor endpoints
152
+ */
153
+ async mockFloorEndpoints(options?: { empty?: boolean }) {
154
+ const floors = options?.empty ? mockFloorsEmpty : mockFloors;
155
+
156
+ // Mock GET /api/v2/floor/:id - get single floor (must be before generic floor route)
157
+ await this.page.route('**/api/v2/floor/*/**', (route) => {
158
+ const url = route.request().url();
159
+ const idMatch = url.match(/\/floor\/(\d+)\//);
160
+ if (idMatch) {
161
+ const id = Number.parseInt(idMatch[1]);
162
+
163
+ // Handle floor-specific operations
164
+ if (url.includes('/printer')) {
165
+ // Add/remove printer from floor
166
+ if (route.request().method() === 'POST') {
167
+ const floor = floors.find((f) => f.id === id);
168
+ if (floor) {
169
+ return this.fulfillJson(route, floor);
170
+ }
171
+ }
172
+ if (route.request().method() === 'DELETE') {
173
+ const floor = floors.find((f) => f.id === id);
174
+ if (floor) {
175
+ return this.fulfillJson(route, floor);
176
+ }
177
+ }
178
+ }
179
+
180
+ // Handle floor name/order updates
181
+ if (route.request().method() === 'PATCH' || route.request().method() === 'PUT') {
182
+ const floor = floors.find((f) => f.id === id);
183
+ if (floor) {
184
+ return this.fulfillJson(route, floor);
185
+ }
186
+ }
187
+
188
+ // Handle floor GET
189
+ if (route.request().method() === 'GET') {
190
+ const floor = floors.find((f) => f.id === id);
191
+ if (floor) {
192
+ return this.fulfillJson(route, floor);
193
+ }
194
+ return this.fulfillJson(route, { error: 'Floor not found' }, 404);
195
+ }
196
+
197
+ // Handle floor DELETE
198
+ if (route.request().method() === 'DELETE') {
199
+ return this.fulfillJson(route, { success: true });
200
+ }
201
+ }
202
+ return route.continue();
203
+ });
204
+
205
+ // Mock GET/POST /api/v2/floor/ - list/create floors (generic endpoint)
206
+ await this.page.route('**/api/v2/floor/', (route) => {
207
+ if (route.request().method() === 'GET') {
208
+ return this.fulfillJson(route, floors);
209
+ }
210
+ if (route.request().method() === 'POST') {
211
+ const newFloor = {
212
+ id: floors.length + 1,
213
+ ...JSON.parse(route.request().postData() || '{}'),
214
+ printers: [],
215
+ };
216
+ return this.fulfillJson(route, newFloor, 201);
217
+ }
218
+ return route.continue();
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Mock camera endpoints
224
+ */
225
+ async mockCameraEndpoints(options?: { empty?: boolean }) {
226
+ const cameras = options?.empty ? mockCamerasEmpty : mockCameras;
227
+
228
+ // Mock actual camera stream URLs to return a placeholder image
229
+ // This prevents the browser from trying to load real camera streams
230
+ await this.page.route('**://*/webcam/**', (route) => {
231
+ // Return a simple 1x1 gray pixel as PNG
232
+ const grayPixel = Buffer.from(
233
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM8w8DwHwAEOQHNmnaaOAAAAABJRU5ErkJggg==',
234
+ 'base64'
235
+ );
236
+ return route.fulfill({
237
+ status: 200,
238
+ contentType: 'image/png',
239
+ body: grayPixel,
240
+ });
241
+ });
242
+
243
+ // Mock any IP-based camera stream URLs (like http://192.168.1.100/...)
244
+ await this.page.route('**://192.168.*/**', (route) => {
245
+ const grayPixel = Buffer.from(
246
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM8w8DwHwAEOQHNmnaaOAAAAABJRU5ErkJggg==',
247
+ 'base64'
248
+ );
249
+ return route.fulfill({
250
+ status: 200,
251
+ contentType: 'image/png',
252
+ body: grayPixel,
253
+ });
254
+ });
255
+
256
+ // Mock individual camera operations /api/v2/camera-stream/:id (must be before generic route)
257
+ await this.page.route('**/api/v2/camera-stream/*/***', (route) => {
258
+ const url = route.request().url();
259
+ const idMatch = url.match(/\/camera-stream\/(\d+)/);
260
+
261
+ if (idMatch) {
262
+ const id = Number.parseInt(idMatch[1]);
263
+ const camera = cameras.find((c) => c.id === id);
264
+
265
+ if (route.request().method() === 'GET' && camera) {
266
+ return this.fulfillJson(route, camera);
267
+ }
268
+
269
+ if (route.request().method() === 'PATCH' || route.request().method() === 'PUT') {
270
+ if (camera) {
271
+ return this.fulfillJson(route, camera);
272
+ }
273
+ }
274
+
275
+ if (route.request().method() === 'DELETE') {
276
+ return this.fulfillJson(route, { success: true });
277
+ }
278
+ }
279
+
280
+ return route.continue();
281
+ });
282
+
283
+ // Mock GET/POST /api/v2/camera-stream/ - list/create cameras
284
+ await this.page.route('**/api/v2/camera-stream/', (route) => {
285
+ if (route.request().method() === 'GET') {
286
+ return this.fulfillJson(route, cameras);
287
+ }
288
+ if (route.request().method() === 'POST') {
289
+ const newCamera = {
290
+ id: cameras.length + 1,
291
+ ...JSON.parse(route.request().postData() || '{}'),
292
+ };
293
+ return this.fulfillJson(route, newCamera, 201);
294
+ }
295
+ return route.continue();
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Mock file endpoints
301
+ */
302
+ async mockFileEndpoints(options?: { empty?: boolean }) {
303
+ const files = options?.empty ? mockFilesEmpty : mockFiles;
304
+
305
+ // Mock GET /api/v2/file-storage - list files
306
+ await this.page.route('**/api/v2/file-storage**', (route) => {
307
+ if (route.request().method() === 'GET') {
308
+ return this.fulfillJson(route, files);
309
+ }
310
+ return route.continue();
311
+ });
312
+
313
+ // Mock file upload
314
+ await this.page.route('**/api/v2/file-storage/upload', (route) => {
315
+ return this.fulfillJson(
316
+ route,
317
+ { success: true, fileId: 'uploaded-file-id' },
318
+ 201
319
+ );
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Mock print job endpoints
325
+ */
326
+ async mockJobEndpoints(options?: { empty?: boolean }) {
327
+ const jobs = options?.empty ? mockJobsEmpty : mockJobs;
328
+
329
+ // Mock GET /api/v2/print-jobs/search-paged - paginated job search (with any query params)
330
+ await this.page.route('**/api/v2/print-jobs/search-paged**', (route) => {
331
+ if (route.request().method() === 'GET') {
332
+ const url = new URL(route.request().url());
333
+ const page = Number.parseInt(url.searchParams.get('page') || '1');
334
+ const pageSize = Number.parseInt(url.searchParams.get('pageSize') || '500');
335
+
336
+ return this.fulfillJson(route, {
337
+ items: jobs,
338
+ page,
339
+ count: jobs.length,
340
+ pages: Math.ceil(jobs.length / pageSize),
341
+ });
342
+ }
343
+ return route.continue();
344
+ });
345
+
346
+ // Mock GET /api/v2/print-job/:id - get job details (register BEFORE the generic list)
347
+ await this.page.route('**/api/v2/print-job/*', (route) => {
348
+ const url = route.request().url();
349
+ // Only match numeric IDs, not "search-paged"
350
+ const idMatch = url.match(/\/print-job\/(\d+)$/);
351
+ if (idMatch && route.request().method() === 'GET') {
352
+ return this.fulfillJson(route, mockJobDetails);
353
+ }
354
+ return route.continue();
355
+ });
356
+
357
+ // Mock GET /api/v2/print-job - list jobs (exact match, no query params expected here)
358
+ await this.page.route('**/api/v2/print-job', (route) => {
359
+ const url = route.request().url();
360
+ // Only match exact /print-job, not /print-jobs or /print-job/something
361
+ if (url.endsWith('/print-job') || url.includes('/print-job?')) {
362
+ if (route.request().method() === 'GET') {
363
+ return this.fulfillJson(route, jobs);
364
+ }
365
+ }
366
+ return route.continue();
367
+ });
368
+
369
+ // Mock GET /api/v2/print-queue - get print queue (with query params)
370
+ await this.page.route('**/api/v2/print-queue*', (route) => {
371
+ if (route.request().method() === 'GET') {
372
+ const url = new URL(route.request().url());
373
+ const page = Number.parseInt(url.searchParams.get('page') || '1');
374
+ const pageSize = Number.parseInt(url.searchParams.get('pageSize') || '50');
375
+
376
+ return this.fulfillJson(route, {
377
+ items: mockQueue,
378
+ page,
379
+ pageSize,
380
+ totalCount: mockQueue.length,
381
+ totalPages: Math.ceil(mockQueue.length / pageSize),
382
+ });
383
+ }
384
+ return route.continue();
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Mock settings endpoints
390
+ */
391
+ async mockSettingsEndpoints() {
392
+ // Register settings routes in LIFO order (generic FIRST, specific LAST)
393
+
394
+ // Mock GET /api/v2/settings - full settings object (generic)
395
+ await this.page.route('**/api/v2/settings', (route) => {
396
+ const url = route.request().url();
397
+ // Skip if it's a more specific settings endpoint
398
+ if (url.includes('/settings/')) {
399
+ return route.continue();
400
+ }
401
+
402
+ if (route.request().method() === 'GET') {
403
+ return this.fulfillJson(route, {
404
+ server: mockServerSettings,
405
+ wizard: {
406
+ wizardCompleted: true,
407
+ wizardVersion: 1,
408
+ latestWizardVersion: 1,
409
+ },
410
+ frontend: {
411
+ largeTiles: false,
412
+ gridCols: 4,
413
+ gridRows: 3,
414
+ tilePreferCancelOverQuickStop: false,
415
+ gridNameSortDirection: 'horizontal',
416
+ },
417
+ timeout: {
418
+ apiTimeout: 10000,
419
+ apiUploadTimeout: 600000,
420
+ },
421
+ });
422
+ }
423
+ return route.continue();
424
+ });
425
+
426
+ // Mock GET /api/v2/settings/server
427
+ await this.page.route('**/api/v2/settings/server', (route) => {
428
+ if (route.request().method() === 'GET') {
429
+ return this.fulfillJson(route, mockServerSettings);
430
+ }
431
+ // PATCH /api/v2/settings/server - update settings
432
+ if (route.request().method() === 'PATCH') {
433
+ return this.fulfillJson(route, mockServerSettings);
434
+ }
435
+ return route.continue();
436
+ });
437
+
438
+ // Mock GET /api/v2/settings/sensitive (specific - registered late)
439
+ await this.page.route('**/api/v2/settings/sensitive', (route) => {
440
+ if (route.request().method() === 'GET') {
441
+ return this.fulfillJson(route, {
442
+ sentryDiagnosticsEnabled: false,
443
+ });
444
+ }
445
+ return route.continue();
446
+ });
447
+
448
+ // Mock GET /api/v2/settings/slicer-api-key (specific - registered late)
449
+ await this.page.route('**/api/v2/settings/slicer-api-key', (route) => {
450
+ if (route.request().method() === 'GET') {
451
+ return this.fulfillJson(route, {
452
+ slicerApiKey: 'mock-slicer-api-key-12345',
453
+ });
454
+ }
455
+ return route.continue();
456
+ });
457
+
458
+ // Register user routes in reverse order (LIFO - Last In First Out)
459
+ // Most generic routes registered FIRST, specific routes LAST
460
+
461
+ // Mock individual user operations (generic)
462
+ await this.page.route('**/api/v2/user/*', (route) => {
463
+ const url = route.request().url();
464
+ // Skip if it's /user/profile, /user/me, or /user/roles (handled by more specific routes)
465
+ if (url.endsWith('/user/profile') || url.endsWith('/user/me') || url.endsWith('/user/roles')) {
466
+ return route.continue();
467
+ }
468
+
469
+ const idMatch = url.match(/\/user\/(\d+)$/);
470
+ if (idMatch && route.request().method() === 'GET') {
471
+ const id = Number.parseInt(idMatch[1]);
472
+ const user = mockUsers.find((u) => u.id === id);
473
+ if (user) {
474
+ return this.fulfillJson(route, user);
475
+ }
476
+ }
477
+ return route.continue();
478
+ });
479
+
480
+ // Mock user list endpoint
481
+ await this.page.route('**/api/v2/user/', (route) => {
482
+ if (route.request().method() === 'GET') {
483
+ return this.fulfillJson(route, mockUsers);
484
+ }
485
+ return route.continue();
486
+ });
487
+
488
+ // Mock user roles endpoint (specific)
489
+ await this.page.route('**/api/v2/user/roles', (route) => {
490
+ if (route.request().method() === 'GET') {
491
+ return this.fulfillJson(route, [
492
+ { id: 1, name: 'admin', description: 'Administrator' },
493
+ { id: 2, name: 'user', description: 'Regular User' },
494
+ { id: 3, name: 'guest', description: 'Guest' },
495
+ ]);
496
+ }
497
+ return route.continue();
498
+ });
499
+
500
+ // Mock current user endpoint (more specific)
501
+ await this.page.route('**/api/v2/user/me', (route) => {
502
+ return this.fulfillJson(route, mockCurrentUser);
503
+ });
504
+
505
+ // Mock user profile endpoint (most specific - registered LAST, matched FIRST)
506
+ await this.page.route('**/api/v2/user/profile', (route) => {
507
+ return this.fulfillJson(route, mockCurrentUser);
508
+ });
509
+ }
510
+
511
+ /**
512
+ * Mock server/system endpoints
513
+ */
514
+ async mockServerEndpoints() {
515
+ // Mock GET /api/v2/test - health check/test endpoint
516
+ await this.page.route('**/api/v2/test', (route) => {
517
+ return this.fulfillJson(route, {
518
+ status: 'ok',
519
+ message: 'API is working',
520
+ timestamp: Date.now(),
521
+ });
522
+ });
523
+
524
+ // Mock GET /api/v2/features - feature flags
525
+ await this.page.route('**/api/v2/features', (route) => {
526
+ return this.fulfillJson(route, {
527
+ multiCameraSupport: true,
528
+ experimentalClientSupport: false,
529
+ experimentalMoonrakerSupport: false,
530
+ experimentalPrusaLinkSupport: false,
531
+ });
532
+ });
533
+
534
+ // Mock GET /api/v2/version
535
+ await this.page.route('**/api/v2/version', (route) => {
536
+ return this.fulfillJson(route, {
537
+ version: '2.0.0-mock',
538
+ installedAt: 1704067200000,
539
+ updateAvailable: false,
540
+ });
541
+ });
542
+
543
+ // Mock GET /api/v2/server/version
544
+ await this.page.route('**/api/v2/server/version', (route) => {
545
+ return this.fulfillJson(route, {
546
+ version: '2.0.0-mock',
547
+ apiVersion: 'v2',
548
+ });
549
+ });
550
+
551
+ // Mock GET /api/v2/server/health
552
+ await this.page.route('**/api/v2/server/health', (route) => {
553
+ return this.fulfillJson(route, {
554
+ status: 'healthy',
555
+ uptime: 123456,
556
+ });
557
+ });
558
+
559
+ // Mock GET /api/v2/server/github-rate-limit
560
+ await this.page.route('**/api/v2/server/github-rate-limit', (route) => {
561
+ return this.fulfillJson(route, {
562
+ limit: 60,
563
+ remaining: 58,
564
+ reset: Date.now() + 3600000, // 1 hour from now
565
+ used: 2,
566
+ });
567
+ });
568
+ }
569
+
570
+ /**
571
+ * Mock printer tag endpoints
572
+ */
573
+ async mockPrinterTagEndpoints() {
574
+ // Mock GET /api/v2/printer-tag - list all tags with printers
575
+ await this.page.route('**/api/v2/printer-tag', (route) => {
576
+ if (route.request().method() === 'GET') {
577
+ return this.fulfillJson(route, [
578
+ {
579
+ id: 1,
580
+ name: 'Production',
581
+ color: '#4CAF50',
582
+ printers: [
583
+ { printerId: 1, tagId: 1 },
584
+ { printerId: 2, tagId: 1 },
585
+ ],
586
+ },
587
+ {
588
+ id: 2,
589
+ name: 'Testing',
590
+ color: '#2196F3',
591
+ printers: [
592
+ { printerId: 3, tagId: 2 },
593
+ ],
594
+ },
595
+ {
596
+ id: 3,
597
+ name: 'Maintenance',
598
+ color: '#FF9800',
599
+ printers: [],
600
+ },
601
+ ]);
602
+ }
603
+ // POST /api/v2/printer-tag - create tag
604
+ if (route.request().method() === 'POST') {
605
+ return this.fulfillJson(
606
+ route,
607
+ {
608
+ id: 4,
609
+ name: 'New Tag',
610
+ color: '#9C27B0',
611
+ printers: [],
612
+ },
613
+ 201
614
+ );
615
+ }
616
+ return route.continue();
617
+ });
618
+
619
+ // Mock tag operations (update name, color, add/remove printer)
620
+ await this.page.route('**/api/v2/printer-tag/*', (route) => {
621
+ // Return updated tags array for all operations
622
+ return this.fulfillJson(route, [
623
+ {
624
+ id: 1,
625
+ name: 'Production',
626
+ color: '#4CAF50',
627
+ printers: [
628
+ { printerId: 1, tagId: 1 },
629
+ { printerId: 2, tagId: 1 },
630
+ ],
631
+ },
632
+ {
633
+ id: 2,
634
+ name: 'Testing',
635
+ color: '#2196F3',
636
+ printers: [{ printerId: 3, tagId: 2 }],
637
+ },
638
+ {
639
+ id: 3,
640
+ name: 'Maintenance',
641
+ color: '#FF9800',
642
+ printers: [],
643
+ },
644
+ ]);
645
+ });
646
+ }
647
+
648
+ /**
649
+ * Mock all endpoints at once with default options
650
+ */
651
+ async mockAllEndpoints(options?: {
652
+ loginRequired?: boolean;
653
+ emptyData?: boolean;
654
+ }) {
655
+ // Playwright uses LIFO (Last In First Out) for route matching
656
+ // Routes registered LAST are matched FIRST
657
+ // So: register catch-all FIRST, specific routes LAST
658
+
659
+ // Register catch-all FIRST (will be matched LAST, only if nothing else matched)
660
+ await this.page.route('**/api/v2/**', (route) => {
661
+ const url = route.request().url();
662
+ const method = route.request().method();
663
+ console.warn(`[Mock] Unmocked API endpoint called: ${method} ${url}`);
664
+
665
+ // Return a generic success response
666
+ return this.fulfillJson(route, {
667
+ success: true,
668
+ message: 'Unmocked endpoint - generic response',
669
+ });
670
+ });
671
+
672
+ // Register specific endpoints LAST (will be matched FIRST, taking precedence)
673
+ await this.mockAuthEndpoints({ loginRequired: options?.loginRequired });
674
+ await this.mockServerEndpoints();
675
+ await this.mockSettingsEndpoints();
676
+ await this.mockPrinterEndpoints({ empty: options?.emptyData });
677
+ await this.mockPrinterTagEndpoints();
678
+ await this.mockFloorEndpoints({ empty: options?.emptyData });
679
+ await this.mockCameraEndpoints({ empty: options?.emptyData });
680
+ await this.mockFileEndpoints({ empty: options?.emptyData });
681
+ await this.mockJobEndpoints({ empty: options?.emptyData });
682
+ }
683
+
684
+ /**
685
+ * Update Socket.IO mock data at runtime (for empty data scenarios)
686
+ */
687
+ async updateSocketIOData(data: any) {
688
+ await this.page.evaluate((newData) => {
689
+ (globalThis as any).__SOCKETIO_MOCK_DATA__ = newData;
690
+ }, data);
691
+ }
692
+
693
+ /**
694
+ * Remove all route mocks
695
+ */
696
+ async unmockAll() {
697
+ await this.page.unrouteAll({ behavior: 'ignoreErrors' });
698
+ }
699
+ }