@fdm-monster/client-next 2.2.2 → 2.2.4
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.
- package/.yarn/install-state.gz +0 -0
- package/README.md +19 -0
- package/RELEASE_NOTES.MD +21 -0
- package/dist/assets/index-CvbkNANW.js +105 -0
- package/dist/assets/index-CvbkNANW.js.map +1 -0
- package/dist/assets/index-DfA7W6iO.css +1 -0
- package/dist/index.html +3 -3
- package/package.json +21 -2
- package/screenshots/COVERAGE.md +383 -0
- package/screenshots/README.md +431 -0
- package/screenshots/fixtures/api-mock.ts +699 -0
- package/screenshots/fixtures/data/auth.fixtures.ts +79 -0
- package/screenshots/fixtures/data/cameras.fixtures.ts +48 -0
- package/screenshots/fixtures/data/files.fixtures.ts +56 -0
- package/screenshots/fixtures/data/floors.fixtures.ts +39 -0
- package/screenshots/fixtures/data/jobs.fixtures.ts +172 -0
- package/screenshots/fixtures/data/printers.fixtures.ts +132 -0
- package/screenshots/fixtures/data/settings.fixtures.ts +62 -0
- package/screenshots/fixtures/socketio-mock.ts +76 -0
- package/screenshots/fixtures/test-fixtures.ts +112 -0
- package/screenshots/helpers/dialog.helper.ts +196 -0
- package/screenshots/helpers/form.helper.ts +207 -0
- package/screenshots/helpers/navigation.helper.ts +191 -0
- package/screenshots/playwright.screenshots.config.ts +70 -0
- package/screenshots/suites/00-example.screenshots.spec.ts +29 -0
- package/screenshots/suites/01-auth.screenshots.spec.ts +130 -0
- package/screenshots/suites/02-dashboard.screenshots.spec.ts +106 -0
- package/screenshots/suites/03-printer-grid.screenshots.spec.ts +160 -0
- package/screenshots/suites/04-printer-list.screenshots.spec.ts +184 -0
- package/screenshots/suites/05-camera-grid.screenshots.spec.ts +127 -0
- package/screenshots/suites/06-print-jobs.screenshots.spec.ts +139 -0
- package/screenshots/suites/07-queue.screenshots.spec.ts +86 -0
- package/screenshots/suites/08-files.screenshots.spec.ts +142 -0
- package/screenshots/suites/09-settings.screenshots.spec.ts +130 -0
- package/screenshots/suites/10-panels-dialogs.screenshots.spec.ts +245 -0
- package/screenshots/utils.ts +216 -0
- package/vitest.config.ts +8 -0
- package/dist/assets/index-BlOaSQti.js +0 -105
- package/dist/assets/index-BlOaSQti.js.map +0 -1
- package/dist/assets/index-TeWdSn_6.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
|
+
}
|