@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,60 @@
1
+ import { expect, test } from '@fixtures';
2
+
3
+ import type { Pet } from '@api/models/pet';
4
+
5
+ // Layer 3 — the test layer. Tests speak business language via the service (petService) and, where
6
+ // useful, drop to the base client (apiClient) to assert on the raw HTTP shape. Neither touches
7
+ // Playwright's request context directly. baseURL comes from the `api` project (API_BASE_URL).
8
+
9
+ test.describe('Petstore /pet API', () => {
10
+ test('findAvailable returns only available pets', async ({ petService }) => {
11
+ const pets = await petService.findAvailable();
12
+
13
+ expect(pets.length).toBeGreaterThan(0);
14
+ expect(pets.every(pet => pet.status === 'available')).toBeTruthy();
15
+ });
16
+
17
+ test('service-layer filter: available pets by category all carry that category', async ({
18
+ petService,
19
+ }) => {
20
+ const category = 'Dogs';
21
+ const all = await petService.findAvailable();
22
+ const dogs = await petService.findAvailableByCategory(category);
23
+
24
+ // The endpoint has no category filter — the service derived this by reading then filtering.
25
+ expect(all.length).toBeGreaterThan(0);
26
+ expect(dogs.length).toBeLessThanOrEqual(all.length);
27
+ expect(dogs.every(pet => pet.category?.name === category)).toBeTruthy();
28
+ });
29
+
30
+ test('CRUD: create → read back → delete', async ({ petService }) => {
31
+ const petId = Date.now(); // unique per run, within JS safe-integer range
32
+ const draft: Pet = {
33
+ id: petId,
34
+ name: 'claude-e2e',
35
+ photoUrls: ['https://example.com/dog.png'],
36
+ category: { id: 1, name: 'Dogs' },
37
+ tags: [{ id: 1, name: 'e2e' }],
38
+ status: 'available',
39
+ };
40
+
41
+ const created = await petService.create(draft);
42
+ expect(created.id).toBe(petId);
43
+ expect(created.name).toBe(draft.name);
44
+
45
+ const fetched = await petService.getById(petId);
46
+ expect(fetched.name).toBe(draft.name);
47
+ expect(fetched.status).toBe('available');
48
+
49
+ const deleteStatus = await petService.deleteById(petId);
50
+ expect(deleteStatus).toBe(200);
51
+ });
52
+
53
+ test('base client: raw findByStatus response shape', async ({ apiClient }) => {
54
+ const res = await apiClient.get<Pet[]>('/pet/findByStatus', { params: { status: 'pending' } });
55
+
56
+ expect(res.ok).toBeTruthy();
57
+ expect(res.status).toBe(200);
58
+ expect(Array.isArray(res.data)).toBeTruthy();
59
+ });
60
+ });
@@ -0,0 +1,34 @@
1
+ import { expect, test } from '@fixtures';
2
+
3
+ /**
4
+ * RUNNABLE DEMO of lazy, cached session auth.
5
+ *
6
+ * `test.use({ session: 'demoUser' })` triggers a one-time login to saucedemo.com (see
7
+ * fixtures/auth.ts → ensureSession) which caches `.auth/demoUser.json`; every later test and run
8
+ * reuses that file — no repeated logins, no setup project.
9
+ *
10
+ * Run: `npx playwright test tests/example/authSession.spec.ts --project=chromium`
11
+ *
12
+ * NOTE: each session needs its own describe with a single `test.use({ session })` — two `test.use`
13
+ * calls in one describe do not create two scopes; the last one wins for every test in that describe.
14
+ */
15
+ const INVENTORY_URL = 'https://www.saucedemo.com/inventory.html';
16
+
17
+ test.describe('with the "demoUser" session', () => {
18
+ test.use({ session: 'demoUser' });
19
+
20
+ test('reaches the inventory without logging in again', async ({ page }) => {
21
+ await page.goto(INVENTORY_URL);
22
+
23
+ await expect(page).toHaveURL(/\/inventory\.html/);
24
+ await expect(page.locator('.title')).toHaveText('Products');
25
+ });
26
+ });
27
+
28
+ test.describe('without a session', () => {
29
+ test('is bounced to the login page', async ({ page }) => {
30
+ await page.goto(INVENTORY_URL);
31
+
32
+ await expect(page).toHaveURL('https://www.saucedemo.com/');
33
+ });
34
+ });
@@ -0,0 +1,50 @@
1
+ import { expect, test } from '@fixtures';
2
+ import { LoginPage } from '@pages/LoginPage';
3
+
4
+ test.describe('Login page', () => {
5
+ test('renders the login form', async ({ page }) => {
6
+ const loginPage = new LoginPage(page);
7
+ await loginPage.goto();
8
+
9
+ expect(await loginPage.isFormVisible()).toBeTruthy();
10
+ });
11
+
12
+ test('shows an error for invalid credentials', async ({ page }) => {
13
+ const loginPage = new LoginPage(page);
14
+ await loginPage.goto();
15
+
16
+ await loginPage.login('invalid@example.com', 'wrongpassword');
17
+ await expect(loginPage.errorMessage).toBeVisible();
18
+ });
19
+ });
20
+
21
+ // Suite-level auth: `test.use({ session })` at describe scope logs in once (lazily) and applies the
22
+ // cached storageState to every test in this group.
23
+ test.describe('Suite-level auth', () => {
24
+ test.use({ session: 'adminUser' });
25
+
26
+ test('reaches a protected page without redirecting to login', async ({ page }) => {
27
+ // baseURL is saucedemo.com; /inventory.html is gated — unauthenticated visits bounce to '/'.
28
+ await page.goto('/inventory.html');
29
+ await expect(page).toHaveURL(/\/inventory\.html/);
30
+ });
31
+ });
32
+
33
+ // Test-level auth: `test.as(session)` authenticates a single test. Different tests can use different
34
+ // sessions in the same describe, and the declaration modifiers (.skip/.only/.fixme/.fail) work too.
35
+ test.describe('Test-level auth (test.as)', () => {
36
+ test.as('adminUser')('admin reaches the inventory', async ({ page }) => {
37
+ await page.goto('/inventory.html');
38
+ await expect(page).toHaveURL(/\/inventory\.html/);
39
+ });
40
+
41
+ test.as('customerUser')('customer reaches the inventory', async ({ page }) => {
42
+ await page.goto('/inventory.html');
43
+ await expect(page).toHaveURL(/\/inventory\.html/);
44
+ });
45
+
46
+ test.as('adminUser').fixme('admin flow pending a fix', async ({ page }) => {
47
+ await page.goto('/inventory.html');
48
+ await expect(page).toHaveURL(/\/broken/);
49
+ });
50
+ });
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+
8
+ "types": ["node", "@playwright/test"],
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+
12
+ "resolveJsonModule": true,
13
+ "esModuleInterop": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "strict": true,
16
+ "skipLibCheck": true,
17
+
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@api/*": ["api/*"],
21
+ "@pages/*": ["pages/*"],
22
+ "@config/*": ["config/*"],
23
+ "@utils/*": ["utils/*"],
24
+ "@fixtures": ["fixtures/index.ts"],
25
+ "@fixtures/*": ["fixtures/*"],
26
+ "@tests/*": ["tests/*"],
27
+ "@env/*": ["env/*"],
28
+ "@testData/*": ["testData/*"]
29
+ }
30
+ },
31
+ "include": ["**/*.ts"],
32
+ "exclude": ["node_modules", "dist", "playwright-report", "test-results", "allure-results"]
33
+ }
@@ -0,0 +1,224 @@
1
+ import { APIRequestContext, APIResponse, request } from '@playwright/test';
2
+
3
+ /**
4
+ * API Utility Functions for Playwright Tests
5
+ * Provides common API interaction helpers
6
+ */
7
+
8
+ export interface RequestOptions {
9
+ headers?: Record<string, string>;
10
+ data?: unknown;
11
+ params?: Record<string, string | number | boolean>;
12
+ timeout?: number;
13
+ }
14
+
15
+ export interface APIConfig {
16
+ baseURL: string;
17
+ headers?: Record<string, string>;
18
+ timeout?: number;
19
+ }
20
+
21
+ export class APIUtils {
22
+ private apiContext: APIRequestContext | null = null;
23
+ private config: APIConfig;
24
+
25
+ constructor(config: APIConfig) {
26
+ this.config = config;
27
+ }
28
+
29
+ /**
30
+ * Initialize API context
31
+ * Note: baseURL is NOT set in context to avoid automatic "/" insertion
32
+ * URLs will be manually concatenated in request methods
33
+ */
34
+ async init(): Promise<void> {
35
+ this.apiContext = await request.newContext({
36
+ extraHTTPHeaders: this.config.headers || {},
37
+ timeout: this.config.timeout || 30000,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Dispose API context
43
+ */
44
+ async dispose(): Promise<void> {
45
+ if (this.apiContext) {
46
+ await this.apiContext.dispose();
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Set authorization token
52
+ */
53
+ setAuthToken(token: string, type: 'Bearer' | 'Basic' = 'Bearer'): void {
54
+ if (!this.config.headers) {
55
+ this.config.headers = {};
56
+ }
57
+ this.config.headers['Authorization'] = `${type} ${token}`;
58
+ }
59
+
60
+ /**
61
+ * Get API context
62
+ */
63
+ getContext(): APIRequestContext {
64
+ if (!this.apiContext) {
65
+ throw new Error('API context not initialized. Call init() first.');
66
+ }
67
+ return this.apiContext;
68
+ }
69
+
70
+ /**
71
+ * Build full URL by concatenating baseURL and path
72
+ * @param path - Relative path or full URL
73
+ * @returns Full URL
74
+ */
75
+ private buildUrl(path: string): string {
76
+ // If path is already a full URL, return it as is
77
+ if (path.startsWith('http://') || path.startsWith('https://')) {
78
+ return path;
79
+ }
80
+ // Direct concatenation without adding "/"
81
+ return `${this.config.baseURL}${path}`;
82
+ }
83
+
84
+ /**
85
+ * Make GET request
86
+ */
87
+ async get(url: string, options?: RequestOptions): Promise<APIResponse> {
88
+ const context = this.getContext();
89
+ const fullUrl = this.buildUrl(url);
90
+ return await context.get(fullUrl, {
91
+ headers: options?.headers,
92
+ params: options?.params as Record<string, string | number | boolean>,
93
+ timeout: options?.timeout,
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Make POST request
99
+ */
100
+ async post(url: string, options?: RequestOptions): Promise<APIResponse> {
101
+ const context = this.getContext();
102
+ const fullUrl = this.buildUrl(url);
103
+ return await context.post(fullUrl, {
104
+ headers: options?.headers,
105
+ data: options?.data,
106
+ params: options?.params as Record<string, string | number | boolean>,
107
+ timeout: options?.timeout,
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Make PUT request
113
+ */
114
+ async put(url: string, options?: RequestOptions): Promise<APIResponse> {
115
+ const context = this.getContext();
116
+ const fullUrl = this.buildUrl(url);
117
+ return await context.put(fullUrl, {
118
+ headers: options?.headers,
119
+ data: options?.data,
120
+ params: options?.params as Record<string, string | number | boolean>,
121
+ timeout: options?.timeout,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Make PATCH request
127
+ */
128
+ async patch(url: string, options?: RequestOptions): Promise<APIResponse> {
129
+ const context = this.getContext();
130
+ const fullUrl = this.buildUrl(url);
131
+ return await context.patch(fullUrl, {
132
+ headers: options?.headers,
133
+ data: options?.data,
134
+ params: options?.params as Record<string, string | number | boolean>,
135
+ timeout: options?.timeout,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Make DELETE request
141
+ */
142
+ async delete(url: string, options?: RequestOptions): Promise<APIResponse> {
143
+ const context = this.getContext();
144
+ const fullUrl = this.buildUrl(url);
145
+ return await context.delete(fullUrl, {
146
+ headers: options?.headers,
147
+ data: options?.data,
148
+ params: options?.params as Record<string, string | number | boolean>,
149
+ timeout: options?.timeout,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Make HEAD request
155
+ */
156
+ async head(url: string, options?: RequestOptions): Promise<APIResponse> {
157
+ const context = this.getContext();
158
+ const fullUrl = this.buildUrl(url);
159
+ return await context.head(fullUrl, {
160
+ headers: options?.headers,
161
+ params: options?.params as Record<string, string | number | boolean>,
162
+ timeout: options?.timeout,
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Parse JSON response
168
+ */
169
+ static async parseJSON<T = unknown>(response: APIResponse): Promise<T> {
170
+ return await response.json();
171
+ }
172
+
173
+ /**
174
+ * Get response text
175
+ */
176
+ static async getText(response: APIResponse): Promise<string> {
177
+ return await response.text();
178
+ }
179
+
180
+ /**
181
+ * Get response body as buffer
182
+ */
183
+ static async getBody(response: APIResponse): Promise<Buffer> {
184
+ return await response.body();
185
+ }
186
+
187
+ /**
188
+ * Get response headers
189
+ */
190
+ static getHeaders(response: APIResponse): Record<string, string> {
191
+ return response.headers();
192
+ }
193
+
194
+ /**
195
+ * Get response status code
196
+ */
197
+ static getStatus(response: APIResponse): number {
198
+ return response.status();
199
+ }
200
+
201
+ /**
202
+ * Check if response is successful (2xx)
203
+ */
204
+ static isSuccess(response: APIResponse): boolean {
205
+ const status = response.status();
206
+ return status >= 200 && status < 300;
207
+ }
208
+
209
+ /**
210
+ * Check if response is client error (4xx)
211
+ */
212
+ static isClientError(response: APIResponse): boolean {
213
+ const status = response.status();
214
+ return status >= 400 && status < 500;
215
+ }
216
+
217
+ /**
218
+ * Check if response is server error (5xx)
219
+ */
220
+ static isServerError(response: APIResponse): boolean {
221
+ const status = response.status();
222
+ return status >= 500;
223
+ }
224
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Date and Time Utility Functions
3
+ * Provides date manipulation and formatting helpers
4
+ */
5
+
6
+ export class DateUtils {
7
+ /**
8
+ * Get current date in ISO format
9
+ */
10
+ static getCurrentDate(): string {
11
+ return new Date().toISOString().split('T')[0];
12
+ }
13
+
14
+ /**
15
+ * Get today's date formatted (YYYY-MM-DD or custom format)
16
+ */
17
+ static getTodayFormatted(format: string = 'MM/DD/YYYY'): string {
18
+ return this.formatDate(new Date(), format);
19
+ }
20
+
21
+ /**
22
+ * Get current timestamp
23
+ */
24
+ static getCurrentTimestamp(): number {
25
+ return Date.now();
26
+ }
27
+
28
+ /**
29
+ * Format date to specific format (uses UTC time)
30
+ * M/D = single digit (3/5), MM/DD = with leading zeros (03/05)
31
+ * h = 12-hour format (9), HH = 24-hour format (21)
32
+ * Example: "M/D/YYYY h:mm A" -> "3/20/2026 9:04 PM"
33
+ */
34
+ static formatDate(date: Date, format: string = 'MM/DD/YYYY'): string {
35
+ const year = date.getUTCFullYear();
36
+ const month = date.getUTCMonth() + 1;
37
+ const day = date.getUTCDate();
38
+
39
+ return format
40
+ .replace('YYYY', String(year))
41
+ .replace('MM', String(month).padStart(2, '0'))
42
+ .replace('DD', String(day).padStart(2, '0'))
43
+ .replace('M', String(month))
44
+ .replace('D', String(day));
45
+ }
46
+
47
+ /**
48
+ * Add days to date
49
+ */
50
+ static addDays(date: Date, days: number): Date {
51
+ const result = new Date(date);
52
+ result.setDate(result.getDate() + days);
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Subtract days from date
58
+ */
59
+ static subtractDays(date: Date, days: number): Date {
60
+ return this.addDays(date, -days);
61
+ }
62
+
63
+ /**
64
+ * Add months to date
65
+ */
66
+ static addMonths(date: Date, months: number): Date {
67
+ const result = new Date(date);
68
+ result.setMonth(result.getMonth() + months);
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * Add years to date
74
+ */
75
+ static addYears(date: Date, years: number): Date {
76
+ const result = new Date(date);
77
+ result.setFullYear(result.getFullYear() + years);
78
+ return result;
79
+ }
80
+
81
+ /**
82
+ * Get date difference in days
83
+ */
84
+ static getDaysDifference(date1: Date, date2: Date): number {
85
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
86
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
87
+ }
88
+
89
+ /**
90
+ * Check if date is today
91
+ */
92
+ static isToday(date: Date): boolean {
93
+ const today = new Date();
94
+ return (
95
+ date.getDate() === today.getDate() &&
96
+ date.getMonth() === today.getMonth() &&
97
+ date.getFullYear() === today.getFullYear()
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Check if date is in the past
103
+ */
104
+ static isPast(date: Date): boolean {
105
+ return date < new Date();
106
+ }
107
+
108
+ /**
109
+ * Check if date is in the future
110
+ */
111
+ static isFuture(date: Date): boolean {
112
+ return date > new Date();
113
+ }
114
+
115
+ /**
116
+ * Get start of day
117
+ */
118
+ static startOfDay(date: Date): Date {
119
+ const result = new Date(date);
120
+ result.setHours(0, 0, 0, 0);
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Get end of day
126
+ */
127
+ static endOfDay(date: Date): Date {
128
+ const result = new Date(date);
129
+ result.setHours(23, 59, 59, 999);
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Parse date string to Date object
135
+ */
136
+ static parseDate(dateString: string): Date {
137
+ return new Date(dateString);
138
+ }
139
+
140
+ /**
141
+ * Get relative time string (e.g., "2 days ago")
142
+ */
143
+ static getRelativeTime(date: Date): string {
144
+ const now = new Date();
145
+ const diffMs = now.getTime() - date.getTime();
146
+ const diffSeconds = Math.floor(diffMs / 1000);
147
+ const diffMinutes = Math.floor(diffSeconds / 60);
148
+ const diffHours = Math.floor(diffMinutes / 60);
149
+ const diffDays = Math.floor(diffHours / 24);
150
+
151
+ if (diffSeconds < 60) return 'just now';
152
+ if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`;
153
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
154
+ if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
155
+
156
+ return this.formatDate(date);
157
+ }
158
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Generic test helper classes — string/date/validation/wait/UI/API utilities used across specs.
3
+ * Import from `@utils`. These are plain static helpers with no external dependencies; extend or trim
4
+ * them for your app.
5
+ *
6
+ * @example
7
+ * import { StringUtils } from '@utils';
8
+ * const slug = StringUtils.slugify('Hello World');
9
+ */
10
+ export * from './apiUtils';
11
+ export * from './dateUtils';
12
+ export * from './stringUtils';
13
+ export * from './uiUtils';
14
+ export * from './validationUtils';
15
+ export * from './waitUtils';