@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,112 @@
|
|
|
1
|
+
import { test as base, Page } from '@playwright/test';
|
|
2
|
+
import { ApiMock } from './api-mock';
|
|
3
|
+
import { mockSocketIO } from './socketio-mock';
|
|
4
|
+
import { mockPrinters } from './data/printers.fixtures';
|
|
5
|
+
import { mockFloors } from './data/floors.fixtures';
|
|
6
|
+
import { mockCameras } from './data/cameras.fixtures';
|
|
7
|
+
import { mockFiles } from './data/files.fixtures';
|
|
8
|
+
import { mockJobs } from './data/jobs.fixtures';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom fixtures for screenshot tests
|
|
12
|
+
* Extends Playwright's base test with additional fixtures for API mocking and authenticated pages
|
|
13
|
+
*/
|
|
14
|
+
type ScreenshotFixtures = {
|
|
15
|
+
apiMock: ApiMock;
|
|
16
|
+
authenticatedPage: Page;
|
|
17
|
+
mockPrinters: typeof mockPrinters;
|
|
18
|
+
mockFloors: typeof mockFloors;
|
|
19
|
+
mockCameras: typeof mockCameras;
|
|
20
|
+
mockFiles: typeof mockFiles;
|
|
21
|
+
mockJobs: typeof mockJobs;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extended test object with custom fixtures
|
|
26
|
+
* Usage: import { test, expect } from '../fixtures/test-fixtures'
|
|
27
|
+
*/
|
|
28
|
+
export const test = base.extend<ScreenshotFixtures>({
|
|
29
|
+
/**
|
|
30
|
+
* Page fixture override - automatically mocks SocketIO for all pages
|
|
31
|
+
* This prevents "server disconnected" messages and injects initial data
|
|
32
|
+
*/
|
|
33
|
+
page: async ({ page }, use) => {
|
|
34
|
+
// Mock SocketIO with initial data before page loads
|
|
35
|
+
await mockSocketIO(page, {
|
|
36
|
+
floors: mockFloors,
|
|
37
|
+
printers: mockPrinters,
|
|
38
|
+
socketStates: {},
|
|
39
|
+
printerEvents: {},
|
|
40
|
+
trackedUploads: { current: [] },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Let all assets (PNG, JPG, SVG) load normally from Vite dev server
|
|
44
|
+
await use(page);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ApiMock fixture - provides API mocking instance
|
|
49
|
+
* Automatically cleans up routes after each test
|
|
50
|
+
*/
|
|
51
|
+
apiMock: async ({ page }, use) => {
|
|
52
|
+
const mock = new ApiMock(page);
|
|
53
|
+
await use(mock);
|
|
54
|
+
await mock.unmockAll();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Authenticated page fixture - provides a page with mocked authentication
|
|
59
|
+
* Use this when you need to test pages that require login
|
|
60
|
+
* Note: Socket.IO data is injected via page fixture above
|
|
61
|
+
*/
|
|
62
|
+
authenticatedPage: async ({ page, apiMock }, use) => {
|
|
63
|
+
// Mock authentication endpoints (no login required)
|
|
64
|
+
await apiMock.mockAuthEndpoints({ loginRequired: false });
|
|
65
|
+
|
|
66
|
+
// Navigate to the app
|
|
67
|
+
await page.goto('/');
|
|
68
|
+
|
|
69
|
+
// Set auth tokens in localStorage
|
|
70
|
+
await page.evaluate(() => {
|
|
71
|
+
localStorage.setItem(
|
|
72
|
+
'auth-token',
|
|
73
|
+
'mock-jwt-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
|
|
74
|
+
);
|
|
75
|
+
localStorage.setItem(
|
|
76
|
+
'refresh-token',
|
|
77
|
+
'mock-refresh-token-abcd1234efgh5678ijkl9012'
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await use(page);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mock data fixtures - provides test data
|
|
86
|
+
* These can be imported and used in tests or modified as needed
|
|
87
|
+
*/
|
|
88
|
+
mockPrinters: async ({}, use) => {
|
|
89
|
+
await use(mockPrinters);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
mockFloors: async ({}, use) => {
|
|
93
|
+
await use(mockFloors);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
mockCameras: async ({}, use) => {
|
|
97
|
+
await use(mockCameras);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
mockFiles: async ({}, use) => {
|
|
101
|
+
await use(mockFiles);
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
mockJobs: async ({}, use) => {
|
|
105
|
+
await use(mockJobs);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-export expect from Playwright for convenience
|
|
111
|
+
*/
|
|
112
|
+
export { expect } from '@playwright/test';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper utilities for interacting with Vuetify dialogs and modals
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class DialogHelper {
|
|
8
|
+
constructor(private page: Page) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wait for a dialog to appear
|
|
12
|
+
* @param selector Optional specific dialog selector, defaults to active Vuetify dialog
|
|
13
|
+
* @param timeout Maximum time to wait in milliseconds
|
|
14
|
+
*/
|
|
15
|
+
async waitForDialog(selector?: string, timeout = 5000): Promise<Locator> {
|
|
16
|
+
if (selector) {
|
|
17
|
+
await this.page.waitForSelector(selector, {
|
|
18
|
+
state: 'visible',
|
|
19
|
+
timeout,
|
|
20
|
+
});
|
|
21
|
+
return this.page.locator(selector).first();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// For Vuetify 3, try multiple selectors
|
|
25
|
+
const selectors = [
|
|
26
|
+
'.v-overlay--active .v-card',
|
|
27
|
+
'.v-dialog .v-card',
|
|
28
|
+
'[role="dialog"]',
|
|
29
|
+
'.v-dialog--active',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const sel of selectors) {
|
|
33
|
+
const visible = await this.page.locator(sel).isVisible().catch(() => false);
|
|
34
|
+
if (visible) {
|
|
35
|
+
return this.page.locator(sel).first();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Wait for any of the selectors
|
|
40
|
+
await this.page.waitForSelector('.v-overlay--active .v-card, .v-dialog .v-card, [role="dialog"]', {
|
|
41
|
+
state: 'visible',
|
|
42
|
+
timeout,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return this.page.locator('.v-overlay--active .v-card, .v-dialog .v-card').first();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Wait for dialog to disappear
|
|
50
|
+
* @param selector Optional specific dialog selector
|
|
51
|
+
* @param timeout Maximum time to wait in milliseconds
|
|
52
|
+
*/
|
|
53
|
+
async waitForDialogToClose(selector?: string, timeout = 5000): Promise<void> {
|
|
54
|
+
const dialogSelector = selector || '.v-dialog--active';
|
|
55
|
+
await this.page.waitForSelector(dialogSelector, {
|
|
56
|
+
state: 'hidden',
|
|
57
|
+
timeout,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Click the close button in the dialog (usually the X button)
|
|
63
|
+
*/
|
|
64
|
+
async closeDialog(): Promise<void> {
|
|
65
|
+
// Try multiple common close button selectors
|
|
66
|
+
const closeSelectors = [
|
|
67
|
+
'[data-testid="dialog-close"]',
|
|
68
|
+
'[data-testid="close-dialog"]',
|
|
69
|
+
'.v-dialog button[aria-label="Close"]',
|
|
70
|
+
'.v-dialog .v-btn--icon',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const selector of closeSelectors) {
|
|
74
|
+
const button = this.page.locator(selector).first();
|
|
75
|
+
if (await button.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
76
|
+
await button.click();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback: press Escape key
|
|
82
|
+
await this.page.keyboard.press('Escape');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Click the submit/confirm button in the dialog
|
|
87
|
+
* @param buttonText Optional specific button text, defaults to common submit button selectors
|
|
88
|
+
*/
|
|
89
|
+
async submitDialog(buttonText?: string): Promise<void> {
|
|
90
|
+
if (buttonText) {
|
|
91
|
+
const button = this.page
|
|
92
|
+
.locator('.v-dialog button')
|
|
93
|
+
.filter({ hasText: buttonText })
|
|
94
|
+
.first();
|
|
95
|
+
await button.click();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try common submit button selectors
|
|
100
|
+
const submitSelectors = [
|
|
101
|
+
'[data-testid="dialog-submit"]',
|
|
102
|
+
'[data-testid="dialog-confirm"]',
|
|
103
|
+
'[data-testid="submit-dialog"]',
|
|
104
|
+
'.v-dialog button[type="submit"]',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const selector of submitSelectors) {
|
|
108
|
+
const button = this.page.locator(selector).first();
|
|
109
|
+
if (await button.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
110
|
+
await button.click();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback: look for primary button
|
|
116
|
+
const primaryButton = this.page
|
|
117
|
+
.locator('.v-dialog .v-btn--variant-elevated')
|
|
118
|
+
.last();
|
|
119
|
+
if (await primaryButton.isVisible()) {
|
|
120
|
+
await primaryButton.click();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Click the cancel button in the dialog
|
|
126
|
+
*/
|
|
127
|
+
async cancelDialog(): Promise<void> {
|
|
128
|
+
const cancelSelectors = [
|
|
129
|
+
'[data-testid="dialog-cancel"]',
|
|
130
|
+
'[data-testid="cancel-dialog"]',
|
|
131
|
+
'.v-dialog button:has-text("Cancel")',
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const selector of cancelSelectors) {
|
|
135
|
+
const button = this.page.locator(selector).first();
|
|
136
|
+
if (await button.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
137
|
+
await button.click();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get dialog title text
|
|
145
|
+
*/
|
|
146
|
+
async getDialogTitle(): Promise<string> {
|
|
147
|
+
const titleSelectors = [
|
|
148
|
+
'.v-dialog .v-card-title',
|
|
149
|
+
'.v-dialog .v-toolbar-title',
|
|
150
|
+
'.v-dialog h2',
|
|
151
|
+
'.v-dialog h3',
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
for (const selector of titleSelectors) {
|
|
155
|
+
const title = this.page.locator(selector).first();
|
|
156
|
+
if (await title.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
157
|
+
return await title.textContent() || '';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if dialog is currently open
|
|
166
|
+
*/
|
|
167
|
+
async isDialogOpen(selector?: string): Promise<boolean> {
|
|
168
|
+
const dialogSelector = selector || '.v-dialog--active';
|
|
169
|
+
return await this.page
|
|
170
|
+
.locator(dialogSelector)
|
|
171
|
+
.isVisible()
|
|
172
|
+
.catch(() => false);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Wait for dialog content to be fully loaded
|
|
177
|
+
* Waits for network idle and any loading spinners to disappear
|
|
178
|
+
*/
|
|
179
|
+
async waitForDialogContentReady(timeout = 5000): Promise<void> {
|
|
180
|
+
// Wait for any loading spinners to disappear
|
|
181
|
+
await this.page
|
|
182
|
+
.locator('.v-dialog .v-progress-circular')
|
|
183
|
+
.waitFor({ state: 'hidden', timeout })
|
|
184
|
+
.catch(() => {});
|
|
185
|
+
|
|
186
|
+
// Wait a bit for animations
|
|
187
|
+
await this.page.waitForTimeout(300);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a dialog helper instance for a page
|
|
193
|
+
*/
|
|
194
|
+
export function createDialogHelper(page: Page): DialogHelper {
|
|
195
|
+
return new DialogHelper(page);
|
|
196
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper utilities for filling forms
|
|
5
|
+
* Provides methods to fill common form fields used in FDM Monster
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class FormHelper {
|
|
9
|
+
constructor(private page: Page) {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fill a text input field
|
|
13
|
+
* @param selector Field selector or label text
|
|
14
|
+
* @param value Value to fill
|
|
15
|
+
*/
|
|
16
|
+
async fillTextField(selector: string, value: string): Promise<void> {
|
|
17
|
+
const input = this.page.locator(selector).first();
|
|
18
|
+
await input.clear();
|
|
19
|
+
await input.fill(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fill printer form fields
|
|
24
|
+
*/
|
|
25
|
+
async fillPrinterForm(data: {
|
|
26
|
+
name?: string;
|
|
27
|
+
printerURL?: string;
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
printerType?: number;
|
|
30
|
+
username?: string;
|
|
31
|
+
password?: string;
|
|
32
|
+
}): Promise<void> {
|
|
33
|
+
if (data.name !== undefined) {
|
|
34
|
+
await this.fillTextField('[name="name"]', data.name);
|
|
35
|
+
}
|
|
36
|
+
if (data.printerURL !== undefined) {
|
|
37
|
+
await this.fillTextField('[name="printerURL"]', data.printerURL);
|
|
38
|
+
}
|
|
39
|
+
if (data.apiKey !== undefined) {
|
|
40
|
+
await this.fillTextField('[name="apiKey"]', data.apiKey);
|
|
41
|
+
}
|
|
42
|
+
if (data.username !== undefined) {
|
|
43
|
+
await this.fillTextField('[name="username"]', data.username);
|
|
44
|
+
}
|
|
45
|
+
if (data.password !== undefined) {
|
|
46
|
+
await this.fillTextField('[name="password"]', data.password);
|
|
47
|
+
}
|
|
48
|
+
if (data.printerType !== undefined) {
|
|
49
|
+
await this.selectDropdownOption('[name="printerType"]', data.printerType.toString());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fill floor form fields
|
|
55
|
+
*/
|
|
56
|
+
async fillFloorForm(data: { name?: string; order?: string }): Promise<void> {
|
|
57
|
+
if (data.name !== undefined) {
|
|
58
|
+
await this.fillTextField('[name="name"]', data.name);
|
|
59
|
+
}
|
|
60
|
+
if (data.order !== undefined) {
|
|
61
|
+
await this.fillTextField('[name="order"]', data.order);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fill camera form fields
|
|
67
|
+
*/
|
|
68
|
+
async fillCameraForm(data: {
|
|
69
|
+
name?: string;
|
|
70
|
+
streamURL?: string;
|
|
71
|
+
printerId?: number;
|
|
72
|
+
}): Promise<void> {
|
|
73
|
+
if (data.name !== undefined) {
|
|
74
|
+
await this.fillTextField('[name="name"]', data.name);
|
|
75
|
+
}
|
|
76
|
+
if (data.streamURL !== undefined) {
|
|
77
|
+
await this.fillTextField('[name="streamURL"]', data.streamURL);
|
|
78
|
+
}
|
|
79
|
+
if (data.printerId !== undefined) {
|
|
80
|
+
await this.selectDropdownOption('[name="printerId"]', data.printerId.toString());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fill login form
|
|
86
|
+
*/
|
|
87
|
+
async fillLoginForm(username: string, password: string): Promise<void> {
|
|
88
|
+
await this.fillTextField('[name="username"]', username);
|
|
89
|
+
await this.fillTextField('[name="password"]', password);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fill registration form
|
|
94
|
+
*/
|
|
95
|
+
async fillRegistrationForm(
|
|
96
|
+
username: string,
|
|
97
|
+
password: string,
|
|
98
|
+
confirmPassword?: string
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
await this.fillTextField('[name="username"]', username);
|
|
101
|
+
await this.fillTextField('[name="password"]', password);
|
|
102
|
+
if (confirmPassword !== undefined) {
|
|
103
|
+
await this.fillTextField('[name="confirmPassword"]', confirmPassword);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Select option from dropdown (Vuetify select)
|
|
109
|
+
* @param selector Select field selector
|
|
110
|
+
* @param value Value or visible text to select
|
|
111
|
+
*/
|
|
112
|
+
async selectDropdownOption(selector: string, value: string): Promise<void> {
|
|
113
|
+
// Click to open the select
|
|
114
|
+
const select = this.page.locator(selector).first();
|
|
115
|
+
await select.click();
|
|
116
|
+
|
|
117
|
+
// Wait for menu to appear
|
|
118
|
+
await this.page.waitForSelector('.v-menu--active', { state: 'visible' });
|
|
119
|
+
|
|
120
|
+
// Try to click by value or text
|
|
121
|
+
const option = this.page
|
|
122
|
+
.locator('.v-menu--active .v-list-item')
|
|
123
|
+
.filter({ hasText: value })
|
|
124
|
+
.first();
|
|
125
|
+
|
|
126
|
+
if (await option.isVisible()) {
|
|
127
|
+
await option.click();
|
|
128
|
+
} else {
|
|
129
|
+
// Fallback: click first option if no match found
|
|
130
|
+
await this.page.locator('.v-menu--active .v-list-item').first().click();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Toggle a checkbox
|
|
136
|
+
* @param selector Checkbox selector
|
|
137
|
+
* @param checked Desired state (true for checked, false for unchecked)
|
|
138
|
+
*/
|
|
139
|
+
async toggleCheckbox(selector: string, checked: boolean): Promise<void> {
|
|
140
|
+
const checkbox = this.page.locator(selector).first();
|
|
141
|
+
const isChecked = await checkbox.isChecked();
|
|
142
|
+
|
|
143
|
+
if (isChecked !== checked) {
|
|
144
|
+
await checkbox.click();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Toggle a switch (Vuetify switch)
|
|
150
|
+
* @param selector Switch selector
|
|
151
|
+
* @param enabled Desired state (true for on, false for off)
|
|
152
|
+
*/
|
|
153
|
+
async toggleSwitch(selector: string, enabled: boolean): Promise<void> {
|
|
154
|
+
const switchElement = this.page.locator(selector).first();
|
|
155
|
+
const isEnabled = await switchElement.getAttribute('aria-checked');
|
|
156
|
+
|
|
157
|
+
if ((isEnabled === 'true') !== enabled) {
|
|
158
|
+
await switchElement.click();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Wait for form validation to complete
|
|
164
|
+
* Waits for error messages to appear or disappear
|
|
165
|
+
*/
|
|
166
|
+
async waitForValidation(timeout = 2000): Promise<void> {
|
|
167
|
+
await this.page.waitForTimeout(500);
|
|
168
|
+
// Wait for any validation messages to settle
|
|
169
|
+
await this.page
|
|
170
|
+
.locator('.v-messages__message')
|
|
171
|
+
.first()
|
|
172
|
+
.waitFor({ state: 'visible', timeout })
|
|
173
|
+
.catch(() => {});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get validation error message for a field
|
|
178
|
+
* @param fieldSelector Field selector
|
|
179
|
+
*/
|
|
180
|
+
async getFieldError(fieldSelector: string): Promise<string | null> {
|
|
181
|
+
const field = this.page.locator(fieldSelector).first();
|
|
182
|
+
const errorMessage = field
|
|
183
|
+
.locator('.. >> .v-messages__message')
|
|
184
|
+
.first();
|
|
185
|
+
|
|
186
|
+
if (await errorMessage.isVisible().catch(() => false)) {
|
|
187
|
+
return await errorMessage.textContent();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if form has any validation errors
|
|
195
|
+
*/
|
|
196
|
+
async hasValidationErrors(): Promise<boolean> {
|
|
197
|
+
const errorMessages = this.page.locator('.v-messages__message.error--text');
|
|
198
|
+
return (await errorMessages.count()) > 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a form helper instance for a page
|
|
204
|
+
*/
|
|
205
|
+
export function createFormHelper(page: Page): FormHelper {
|
|
206
|
+
return new FormHelper(page);
|
|
207
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper utilities for navigation and routing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class NavigationHelper {
|
|
8
|
+
constructor(private page: Page) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Navigate to a specific route
|
|
12
|
+
* @param path Route path (e.g., '/dashboard', '/printer-grid')
|
|
13
|
+
*/
|
|
14
|
+
async navigateTo(path: string): Promise<void> {
|
|
15
|
+
await this.page.goto(path);
|
|
16
|
+
await this.page.waitForLoadState('networkidle');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Navigate to dashboard
|
|
21
|
+
*/
|
|
22
|
+
async goToDashboard(): Promise<void> {
|
|
23
|
+
await this.navigateTo('/dashboard');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Navigate to printer grid
|
|
28
|
+
*/
|
|
29
|
+
async goToPrinterGrid(): Promise<void> {
|
|
30
|
+
await this.navigateTo('/printer-grid');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Navigate to printer list
|
|
35
|
+
*/
|
|
36
|
+
async goToPrinterList(): Promise<void> {
|
|
37
|
+
await this.navigateTo('/printer-list');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Navigate to camera grid
|
|
42
|
+
*/
|
|
43
|
+
async goToCameras(): Promise<void> {
|
|
44
|
+
await this.navigateTo('/cameras');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Navigate to print jobs
|
|
49
|
+
*/
|
|
50
|
+
async goToPrintJobs(): Promise<void> {
|
|
51
|
+
await this.navigateTo('/print-jobs');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Navigate to print queue
|
|
56
|
+
*/
|
|
57
|
+
async goToPrintQueue(): Promise<void> {
|
|
58
|
+
await this.navigateTo('/queue');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Navigate to files
|
|
63
|
+
*/
|
|
64
|
+
async goToFiles(): Promise<void> {
|
|
65
|
+
await this.navigateTo('/files');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Navigate to settings
|
|
70
|
+
* @param subsection Optional settings subsection (e.g., 'server', 'users', 'floors')
|
|
71
|
+
*/
|
|
72
|
+
async goToSettings(subsection?: string): Promise<void> {
|
|
73
|
+
if (subsection) {
|
|
74
|
+
await this.navigateTo(`/settings/${subsection}`);
|
|
75
|
+
} else {
|
|
76
|
+
await this.navigateTo('/settings');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Navigate to login page
|
|
82
|
+
*/
|
|
83
|
+
async goToLogin(): Promise<void> {
|
|
84
|
+
await this.navigateTo('/login');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Navigate to registration page
|
|
89
|
+
*/
|
|
90
|
+
async goToRegister(): Promise<void> {
|
|
91
|
+
await this.navigateTo('/registration');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Navigate to first time setup
|
|
96
|
+
*/
|
|
97
|
+
async goToFirstTimeSetup(): Promise<void> {
|
|
98
|
+
await this.navigateTo('/first-time-setup');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Open sidebar/navigation drawer (if collapsed on mobile/tablet)
|
|
103
|
+
*/
|
|
104
|
+
async openSidebar(): Promise<void> {
|
|
105
|
+
const sidebarToggle = this.page.locator('[data-testid="sidebar-toggle"]');
|
|
106
|
+
const navigationDrawer = this.page.locator('.v-navigation-drawer');
|
|
107
|
+
|
|
108
|
+
// Check if sidebar is hidden (mobile view)
|
|
109
|
+
const isHidden = await navigationDrawer.isHidden().catch(() => true);
|
|
110
|
+
|
|
111
|
+
if (isHidden && (await sidebarToggle.isVisible())) {
|
|
112
|
+
await sidebarToggle.click();
|
|
113
|
+
await this.page.waitForSelector('.v-navigation-drawer', {
|
|
114
|
+
state: 'visible',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Click a navigation link in the sidebar
|
|
121
|
+
* @param linkText Text of the navigation link
|
|
122
|
+
*/
|
|
123
|
+
async clickSidebarLink(linkText: string): Promise<void> {
|
|
124
|
+
await this.openSidebar();
|
|
125
|
+
|
|
126
|
+
const link = this.page
|
|
127
|
+
.locator('.v-navigation-drawer a, .v-navigation-drawer .v-list-item')
|
|
128
|
+
.filter({ hasText: linkText })
|
|
129
|
+
.first();
|
|
130
|
+
|
|
131
|
+
await link.click();
|
|
132
|
+
await this.page.waitForLoadState('networkidle');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wait for page to be fully loaded
|
|
137
|
+
* Waits for network idle and any loading indicators to disappear
|
|
138
|
+
*/
|
|
139
|
+
async waitForPageLoad(timeout = 5000): Promise<void> {
|
|
140
|
+
await this.page.waitForLoadState('networkidle');
|
|
141
|
+
|
|
142
|
+
// Wait for any loading spinners to disappear
|
|
143
|
+
await this.page
|
|
144
|
+
.locator('.v-progress-circular, .v-progress-linear')
|
|
145
|
+
.first()
|
|
146
|
+
.waitFor({ state: 'hidden', timeout })
|
|
147
|
+
.catch(() => {});
|
|
148
|
+
|
|
149
|
+
// Small delay for animations
|
|
150
|
+
await this.page.waitForTimeout(300);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get current route path
|
|
155
|
+
*/
|
|
156
|
+
async getCurrentPath(): Promise<string> {
|
|
157
|
+
return await this.page.evaluate(() => window.location.pathname);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if currently on a specific route
|
|
162
|
+
* @param path Route path to check
|
|
163
|
+
*/
|
|
164
|
+
async isOnRoute(path: string): Promise<boolean> {
|
|
165
|
+
const currentPath = await this.getCurrentPath();
|
|
166
|
+
return currentPath === path;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Go back to previous page
|
|
171
|
+
*/
|
|
172
|
+
async goBack(): Promise<void> {
|
|
173
|
+
await this.page.goBack();
|
|
174
|
+
await this.waitForPageLoad();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Refresh current page
|
|
179
|
+
*/
|
|
180
|
+
async refresh(): Promise<void> {
|
|
181
|
+
await this.page.reload();
|
|
182
|
+
await this.waitForPageLoad();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a navigation helper instance for a page
|
|
188
|
+
*/
|
|
189
|
+
export function createNavigationHelper(page: Page): NavigationHelper {
|
|
190
|
+
return new NavigationHelper(page);
|
|
191
|
+
}
|