@fdm-monster/client-next 2.2.2 → 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.
- package/.yarn/install-state.gz +0 -0
- package/README.md +19 -0
- package/RELEASE_NOTES.MD +10 -0
- package/dist/assets/{index-BlOaSQti.js → index-BAB7cJ3l.js} +52 -52
- package/dist/assets/{index-BlOaSQti.js.map → index-BAB7cJ3l.js.map} +1 -1
- 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-TeWdSn_6.css +0 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { test, expect } from '../fixtures/test-fixtures';
|
|
2
|
+
import { captureFullPage } from '../utils';
|
|
3
|
+
import { createNavigationHelper } from '../helpers/navigation.helper';
|
|
4
|
+
import { createDialogHelper } from '../helpers/dialog.helper';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Panels and Dialogs Screenshots
|
|
8
|
+
* Captures: YAML import/export, OctoFarm import, menus, various dialogs
|
|
9
|
+
*/
|
|
10
|
+
test.describe('Panels and Dialogs Screenshots', () => {
|
|
11
|
+
test.beforeEach(async ({ apiMock }) => {
|
|
12
|
+
await apiMock.mockAllEndpoints({ loginRequired: false });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('01-yaml-export-panel', async ({ authenticatedPage }) => {
|
|
16
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
17
|
+
await nav.goToSettings();
|
|
18
|
+
|
|
19
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
20
|
+
|
|
21
|
+
// Look for YAML export button
|
|
22
|
+
const exportBtn = authenticatedPage.locator('button').filter({ hasText: /yaml.*export|export.*yaml/i }).first();
|
|
23
|
+
if (await exportBtn.isVisible().catch(() => false)) {
|
|
24
|
+
await exportBtn.click();
|
|
25
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await captureFullPage(authenticatedPage, 'yaml-export-panel.png', 'dialogs');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('02-yaml-import-panel', async ({ authenticatedPage }) => {
|
|
32
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
33
|
+
await nav.goToSettings();
|
|
34
|
+
|
|
35
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
36
|
+
|
|
37
|
+
// Look for YAML import button
|
|
38
|
+
const importBtn = authenticatedPage.locator('button').filter({ hasText: /yaml.*import|import.*yaml/i }).first();
|
|
39
|
+
if (await importBtn.isVisible().catch(() => false)) {
|
|
40
|
+
await importBtn.click();
|
|
41
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await captureFullPage(authenticatedPage, 'yaml-import-panel.png', 'dialogs');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('03-octofarm-import-panel', async ({ authenticatedPage }) => {
|
|
48
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
49
|
+
await nav.goToSettings();
|
|
50
|
+
|
|
51
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
52
|
+
|
|
53
|
+
// Look for OctoFarm import button
|
|
54
|
+
const importBtn = authenticatedPage.locator('button').filter({ hasText: /octofarm.*import|import.*octofarm/i }).first();
|
|
55
|
+
if (await importBtn.isVisible().catch(() => false)) {
|
|
56
|
+
await importBtn.click();
|
|
57
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await captureFullPage(authenticatedPage, 'octofarm-import-panel.png', 'dialogs');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('04-jobs-menu', async ({ authenticatedPage }) => {
|
|
64
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
65
|
+
await nav.goToJobs();
|
|
66
|
+
|
|
67
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
68
|
+
|
|
69
|
+
// Open jobs menu
|
|
70
|
+
const menuBtn = authenticatedPage.locator('button[aria-label*="menu" i], .menu-btn').first();
|
|
71
|
+
if (await menuBtn.isVisible().catch(() => false)) {
|
|
72
|
+
await menuBtn.click();
|
|
73
|
+
await authenticatedPage.waitForTimeout(500);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await captureFullPage(authenticatedPage, 'jobs-menu.png', 'dialogs');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('05-printer-status-menu', async ({ authenticatedPage }) => {
|
|
80
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
81
|
+
await nav.goToPrinterGrid();
|
|
82
|
+
|
|
83
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
84
|
+
|
|
85
|
+
// Click on a printer to open status menu
|
|
86
|
+
const printerCard = authenticatedPage.locator('.printer-card, [data-testid="printer-card"]').first();
|
|
87
|
+
if (await printerCard.isVisible().catch(() => false)) {
|
|
88
|
+
// Look for menu button on printer card
|
|
89
|
+
const menuBtn = printerCard.locator('button[aria-label*="menu" i], .menu-btn').first();
|
|
90
|
+
if (await menuBtn.isVisible().catch(() => false)) {
|
|
91
|
+
await menuBtn.click();
|
|
92
|
+
await authenticatedPage.waitForTimeout(500);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await captureFullPage(authenticatedPage, 'printer-status-menu.png', 'dialogs');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('06-printer-dialog-create', async ({ authenticatedPage }) => {
|
|
100
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
101
|
+
await nav.goToPrinters();
|
|
102
|
+
|
|
103
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
104
|
+
|
|
105
|
+
const addBtn = authenticatedPage.locator('button').filter({ hasText: /add|create|new/i }).first();
|
|
106
|
+
await addBtn.click().catch(() => {});
|
|
107
|
+
|
|
108
|
+
const dialog = createDialogHelper(authenticatedPage);
|
|
109
|
+
await dialog.waitForDialog();
|
|
110
|
+
|
|
111
|
+
await authenticatedPage.waitForTimeout(500);
|
|
112
|
+
|
|
113
|
+
await captureFullPage(authenticatedPage, 'printer-dialog-create.png', 'dialogs');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('07-printer-dialog-update', async ({ authenticatedPage }) => {
|
|
117
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
118
|
+
await nav.goToPrinters();
|
|
119
|
+
|
|
120
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
121
|
+
|
|
122
|
+
// Click edit on first printer
|
|
123
|
+
const editBtn = authenticatedPage.locator('button').filter({ hasText: /edit/i }).first();
|
|
124
|
+
if (await editBtn.isVisible().catch(() => false)) {
|
|
125
|
+
await editBtn.click();
|
|
126
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await captureFullPage(authenticatedPage, 'printer-dialog-update.png', 'dialogs');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('08-printer-dialog-duplicate', async ({ authenticatedPage }) => {
|
|
133
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
134
|
+
await nav.goToPrinters();
|
|
135
|
+
|
|
136
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
137
|
+
|
|
138
|
+
// Look for duplicate button
|
|
139
|
+
const duplicateBtn = authenticatedPage.locator('button').filter({ hasText: /duplicate/i }).first();
|
|
140
|
+
if (await duplicateBtn.isVisible().catch(() => false)) {
|
|
141
|
+
await duplicateBtn.click();
|
|
142
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await captureFullPage(authenticatedPage, 'printer-dialog-duplicate.png', 'dialogs');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('09-printer-test-connection-dialog', async ({ authenticatedPage }) => {
|
|
149
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
150
|
+
await nav.goToPrinters();
|
|
151
|
+
|
|
152
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
153
|
+
|
|
154
|
+
// Look for test connection button
|
|
155
|
+
const testBtn = authenticatedPage.locator('button').filter({ hasText: /test.*connection|connection.*test/i }).first();
|
|
156
|
+
if (await testBtn.isVisible().catch(() => false)) {
|
|
157
|
+
await testBtn.click();
|
|
158
|
+
await authenticatedPage.waitForTimeout(1500);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await captureFullPage(authenticatedPage, 'printer-test-connection-dialog.png', 'dialogs');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('10-printer-type-dropdown', async ({ authenticatedPage }) => {
|
|
165
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
166
|
+
await nav.goToPrinters();
|
|
167
|
+
|
|
168
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
169
|
+
|
|
170
|
+
// Open create printer dialog
|
|
171
|
+
const addBtn = authenticatedPage.locator('button').filter({ hasText: /add|create/i }).first();
|
|
172
|
+
await addBtn.click().catch(() => {});
|
|
173
|
+
|
|
174
|
+
const dialog = createDialogHelper(authenticatedPage);
|
|
175
|
+
await dialog.waitForDialog();
|
|
176
|
+
|
|
177
|
+
await authenticatedPage.waitForTimeout(500);
|
|
178
|
+
|
|
179
|
+
// Click on printer type dropdown
|
|
180
|
+
const typeSelect = authenticatedPage.locator('.v-dialog select, .v-dialog .v-select').filter({ hasText: /type/i }).first();
|
|
181
|
+
if (await typeSelect.isVisible().catch(() => false)) {
|
|
182
|
+
await typeSelect.click();
|
|
183
|
+
await authenticatedPage.waitForTimeout(500);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await captureFullPage(authenticatedPage, 'printer-type-dropdown.png', 'dialogs');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('11-printer-force-save-warning', async ({ authenticatedPage }) => {
|
|
190
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
191
|
+
await nav.goToPrinters();
|
|
192
|
+
|
|
193
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
194
|
+
|
|
195
|
+
// Open create printer dialog and try to save without required fields
|
|
196
|
+
const addBtn = authenticatedPage.locator('button').filter({ hasText: /add|create/i }).first();
|
|
197
|
+
await addBtn.click().catch(() => {});
|
|
198
|
+
|
|
199
|
+
const dialog = createDialogHelper(authenticatedPage);
|
|
200
|
+
await dialog.waitForDialog();
|
|
201
|
+
|
|
202
|
+
await authenticatedPage.waitForTimeout(500);
|
|
203
|
+
|
|
204
|
+
// Try to save (should show validation or force save option)
|
|
205
|
+
const saveBtn = authenticatedPage.locator('.v-dialog button').filter({ hasText: /save|create/i }).first();
|
|
206
|
+
if (await saveBtn.isVisible().catch(() => false)) {
|
|
207
|
+
await saveBtn.click();
|
|
208
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await captureFullPage(authenticatedPage, 'printer-force-save-warning.png', 'dialogs');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('12-edit-floor-panel', async ({ authenticatedPage }) => {
|
|
215
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
216
|
+
await nav.goToSettings('floors');
|
|
217
|
+
|
|
218
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
219
|
+
|
|
220
|
+
// Click on first floor to edit
|
|
221
|
+
const floorItem = authenticatedPage.locator('.floor-item, [data-testid="floor-item"], tr').first();
|
|
222
|
+
if (await floorItem.isVisible().catch(() => false)) {
|
|
223
|
+
await floorItem.click();
|
|
224
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await captureFullPage(authenticatedPage, 'edit-floor-panel.png', 'dialogs');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('13-delete-confirmation-dialog', async ({ authenticatedPage }) => {
|
|
231
|
+
const nav = createNavigationHelper(authenticatedPage);
|
|
232
|
+
await nav.goToPrinters();
|
|
233
|
+
|
|
234
|
+
await authenticatedPage.waitForTimeout(1000);
|
|
235
|
+
|
|
236
|
+
// Look for delete button
|
|
237
|
+
const deleteBtn = authenticatedPage.locator('button').filter({ hasText: /delete|remove/i }).first();
|
|
238
|
+
if (await deleteBtn.isVisible().catch(() => false)) {
|
|
239
|
+
await deleteBtn.click();
|
|
240
|
+
await authenticatedPage.waitForTimeout(500);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await captureFullPage(authenticatedPage, 'delete-confirmation-dialog.png', 'dialogs');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility functions for taking documentation screenshots
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a directory exists, create it if not
|
|
11
|
+
*/
|
|
12
|
+
export function ensureDir(dirPath: string): void {
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get screenshot output path
|
|
20
|
+
*/
|
|
21
|
+
export function getScreenshotPath(filename: string, subdir?: string): string {
|
|
22
|
+
const baseDir = path.join(__dirname, 'output');
|
|
23
|
+
const targetDir = subdir ? path.join(baseDir, subdir) : baseDir;
|
|
24
|
+
ensureDir(targetDir);
|
|
25
|
+
return path.join(targetDir, filename);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wait for page to be fully loaded and animations to complete
|
|
30
|
+
*/
|
|
31
|
+
export async function waitForPageReady(page: Page, timeout = 1000): Promise<void> {
|
|
32
|
+
await page.waitForLoadState('networkidle');
|
|
33
|
+
await page.waitForTimeout(timeout); // Allow animations to complete
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Capture a full page screenshot with consistent settings
|
|
38
|
+
*/
|
|
39
|
+
export async function captureFullPage(
|
|
40
|
+
page: Page,
|
|
41
|
+
filename: string,
|
|
42
|
+
subdir?: string
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
await waitForPageReady(page);
|
|
45
|
+
await page.screenshot({
|
|
46
|
+
path: getScreenshotPath(filename, subdir),
|
|
47
|
+
fullPage: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture viewport screenshot with consistent settings
|
|
53
|
+
*/
|
|
54
|
+
export async function captureViewport(
|
|
55
|
+
page: Page,
|
|
56
|
+
filename: string,
|
|
57
|
+
subdir?: string
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
await waitForPageReady(page);
|
|
60
|
+
await page.screenshot({
|
|
61
|
+
path: getScreenshotPath(filename, subdir),
|
|
62
|
+
fullPage: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Capture element screenshot
|
|
68
|
+
*/
|
|
69
|
+
export async function captureElement(
|
|
70
|
+
page: Page,
|
|
71
|
+
selector: string,
|
|
72
|
+
filename: string,
|
|
73
|
+
subdir?: string
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
await waitForPageReady(page);
|
|
76
|
+
const element = page.locator(selector).first();
|
|
77
|
+
|
|
78
|
+
if (await element.count() === 0) {
|
|
79
|
+
console.warn(`Element not found: ${selector}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await element.screenshot({
|
|
84
|
+
path: getScreenshotPath(filename, subdir),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set up authentication (example - adjust based on your auth implementation)
|
|
90
|
+
*/
|
|
91
|
+
export async function setupAuth(page: Page, token?: string): Promise<void> {
|
|
92
|
+
if (token) {
|
|
93
|
+
await page.goto('/');
|
|
94
|
+
await page.evaluate((authToken) => {
|
|
95
|
+
localStorage.setItem('auth-token', authToken);
|
|
96
|
+
}, token);
|
|
97
|
+
} else {
|
|
98
|
+
// Default login flow
|
|
99
|
+
await page.goto('/login');
|
|
100
|
+
// Add your login logic here
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Capture screenshots at multiple viewport sizes
|
|
106
|
+
*/
|
|
107
|
+
export async function captureResponsive(
|
|
108
|
+
page: Page,
|
|
109
|
+
route: string,
|
|
110
|
+
baseFilename: string,
|
|
111
|
+
viewports: Array<{ width: number; height: number; name: string }>,
|
|
112
|
+
subdir?: string
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
for (const viewport of viewports) {
|
|
115
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
116
|
+
await page.goto(route);
|
|
117
|
+
await waitForPageReady(page);
|
|
118
|
+
|
|
119
|
+
const filename = `${baseFilename}-${viewport.name}.png`;
|
|
120
|
+
await page.screenshot({
|
|
121
|
+
path: getScreenshotPath(filename, subdir),
|
|
122
|
+
fullPage: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Common viewport presets
|
|
129
|
+
*/
|
|
130
|
+
export const VIEWPORTS = {
|
|
131
|
+
DESKTOP_FHD: { width: 1920, height: 1080, name: 'desktop-1920x1080' },
|
|
132
|
+
DESKTOP_HD: { width: 1366, height: 768, name: 'desktop-1366x768' },
|
|
133
|
+
DESKTOP_LAPTOP: { width: 1280, height: 720, name: 'laptop-1280x720' },
|
|
134
|
+
TABLET_PORTRAIT: { width: 768, height: 1024, name: 'tablet-portrait' },
|
|
135
|
+
TABLET_LANDSCAPE: { width: 1024, height: 768, name: 'tablet-landscape' },
|
|
136
|
+
MOBILE_IPHONE_SE: { width: 375, height: 667, name: 'mobile-iphone-se' },
|
|
137
|
+
MOBILE_IPHONE_12: { width: 390, height: 844, name: 'mobile-iphone-12' },
|
|
138
|
+
MOBILE_PIXEL_5: { width: 393, height: 851, name: 'mobile-pixel-5' },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Capture both light and dark theme screenshots
|
|
143
|
+
*/
|
|
144
|
+
export async function captureBothThemes(
|
|
145
|
+
page: Page,
|
|
146
|
+
route: string,
|
|
147
|
+
baseFilename: string,
|
|
148
|
+
themeToggleSelector: string,
|
|
149
|
+
subdir?: string
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
await page.goto(route);
|
|
152
|
+
await waitForPageReady(page);
|
|
153
|
+
|
|
154
|
+
// Capture light theme
|
|
155
|
+
await page.screenshot({
|
|
156
|
+
path: getScreenshotPath(`${baseFilename}-light.png`, subdir),
|
|
157
|
+
fullPage: false,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Toggle to dark theme
|
|
161
|
+
const themeToggle = page.locator(themeToggleSelector);
|
|
162
|
+
if (await themeToggle.count() > 0) {
|
|
163
|
+
await themeToggle.click();
|
|
164
|
+
await page.waitForTimeout(500); // Wait for theme transition
|
|
165
|
+
|
|
166
|
+
// Capture dark theme
|
|
167
|
+
await page.screenshot({
|
|
168
|
+
path: getScreenshotPath(`${baseFilename}-dark.png`, subdir),
|
|
169
|
+
fullPage: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hide elements before taking screenshot (useful for hiding dynamic content)
|
|
176
|
+
*/
|
|
177
|
+
export async function hideElements(page: Page, selectors: string[]): Promise<void> {
|
|
178
|
+
for (const selector of selectors) {
|
|
179
|
+
await page.locator(selector).evaluateAll((elements) => {
|
|
180
|
+
elements.forEach((el: HTMLElement) => {
|
|
181
|
+
el.style.visibility = 'hidden';
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Mask sensitive data in screenshots
|
|
189
|
+
*/
|
|
190
|
+
export async function maskElements(page: Page, selectors: string[]): Promise<void> {
|
|
191
|
+
for (const selector of selectors) {
|
|
192
|
+
await page.locator(selector).evaluateAll((elements) => {
|
|
193
|
+
elements.forEach((el: HTMLElement) => {
|
|
194
|
+
el.style.filter = 'blur(10px)';
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Wait for specific element to be visible
|
|
202
|
+
*/
|
|
203
|
+
export async function waitForElement(
|
|
204
|
+
page: Page,
|
|
205
|
+
selector: string,
|
|
206
|
+
timeout = 5000
|
|
207
|
+
): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
await page.waitForSelector(selector, { timeout, state: 'visible' });
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
console.warn(`Element not visible within timeout: ${selector}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
package/vitest.config.ts
CHANGED
|
@@ -6,6 +6,14 @@ export default mergeConfig(viteConfig, defineConfig({
|
|
|
6
6
|
globals: true,
|
|
7
7
|
setupFiles: ['./test/setup-axios-mock.ts'],
|
|
8
8
|
environment: 'jsdom',
|
|
9
|
+
exclude: [
|
|
10
|
+
'**/node_modules/**',
|
|
11
|
+
'**/dist/**',
|
|
12
|
+
'**/cypress/**',
|
|
13
|
+
'**/.{idea,git,cache,output,temp}/**',
|
|
14
|
+
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
|
15
|
+
'**/screenshots/**', // Exclude Playwright screenshot tests
|
|
16
|
+
],
|
|
9
17
|
server: {
|
|
10
18
|
deps: {
|
|
11
19
|
inline: ['vuetify'],
|