@pwtap/create 0.1.0

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 (103) hide show
  1. package/core-manifest.json +50 -0
  2. package/dist/commands/add.d.ts +8 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +9 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/create.d.ts +12 -0
  7. package/dist/commands/create.d.ts.map +1 -0
  8. package/dist/commands/create.js +114 -0
  9. package/dist/commands/create.js.map +1 -0
  10. package/dist/commands/remove.d.ts +7 -0
  11. package/dist/commands/remove.d.ts.map +1 -0
  12. package/dist/commands/remove.js +9 -0
  13. package/dist/commands/remove.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +55 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/injectors/assets.d.ts +6 -0
  19. package/dist/injectors/assets.d.ts.map +1 -0
  20. package/dist/injectors/assets.js +53 -0
  21. package/dist/injectors/assets.js.map +1 -0
  22. package/dist/injectors/envJson.d.ts +6 -0
  23. package/dist/injectors/envJson.d.ts.map +1 -0
  24. package/dist/injectors/envJson.js +39 -0
  25. package/dist/injectors/envJson.js.map +1 -0
  26. package/dist/injectors/fixturesBarrel.d.ts +11 -0
  27. package/dist/injectors/fixturesBarrel.d.ts.map +1 -0
  28. package/dist/injectors/fixturesBarrel.js +66 -0
  29. package/dist/injectors/fixturesBarrel.js.map +1 -0
  30. package/dist/injectors/packageJson.d.ts +6 -0
  31. package/dist/injectors/packageJson.d.ts.map +1 -0
  32. package/dist/injectors/packageJson.js +25 -0
  33. package/dist/injectors/packageJson.js.map +1 -0
  34. package/dist/injectors/pwConfig.d.ts +10 -0
  35. package/dist/injectors/pwConfig.d.ts.map +1 -0
  36. package/dist/injectors/pwConfig.js +40 -0
  37. package/dist/injectors/pwConfig.js.map +1 -0
  38. package/dist/manifest.d.ts +65 -0
  39. package/dist/manifest.d.ts.map +1 -0
  40. package/dist/manifest.js +22 -0
  41. package/dist/manifest.js.map +1 -0
  42. package/dist/plugin-apply.d.ts +13 -0
  43. package/dist/plugin-apply.d.ts.map +1 -0
  44. package/dist/plugin-apply.js +89 -0
  45. package/dist/plugin-apply.js.map +1 -0
  46. package/dist/prompts.d.ts +22 -0
  47. package/dist/prompts.d.ts.map +1 -0
  48. package/dist/prompts.js +65 -0
  49. package/dist/prompts.js.map +1 -0
  50. package/dist/registry.d.ts +26 -0
  51. package/dist/registry.d.ts.map +1 -0
  52. package/dist/registry.js +34 -0
  53. package/dist/registry.js.map +1 -0
  54. package/dist/util/fs.d.ts +13 -0
  55. package/dist/util/fs.d.ts.map +1 -0
  56. package/dist/util/fs.js +32 -0
  57. package/dist/util/fs.js.map +1 -0
  58. package/dist/util/log.d.ts +8 -0
  59. package/dist/util/log.d.ts.map +1 -0
  60. package/dist/util/log.js +9 -0
  61. package/dist/util/log.js.map +1 -0
  62. package/dist/util/markers.d.ts +20 -0
  63. package/dist/util/markers.d.ts.map +1 -0
  64. package/dist/util/markers.js +51 -0
  65. package/dist/util/markers.js.map +1 -0
  66. package/dist/util/run.d.ts +8 -0
  67. package/dist/util/run.d.ts.map +1 -0
  68. package/dist/util/run.js +27 -0
  69. package/dist/util/run.js.map +1 -0
  70. package/package.json +31 -0
  71. package/template/.commitlintrc.json +3 -0
  72. package/template/.prettierrc +17 -0
  73. package/template/api/core/ApiClient.ts +99 -0
  74. package/template/api/core/types.ts +29 -0
  75. package/template/api/index.ts +4 -0
  76. package/template/api/models/pet.ts +24 -0
  77. package/template/api/services/PetService.ts +67 -0
  78. package/template/config/envUtils.ts +70 -0
  79. package/template/config/index.ts +2 -0
  80. package/template/config/loadEnv.ts +110 -0
  81. package/template/env/environments.example.json +20 -0
  82. package/template/eslint.config.js +74 -0
  83. package/template/fixtures/api.ts +46 -0
  84. package/template/fixtures/auth.ts +150 -0
  85. package/template/fixtures/index.ts +38 -0
  86. package/template/fixtures/ui.ts +112 -0
  87. package/template/pages/BasePage.ts +225 -0
  88. package/template/pages/LoginPage.ts +50 -0
  89. package/template/pages/index.ts +2 -0
  90. package/template/playwright.config.ts +71 -0
  91. package/template/templates/gitignore +19 -0
  92. package/template/testData/users.example.json +17 -0
  93. package/template/tests/api/pet.api.ts +60 -0
  94. package/template/tests/example/authSession.spec.ts +34 -0
  95. package/template/tests/example/login.spec.ts +50 -0
  96. package/template/tsconfig.json +33 -0
  97. package/template/utils/apiUtils.ts +224 -0
  98. package/template/utils/dateUtils.ts +158 -0
  99. package/template/utils/index.ts +15 -0
  100. package/template/utils/stringUtils.ts +192 -0
  101. package/template/utils/uiUtils.ts +236 -0
  102. package/template/utils/validationUtils.ts +190 -0
  103. package/template/utils/waitUtils.ts +207 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * String Utility Functions
