@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.
- package/core-manifest.json +50 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +9 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +114 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/remove.d.ts +7 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +9 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/injectors/assets.d.ts +6 -0
- package/dist/injectors/assets.d.ts.map +1 -0
- package/dist/injectors/assets.js +53 -0
- package/dist/injectors/assets.js.map +1 -0
- package/dist/injectors/envJson.d.ts +6 -0
- package/dist/injectors/envJson.d.ts.map +1 -0
- package/dist/injectors/envJson.js +39 -0
- package/dist/injectors/envJson.js.map +1 -0
- package/dist/injectors/fixturesBarrel.d.ts +11 -0
- package/dist/injectors/fixturesBarrel.d.ts.map +1 -0
- package/dist/injectors/fixturesBarrel.js +66 -0
- package/dist/injectors/fixturesBarrel.js.map +1 -0
- package/dist/injectors/packageJson.d.ts +6 -0
- package/dist/injectors/packageJson.d.ts.map +1 -0
- package/dist/injectors/packageJson.js +25 -0
- package/dist/injectors/packageJson.js.map +1 -0
- package/dist/injectors/pwConfig.d.ts +10 -0
- package/dist/injectors/pwConfig.d.ts.map +1 -0
- package/dist/injectors/pwConfig.js +40 -0
- package/dist/injectors/pwConfig.js.map +1 -0
- package/dist/manifest.d.ts +65 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +22 -0
- package/dist/manifest.js.map +1 -0
- package/dist/plugin-apply.d.ts +13 -0
- package/dist/plugin-apply.d.ts.map +1 -0
- package/dist/plugin-apply.js +89 -0
- package/dist/plugin-apply.js.map +1 -0
- package/dist/prompts.d.ts +22 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +65 -0
- package/dist/prompts.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +34 -0
- package/dist/registry.js.map +1 -0
- package/dist/util/fs.d.ts +13 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +32 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/log.d.ts +8 -0
- package/dist/util/log.d.ts.map +1 -0
- package/dist/util/log.js +9 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/markers.d.ts +20 -0
- package/dist/util/markers.d.ts.map +1 -0
- package/dist/util/markers.js +51 -0
- package/dist/util/markers.js.map +1 -0
- package/dist/util/run.d.ts +8 -0
- package/dist/util/run.d.ts.map +1 -0
- package/dist/util/run.js +27 -0
- package/dist/util/run.js.map +1 -0
- package/package.json +31 -0
- package/template/.commitlintrc.json +3 -0
- package/template/.prettierrc +17 -0
- package/template/api/core/ApiClient.ts +99 -0
- package/template/api/core/types.ts +29 -0
- package/template/api/index.ts +4 -0
- package/template/api/models/pet.ts +24 -0
- package/template/api/services/PetService.ts +67 -0
- package/template/config/envUtils.ts +70 -0
- package/template/config/index.ts +2 -0
- package/template/config/loadEnv.ts +110 -0
- package/template/env/environments.example.json +20 -0
- package/template/eslint.config.js +74 -0
- package/template/fixtures/api.ts +46 -0
- package/template/fixtures/auth.ts +150 -0
- package/template/fixtures/index.ts +38 -0
- package/template/fixtures/ui.ts +112 -0
- package/template/pages/BasePage.ts +225 -0
- package/template/pages/LoginPage.ts +50 -0
- package/template/pages/index.ts +2 -0
- package/template/playwright.config.ts +71 -0
- package/template/templates/gitignore +19 -0
- package/template/testData/users.example.json +17 -0
- package/template/tests/api/pet.api.ts +60 -0
- package/template/tests/example/authSession.spec.ts +34 -0
- package/template/tests/example/login.spec.ts +50 -0
- package/template/tsconfig.json +33 -0
- package/template/utils/apiUtils.ts +224 -0
- package/template/utils/dateUtils.ts +158 -0
- package/template/utils/index.ts +15 -0
- package/template/utils/stringUtils.ts +192 -0
- package/template/utils/uiUtils.ts +236 -0
- package/template/utils/validationUtils.ts +190 -0
- 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';
|