3
+ * Provides string manipulation and formatting helpers
4
+ */
5
+
6
+ export class StringUtils {
7
+ /**
8
+ * Generate random string
9
+ */
10
+ static generateRandomString(length: number = 10): string {
11
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
12
+ let result = '';
13
+ for (let i = 0; i < length; i++) {
14
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
15
+ }
16
+ return result;
17
+ }
18
+
19
+ /**
20
+ * Generate random alphanumeric string
21
+ */
22
+ static generateRandomAlphanumeric(length: number = 10): string {
23
+ return this.generateRandomString(length);
24
+ }
25
+
26
+ /**
27
+ * Generate random numeric string
28
+ */
29
+ static generateRandomNumeric(length: number = 10): string {
30
+ let result = '';
31
+ for (let i = 0; i < length; i++) {
32
+ result += Math.floor(Math.random() * 10);
33
+ }
34
+ return result;
35
+ }
36
+
37
+ /**
38
+ * Generate random email
39
+ */
40
+ static generateRandomEmail(domain: string = 'example.com'): string {
41
+ const username = this.generateRandomString(10).toLowerCase();
42
+ return `${username}@${domain}`;
43
+ }
44
+
45
+ /**
46
+ * Generate a timestamp-based email, unique per run.
47
+ *
48
+ * @param prefix - Username prefix (default: 'test_user')
49
+ * @param domain - Email domain (default: 'example.com')
50
+ * @param digits - Number of trailing timestamp digits to append (default: 4)
51
+ * @returns Email like "test_user_5821@example.com"
52
+ */
53
+ static generateTimestampEmail(
54
+ prefix: string = 'test_user',
55
+ domain: string = 'example.com',
56
+ digits: number = 4,
57
+ ): string {
58
+ const timestamp = Date.now().toString();
59
+ const suffix = timestamp.slice(-digits);
60
+ return `${prefix}_${suffix}@${domain}`;
61
+ }
62
+
63
+ /**
64
+ * Generate random phone number
65
+ * @param countryCode - Country code (default: '+1')
66
+ * @param prefix - Phone number prefix (default: '555')
67
+ * @param length - Number of random digits after prefix (default: 7)
68
+ * @returns Phone number in format like "+1 5551234567"
69
+ */
70
+ static generateRandomPhoneNumber(
71
+ countryCode: string = '+1',
72
+ prefix: string = '555',
73
+ length: number = 7,
74
+ ): string {
75
+ const randomDigits = this.generateRandomNumeric(length);
76
+ return `${countryCode} ${prefix}${randomDigits}`;
77
+ }
78
+
79
+ /**
80
+ * Generate UUID v4
81
+ */
82
+ static generateUUID(): string {
83
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
84
+ const r = (Math.random() * 16) | 0;
85
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
86
+ return v.toString(16);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Capitalize first letter
92
+ */
93
+ static capitalize(str: string): string {
94
+ return str.charAt(0).toUpperCase() + str.slice(1);
95
+ }
96
+
97
+ /**
98
+ * Convert to title case
99
+ */
100
+ static toTitleCase(str: string): string {
101
+ return str
102
+ .toLowerCase()
103
+ .split(' ')
104
+ .map(word => this.capitalize(word))
105
+ .join(' ');
106
+ }
107
+
108
+ /**
109
+ * Convert to camelCase
110
+ */
111
+ static toCamelCase(str: string): string {
112
+ return str
113
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
114
+ index === 0 ? word.toLowerCase() : word.toUpperCase(),
115
+ )
116
+ .replace(/\s+/g, '');
117
+ }
118
+
119
+ /**
120
+ * Convert to snake_case
121
+ */
122
+ static toSnakeCase(str: string): string {
123
+ return str
124
+ .replace(/\W+/g, ' ')
125
+ .split(/ |\B(?=[A-Z])/)
126
+ .map(word => word.toLowerCase())
127
+ .join('_');
128
+ }
129
+
130
+ /**
131
+ * Convert to kebab-case
132
+ */
133
+ static toKebabCase(str: string): string {
134
+ return str
135
+ .replace(/\W+/g, ' ')
136
+ .split(/ |\B(?=[A-Z])/)
137
+ .map(word => word.toLowerCase())
138
+ .join('-');
139
+ }
140
+
141
+ /**
142
+ * Truncate string to specified length
143
+ */
144
+ static truncate(str: string, length: number, suffix: string = '...'): string {
145
+ if (str.length <= length) return str;
146
+ return str.slice(0, length - suffix.length) + suffix;
147
+ }
148
+
149
+ /**
150
+ * Check if string is empty or whitespace
151
+ */
152
+ static isBlank(str: string | null | undefined): boolean {
153
+ return !str || str.trim().length === 0;
154
+ }
155
+
156
+ /**
157
+ * Remove extra whitespace
158
+ */
159
+ static normalizeWhitespace(str: string): string {
160
+ return str.trim().replace(/\s+/g, ' ');
161
+ }
162
+
163
+ /**
164
+ * Escape HTML special characters
165
+ */
166
+ static escapeHtml(str: string): string {
167
+ const htmlEntities: Record<string, string> = {
168
+ '&': '&amp;',
169
+ '<': '&lt;',
170
+ '>': '&gt;',
171
+ '"': '&quot;',
172
+ "'": '&#39;',
173
+ };
174
+ return str.replace(/[&<>"']/g, char => htmlEntities[char]);
175
+ }
176
+
177
+ /**
178
+ * Extract numbers from string
179
+ */
180
+ static extractNumbers(str: string): string {
181
+ return str.replace(/\D/g, '');
182
+ }
183
+
184
+ /**
185
+ * Mask string (e.g., for sensitive data)
186
+ */
187
+ static mask(str: string, visibleChars: number = 4, maskChar: string = '*'): string {
188
+ if (str.length <= visibleChars) return str;
189
+ const masked = maskChar.repeat(str.length - visibleChars);
190
+ return masked + str.slice(-visibleChars);
191
+ }
192
+ }
@@ -0,0 +1,236 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+
3
+ /**
4
+ * UI Utility Functions for Playwright Tests
5
+ * Provides common UI interaction helpers
6
+ */
7
+
8
+ export class UIUtils {
9
+ /**
10
+ * Scroll to element and make it visible
11
+ */
12
+ static async scrollToElement(element: Locator): Promise<void> {
13
+ await element.scrollIntoViewIfNeeded();
14
+ }
15
+
16
+ static async clickButton(page: Page, button: string): Promise<void> {
17
+ await page.getByRole('button', { name: button }).first().click();
18
+ }
19
+
20
+ /**
21
+ * Click element with retry logic
22
+ */
23
+ static async clickWithRetry(
24
+ element: Locator,
25
+ maxRetries: number = 3,
26
+ delay: number = 1000,
27
+ ): Promise<void> {
28
+ for (let i = 0; i < maxRetries; i++) {
29
+ try {
30
+ await element.click({ timeout: 5000 });
31
+ return;
32
+ } catch (error) {
33
+ if (i === maxRetries - 1) throw error;
34
+ await new Promise(resolve => setTimeout(resolve, delay));
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Force click on element (useful for hidden or overlapped elements)
41
+ */
42
+ static async forceClick(element: Locator): Promise<void> {
43
+ await element.click({ force: true });
44
+ }
45
+
46
+ /**
47
+ * Double click on element
48
+ */
49
+ static async doubleClick(element: Locator): Promise<void> {
50
+ await element.dblclick();
51
+ }
52
+
53
+ /**
54
+ * Right click on element
55
+ */
56
+ static async rightClick(element: Locator): Promise<void> {
57
+ await element.click({ button: 'right' });
58
+ }
59
+
60
+ /**
61
+ * Hover over element
62
+ */
63
+ static async hover(element: Locator): Promise<void> {
64
+ await element.hover();
65
+ }
66
+
67
+ /**
68
+ * Fill input with clearing first
69
+ */
70
+ static async fillInput(element: Locator, text: string): Promise<void> {
71
+ await element.clear();
72
+ await element.fill(text);
73
+ }
74
+
75
+ /**
76
+ * Type text slowly (character by character)
77
+ */
78
+ static async typeSlowly(element: Locator, text: string, delay: number = 100): Promise<void> {
79
+ await element.click();
80
+ await element.pressSequentially(text, { delay });
81
+ }
82
+
83
+ /**
84
+ * Select option from dropdown by value
85
+ */
86
+ static async selectDropdownByValue(element: Locator, value: string): Promise<void> {
87
+ await element.selectOption({ value });
88
+ }
89
+
90
+ /**
91
+ * Select option from dropdown by label
92
+ */
93
+ static async selectDropdownByLabel(element: Locator, label: string): Promise<void> {
94
+ await element.selectOption({ label });
95
+ }
96
+
97
+ /**
98
+ * Select option from dropdown by index
99
+ */
100
+ static async selectDropdownByIndex(element: Locator, index: number): Promise<void> {
101
+ await element.selectOption({ index });
102
+ }
103
+
104
+ /**
105
+ * Check checkbox
106
+ */
107
+ static async checkCheckbox(element: Locator): Promise<void> {
108
+ await element.check();
109
+ }
110
+
111
+ /**
112
+ * Uncheck checkbox
113
+ */
114
+ static async uncheckCheckbox(element: Locator): Promise<void> {
115
+ await element.uncheck();
116
+ }
117
+
118
+ /**
119
+ * Toggle checkbox
120
+ */
121
+ static async toggleCheckbox(element: Locator): Promise<void> {
122
+ const isChecked = await element.isChecked();
123
+ if (isChecked) {
124
+ await element.uncheck();
125
+ } else {
126
+ await element.check();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Get element text content
132
+ */
133
+ static async getText(element: Locator): Promise<string> {
134
+ return (await element.textContent()) || '';
135
+ }
136
+
137
+ /**
138
+ * Get element input value
139
+ */
140
+ static async getValue(element: Locator): Promise<string> {
141
+ return await element.inputValue();
142
+ }
143
+
144
+ /**
145
+ * Check if element is visible
146
+ */
147
+ static async isVisible(element: Locator): Promise<boolean> {
148
+ return await element.isVisible();
149
+ }
150
+
151
+ /**
152
+ * Check if element is enabled
153
+ */
154
+ static async isEnabled(element: Locator): Promise<boolean> {
155
+ return await element.isEnabled();
156
+ }
157
+
158
+ /**
159
+ * Check if element exists in DOM
160
+ */
161
+ static async exists(element: Locator): Promise<boolean> {
162
+ return (await element.count()) > 0;
163
+ }
164
+
165
+ /**
166
+ * Upload file to input
167
+ */
168
+ static async uploadFile(element: Locator, filePath: string | string[]): Promise<void> {
169
+ await element.setInputFiles(filePath);
170
+ }
171
+
172
+ /**
173
+ * Clear file input
174
+ */
175
+ static async clearFileInput(element: Locator): Promise<void> {
176
+ await element.setInputFiles([]);
177
+ }
178
+
179
+ /**
180
+ * Get attribute value
181
+ */
182
+ static async getAttribute(element: Locator, name: string): Promise<string | null> {
183
+ return await element.getAttribute(name);
184
+ }
185
+
186
+ /**
187
+ * Focus element
188
+ */
189
+ static async focus(element: Locator): Promise<void> {
190
+ await element.focus();
191
+ }
192
+
193
+ /**
194
+ * Blur element
195
+ */
196
+ static async blur(element: Locator): Promise<void> {
197
+ await element.blur();
198
+ }
199
+
200
+ /**
201
+ * Press keyboard key on element
202
+ */
203
+ static async pressKey(element: Locator, key: string): Promise<void> {
204
+ await element.press(key);
205
+ }
206
+
207
+ /**
208
+ * Drag and drop element
209
+ */
210
+ static async dragAndDrop(source: Locator, target: Locator): Promise<void> {
211
+ await source.dragTo(target);
212
+ }
213
+
214
+ /**
215
+ * Take screenshot of element
216
+ */
217
+ static async screenshot(element: Locator, path: string): Promise<Buffer> {
218
+ return await element.screenshot({ path });
219
+ }
220
+
221
+ /**
222
+ * Wait for element animation to complete
223
+ */
224
+ static async waitForAnimation(element: Locator, timeout: number = 1000): Promise<void> {
225
+ await element.evaluate((el, t) => {
226
+ return new Promise<void>(resolve => {
227
+ const animations = el.getAnimations();
228
+ if (animations.length === 0) {
229
+ setTimeout(resolve, t);
230
+ } else {
231
+ Promise.all(animations.map(a => a.finished)).then(() => resolve());
232
+ }
233
+ });
234
+ }, timeout);
235
+ }
236
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Validation Utility Functions
3
+ * Provides data validation helpers
4
+ */
5
+
6
+ export class ValidationUtils {
7
+ /**
8
+ * Validate email format
9
+ */
10
+ static isValidEmail(email: string): boolean {
11
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
12
+ return emailRegex.test(email);
13
+ }
14
+
15
+ /**
16
+ * Validate password strength
17
+ */
18
+ static isStrongPassword(
19
+ password: string,
20
+ options?: {
21
+ minLength?: number;
22
+ requireUppercase?: boolean;
23
+ requireLowercase?: boolean;
24
+ requireNumbers?: boolean;
25
+ requireSpecialChars?: boolean;
26
+ },
27
+ ): { valid: boolean; errors: string[] } {
28
+ const {
29
+ minLength = 8,
30
+ requireUppercase = true,
31
+ requireLowercase = true,
32
+ requireNumbers = true,
33
+ requireSpecialChars = true,
34
+ } = options || {};
35
+
36
+ const errors: string[] = [];
37
+
38
+ if (password.length < minLength) {
39
+ errors.push(`Password must be at least ${minLength} characters long`);
40
+ }
41
+
42
+ if (requireUppercase && !/[A-Z]/.test(password)) {
43
+ errors.push('Password must contain at least one uppercase letter');
44
+ }
45
+
46
+ if (requireLowercase && !/[a-z]/.test(password)) {
47
+ errors.push('Password must contain at least one lowercase letter');
48
+ }
49
+
50
+ if (requireNumbers && !/\d/.test(password)) {
51
+ errors.push('Password must contain at least one number');
52
+ }
53
+
54
+ if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
55
+ errors.push('Password must contain at least one special character');
56
+ }
57
+
58
+ return {
59
+ valid: errors.length === 0,
60
+ errors,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Validate phone number
66
+ */
67
+ static isValidPhoneNumber(phone: string, countryCode?: string): boolean {
68
+ // Remove all non-numeric characters
69
+ const cleaned = phone.replace(/\D/g, '');
70
+
71
+ // Basic validation (10-15 digits)
72
+ if (cleaned.length < 10 || cleaned.length > 15) {
73
+ return false;
74
+ }
75
+
76
+ // Country-specific validation
77
+ if (countryCode === 'US') {
78
+ return /^1?\d{10}$/.test(cleaned);
79
+ }
80
+
81
+ return true;
82
+ }
83
+
84
+ /**
85
+ * Validate URL format
86
+ */
87
+ static isValidURL(url: string): boolean {
88
+ try {
89
+ const urlObj = new URL(url);
90
+ return ['http:', 'https:'].includes(urlObj.protocol);
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Validate credit card number (Luhn algorithm)
98
+ */
99
+ static isValidCreditCard(cardNumber: string): boolean {
100
+ const cleaned = cardNumber.replace(/\D/g, '');
101
+
102
+ if (cleaned.length < 13 || cleaned.length > 19) {
103
+ return false;
104
+ }
105
+
106
+ let sum = 0;
107
+ let isEven = false;
108
+
109
+ for (let i = cleaned.length - 1; i >= 0; i--) {
110
+ let digit = parseInt(cleaned[i], 10);
111
+
112
+ if (isEven) {
113
+ digit *= 2;
114
+ if (digit > 9) {
115
+ digit -= 9;
116
+ }
117
+ }
118
+
119
+ sum += digit;
120
+ isEven = !isEven;
121
+ }
122
+
123
+ return sum % 10 === 0;
124
+ }
125
+
126
+ /**
127
+ * Validate date string
128
+ */
129
+ static isValidDate(dateString: string): boolean {
130
+ const date = new Date(dateString);
131
+ return !isNaN(date.getTime());
132
+ }
133
+
134
+ /**
135
+ * Validate JSON string
136
+ */
137
+ static isValidJSON(str: string): boolean {
138
+ try {
139
+ JSON.parse(str);
140
+ return true;
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Validate UUID format
148
+ */
149
+ static isValidUUID(uuid: string): boolean {
150
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
151
+ return uuidRegex.test(uuid);
152
+ }
153
+
154
+ /**
155
+ * Validate IP address (v4)
156
+ */
157
+ static isValidIPv4(ip: string): boolean {
158
+ const ipv4Regex =
159
+ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
160
+ return ipv4Regex.test(ip);
161
+ }
162
+
163
+ /**
164
+ * Check if value is within range
165
+ */
166
+ static isInRange(value: number, min: number, max: number): boolean {
167
+ return value >= min && value <= max;
168
+ }
169
+
170
+ /**
171
+ * Check if string matches pattern
172
+ */
173
+ static matchesPattern(str: string, pattern: RegExp): boolean {
174
+ return pattern.test(str);
175
+ }
176
+
177
+ /**
178
+ * Validate array is not empty
179
+ */
180
+ static isNonEmptyArray(arr: unknown): boolean {
181
+ return Array.isArray(arr) && arr.length > 0;
182
+ }
183
+
184
+ /**
185
+ * Validate object has required properties
186
+ */
187
+ static hasRequiredProperties(obj: Record<string, unknown>, requiredProps: string[]): boolean {
188
+ return requiredProps.every(prop => prop in obj && obj[prop] !== undefined);
189
+ }
190
+ }