@keber/qa-framework 1.9.1 → 1.11.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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * fixtures/base.ts
3
+ *
4
+ * Playwright test wrapper with TTL-aware session refresh.
5
+ * Import this file instead of `@playwright/test` when the app uses a
6
+ * server-side session that may expire during long runs.
7
+ */
8
+
9
+ import { test as base, devices } from '@playwright/test';
10
+ import { SESSION_TTL_MS } from '../playwright.config';
11
+ import * as dotenv from 'dotenv';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ dotenv.config();
16
+
17
+ const authTimestamps = new Map<string, number>();
18
+
19
+ function stateFileForProject(storageState: unknown): string {
20
+ if (typeof storageState === 'string' && storageState) {
21
+ return path.resolve(__dirname, '..', storageState);
22
+ }
23
+ return path.resolve(__dirname, '../.auth/user-default.json');
24
+ }
25
+
26
+ function credentialsForState(stateFile: string): { email: string; password: string } {
27
+ if (stateFile.includes('user-2')) {
28
+ return {
29
+ email: process.env.QA_USER2_EMAIL ?? '',
30
+ password: process.env.QA_USER2_PASSWORD ?? '',
31
+ };
32
+ }
33
+
34
+ return {
35
+ email: process.env.QA_USER_EMAIL ?? '',
36
+ password: process.env.QA_USER_PASSWORD ?? '',
37
+ };
38
+ }
39
+
40
+ function lastAuthTime(stateFile: string): number {
41
+ if (!authTimestamps.has(stateFile)) {
42
+ try {
43
+ authTimestamps.set(stateFile, fs.statSync(stateFile).mtimeMs);
44
+ } catch {
45
+ authTimestamps.set(stateFile, 0);
46
+ }
47
+ }
48
+ return authTimestamps.get(stateFile) ?? 0;
49
+ }
50
+
51
+ async function reauth(browser: import('@playwright/test').Browser, stateFile: string): Promise<void> {
52
+ const { email, password } = credentialsForState(stateFile);
53
+ const baseURL = process.env.QA_BASE_URL ?? '';
54
+ const loginPath = process.env.QA_LOGIN_PATH ?? '/login';
55
+ const emailSelector = process.env.QA_LOGIN_EMAIL_SELECTOR ?? 'input[type="email"]';
56
+ const passwordSelector = process.env.QA_LOGIN_PASSWORD_SELECTOR ?? 'input[type="password"]';
57
+ const submitSelector = process.env.QA_LOGIN_SUBMIT_SELECTOR ?? 'button[type="submit"]';
58
+ const successSelector = process.env.QA_LOGIN_SUCCESS_SELECTOR ?? '.dashboard, .main-content, [data-testid="app-shell"]';
59
+
60
+ const context = await browser.newContext({ ...devices['Desktop Chrome'], baseURL });
61
+ const page = await context.newPage();
62
+
63
+ try {
64
+ await page.goto(loginPath, { waitUntil: 'load', timeout: 60_000 });
65
+ await page.locator(emailSelector).fill(email);
66
+ await page.locator(passwordSelector).fill(password);
67
+ await page.locator(submitSelector).click();
68
+ await page.waitForSelector(successSelector, { timeout: 30_000 });
69
+
70
+ const dir = path.dirname(stateFile);
71
+ if (!fs.existsSync(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ }
74
+
75
+ await context.storageState({ path: stateFile });
76
+ authTimestamps.set(stateFile, Date.now());
77
+ console.log(`[session-refresh] storageState renewed: ${stateFile}`);
78
+ } finally {
79
+ await context.close();
80
+ }
81
+ }
82
+
83
+ export const test = base.extend<{ authStatePath: string }>({
84
+ authStatePath: async ({}, use, testInfo) => {
85
+ await use(stateFileForProject(testInfo.project.use.storageState));
86
+ },
87
+
88
+ context: async ({ browser }, use, testInfo) => {
89
+ const stateFile = stateFileForProject(testInfo.project.use.storageState);
90
+ if (Date.now() - lastAuthTime(stateFile) > SESSION_TTL_MS) {
91
+ await reauth(browser, stateFile).catch((error) => {
92
+ console.error(`[session-refresh] reauth failed: ${(error as Error).message}`);
93
+ });
94
+ }
95
+
96
+ const context = await browser.newContext({
97
+ ...devices['Desktop Chrome'],
98
+ storageState: stateFile,
99
+ baseURL: process.env.QA_BASE_URL,
100
+ });
101
+
102
+ await use(context);
103
+ await context.close();
104
+ },
105
+ });
106
+
107
+ export { expect } from '@playwright/test';
108
+ export type { Page, Browser, BrowserContext } from '@playwright/test';
@@ -30,6 +30,74 @@ import * as fs from 'fs';
30
30
 
31
31
  dotenv.config();
32
32
 
33
+ function resolveVar(param: string | undefined, envKey: string, defaultValue: string): [string, string] {
34
+ if (param !== undefined) {
35
+ return [param, 'param'];
36
+ }
37
+ if (process.env[envKey] !== undefined) {
38
+ return [process.env[envKey]!, `env:${envKey}`];
39
+ }
40
+ return [defaultValue, 'default'];
41
+ }
42
+
43
+ function resolveAuthUsers(config: FullConfig): Set<'1' | '2'> {
44
+ const explicit = process.env.QA_AUTH_USER;
45
+ if (explicit) {
46
+ const resolved = new Set<'1' | '2'>();
47
+ for (const token of explicit.split(',').map((value) => value.trim())) {
48
+ if (token === '1' || token === '2') {
49
+ resolved.add(token);
50
+ }
51
+ }
52
+ if (resolved.size) {
53
+ return resolved;
54
+ }
55
+ }
56
+
57
+ const fromProjects = new Set<'1' | '2'>();
58
+ for (const project of config.projects) {
59
+ const storageState = (project.use as { storageState?: unknown } | undefined)?.storageState;
60
+ if (typeof storageState === 'string') {
61
+ if (storageState.includes('user-2')) {
62
+ fromProjects.add('2');
63
+ } else if (storageState.includes('user-1')) {
64
+ fromProjects.add('1');
65
+ }
66
+ }
67
+ }
68
+
69
+ if (fromProjects.size) {
70
+ return fromProjects;
71
+ }
72
+
73
+ return new Set(['1']);
74
+ }
75
+
76
+ async function dismissOnboardingFlow(page: import('@playwright/test').Page): Promise<void> {
77
+ // Override this with your app's onboarding dismissal logic.
78
+ // Use QA_DISMISS_ONBOARDING_LABELS (comma-separated button labels) to enable
79
+ // without modifying this file. Example: QA_DISMISS_ONBOARDING_LABELS=Skip,Got it
80
+ const raw = process.env.QA_DISMISS_ONBOARDING_LABELS ?? '';
81
+ const labels = raw.split(',').map((l: string) => l.trim()).filter(Boolean);
82
+ if (!labels.length) return;
83
+
84
+ for (let round = 0; round < 3; round++) {
85
+ let dismissed = false;
86
+ for (const label of labels) {
87
+ const button = page.getByRole('button', { name: label, exact: true }).first();
88
+ const visible = await button.isVisible({ timeout: 1_000 }).catch(() => false);
89
+ if (visible) {
90
+ await button.click({ timeout: 2_000 }).catch(() => {});
91
+ await page.waitForTimeout(250);
92
+ dismissed = true;
93
+ }
94
+ }
95
+ if (!dismissed) {
96
+ break;
97
+ }
98
+ }
99
+ }
100
+
33
101
  /** Perform login and persist storageState to disk. */
34
102
  async function loginAs(params: {
35
103
  email: string;
@@ -59,11 +127,33 @@ async function loginAs(params: {
59
127
  const page = await context.newPage();
60
128
 
61
129
  try {
62
- await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'domcontentloaded' });
63
- await page.locator(emailSelector).fill(email);
64
- await page.locator(passwordSelector).fill(password);
65
- await page.locator(submitSelector).click();
66
- await page.waitForSelector(successSelector, { timeout: 15_000 });
130
+ const loginUrl = `${baseURL}${loginPath.startsWith('/') ? loginPath : `/${loginPath}`}`;
131
+ let signedIn = false;
132
+
133
+ for (let attempt = 1; attempt <= 3 && !signedIn; attempt++) {
134
+ await page.goto(loginUrl, { waitUntil: 'load', timeout: 60_000 });
135
+ await page.locator(emailSelector).fill(email);
136
+ await page.locator(passwordSelector).fill(password);
137
+ await page.locator(submitSelector).click();
138
+
139
+ const result = await Promise.race([
140
+ page.waitForSelector(successSelector, { timeout: 30_000 }).then(() => 'ok' as const),
141
+ page.locator('text=An unhandled error has occurred.').waitFor({ state: 'visible', timeout: 30_000 }).then(() => 'crash' as const),
142
+ ]).catch(() => 'timeout' as const);
143
+
144
+ if (result === 'ok') {
145
+ signedIn = true;
146
+ } else if (result === 'crash') {
147
+ await page.getByText('Reload').click({ timeout: 5_000 }).catch(() => {});
148
+ await page.waitForTimeout(2_000);
149
+ }
150
+ }
151
+
152
+ if (!signedIn) {
153
+ throw new Error('[qa-framework] Login did not reach the success selector after 3 attempts.');
154
+ }
155
+
156
+ await dismissOnboardingFlow(page);
67
157
 
68
158
  // Ensure .auth/ directory exists
69
159
  const dir = path.dirname(stateFile);
@@ -78,29 +168,29 @@ async function loginAs(params: {
78
168
 
79
169
  export default async function globalSetup(_config: FullConfig): Promise<void> {
80
170
  const baseURL = process.env.QA_BASE_URL!;
81
- const email = process.env.QA_USER_EMAIL!;
82
- const password = process.env.QA_USER_PASSWORD!;
171
+ const authUsers = resolveAuthUsers(_config);
83
172
 
84
- // --- Default role login ---
85
- await loginAs({
86
- email,
87
- password,
88
- baseURL,
89
- stateFile: '.auth/user-default.json',
90
- });
91
-
92
- // --- Additional roles (uncomment and adapt as needed) ---
93
- // await loginAs({
94
- // email: process.env.QA_ADMIN_EMAIL!,
95
- // password: process.env.QA_ADMIN_PASSWORD!,
96
- // baseURL,
97
- // stateFile: '.auth/user-admin.json',
98
- // });
99
- //
100
- // await loginAs({
101
- // email: process.env.QA_READONLY_EMAIL!,
102
- // password: process.env.QA_READONLY_PASSWORD!,
103
- // baseURL,
104
- // stateFile: '.auth/user-readonly.json',
105
- // });
173
+ if (authUsers.has('1')) {
174
+ const [email, emailSource] = resolveVar(process.env.QA_USER_EMAIL, 'QA_USER_EMAIL', 'qa-user@example.com');
175
+ const [password, passwordSource] = resolveVar(process.env.QA_USER_PASSWORD, 'QA_USER_PASSWORD', 'CHANGE_ME');
176
+ console.log(`[global-setup] user-1 credentials from ${emailSource} / ${passwordSource}`);
177
+ await loginAs({
178
+ email,
179
+ password,
180
+ baseURL,
181
+ stateFile: '.auth/user-default.json',
182
+ });
183
+ }
184
+
185
+ if (authUsers.has('2') && process.env.QA_USER2_EMAIL && process.env.QA_USER2_PASSWORD) {
186
+ const [email, emailSource] = resolveVar(process.env.QA_USER2_EMAIL, 'QA_USER2_EMAIL', 'qa-user2@example.com');
187
+ const [password, passwordSource] = resolveVar(process.env.QA_USER2_PASSWORD, 'QA_USER2_PASSWORD', 'CHANGE_ME');
188
+ console.log(`[global-setup] user-2 credentials from ${emailSource} / ${passwordSource}`);
189
+ await loginAs({
190
+ email,
191
+ password,
192
+ baseURL,
193
+ stateFile: '.auth/user-2.json',
194
+ });
195
+ }
106
196
  }
@@ -2,6 +2,8 @@ import { defineConfig, devices } from '@playwright/test';
2
2
  import * as dotenv from 'dotenv';
3
3
  dotenv.config();
4
4
 
5
+ export const SESSION_TTL_MS = Number(process.env.QA_SESSION_TTL_MS ?? 2 * 60 * 60 * 1000);
6
+
5
7
  // -------------------------------------------------------------------
6
8
  // Validate required environment variables at config load time
7
9
  // -------------------------------------------------------------------
@@ -92,6 +94,28 @@ export default defineConfig({
92
94
  },
93
95
  ],
94
96
 
97
+ // ------ Parallel 2-user variant ------
98
+ // Uncomment and replace the single chromium project above to run two parallel
99
+ // workers with separate credentials. Also set QA_USER2_EMAIL and
100
+ // QA_USER2_PASSWORD in .env.
101
+ //
102
+ // {
103
+ // name: 'chromium-user1',
104
+ // use: {
105
+ // ...devices['Desktop Chrome'],
106
+ // storageState: '.auth/user-1.json',
107
+ // },
108
+ // dependencies: ['setup'],
109
+ // },
110
+ // {
111
+ // name: 'chromium-user2',
112
+ // use: {
113
+ // ...devices['Desktop Chrome'],
114
+ // storageState: '.auth/user-2.json',
115
+ // },
116
+ // dependencies: ['setup'],
117
+ // },
118
+
95
119
  // ------ Output directories ------
96
120
  outputDir: 'test-results/',
97
121
  snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
@@ -6,7 +6,7 @@
6
6
  | Title | {{Short title (max 80 chars)}} |
7
7
  | Severity | Critical / High / Medium / Low |
8
8
  | Priority | P0 / P1 / P2 / P3 |
9
- | Status | Open / In Progress / Resolved / Closed |
9
+ | Status | Open / In Progress / Disputed / Resolved / Closed |
10
10
  | Assigned to | {{Developer or "Unassigned"}} |
11
11
  | Module | {{Module name}} |
12
12
  | Submodule | {{Submodule name}} |
@@ -0,0 +1,40 @@
1
+ # -----------------------------------------------------------------------
2
+ # Integration test environment variables
3
+ # -----------------------------------------------------------------------
4
+ # Copy this file to .env and fill in real values.
5
+ # NEVER commit .env to source control.
6
+ # -----------------------------------------------------------------------
7
+
8
+ # -----------------------------------------------------------------------
9
+ # Application under test
10
+ # -----------------------------------------------------------------------
11
+ QA_BASE_URL=https://your-app.qa.example.com
12
+
13
+ # -----------------------------------------------------------------------
14
+ # Auth - choose one strategy
15
+ # -----------------------------------------------------------------------
16
+
17
+ # Strategy 1: Static bearer token (preferred for pure API services)
18
+ # QA_API_TOKEN=your-api-token-here
19
+
20
+ # Strategy 2: Browser login (for apps with cookie/session auth)
21
+ # QA_USER_EMAIL=qa-user@example.com
22
+ # QA_USER_PASSWORD=CHANGE_ME
23
+
24
+ # -----------------------------------------------------------------------
25
+ # Login form selectors (used only when QA_API_TOKEN is not set)
26
+ # -----------------------------------------------------------------------
27
+ # QA_LOGIN_PATH=/login
28
+ # QA_LOGIN_EMAIL_SELECTOR=input[type="email"]
29
+ # QA_LOGIN_PASSWORD_SELECTOR=input[type="password"]
30
+ # QA_LOGIN_SUBMIT_SELECTOR=button[type="submit"]
31
+ # QA_LOGIN_SUCCESS_SELECTOR=.dashboard
32
+
33
+ # -----------------------------------------------------------------------
34
+ # Azure DevOps integration (optional)
35
+ # -----------------------------------------------------------------------
36
+ # ADO_ORG=your-org
37
+ # ADO_PROJECT=YourProject
38
+ # ADO_PLAN_ID=00000
39
+ # ADO_PAT=your-pat-here
40
+ # SYSTEM_ACCESSTOKEN= # Set automatically in Azure Pipelines
@@ -0,0 +1,77 @@
1
+ # API Integration Tests
2
+
3
+ Playwright-based API test project for `qa/07-automation/integration/`.
4
+
5
+ Tests run without a browser. All tests use Playwright's `APIRequestContext`
6
+ to call HTTP endpoints directly. Auth state is established once by
7
+ `global-setup.ts` and reused across the test run.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 18+
12
+ - `@playwright/test` (installed via `npm install` in this directory)
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ cd qa/07-automation/integration
18
+ npm install
19
+ cp .env.example .env
20
+ # Edit .env and fill in QA_BASE_URL and auth credentials
21
+ ```
22
+
23
+ Add the following to your root `.gitignore`:
24
+
25
+ ```
26
+ qa/07-automation/integration/.env
27
+ qa/07-automation/integration/.auth/
28
+ qa/07-automation/integration/test-results/
29
+ qa/07-automation/integration/playwright-report/
30
+ ```
31
+
32
+ ## Environment variables
33
+
34
+ | Variable | Required | Description |
35
+ |---|---|---|
36
+ | `QA_BASE_URL` | Yes | Base URL of the API under test |
37
+ | `QA_API_TOKEN` | One of | Static bearer token for token-based APIs |
38
+ | `QA_USER_EMAIL` | One of | Email/username for browser-login APIs |
39
+ | `QA_USER_PASSWORD` | One of | Password for browser-login APIs |
40
+
41
+ See `.env.example` for all supported variables.
42
+
43
+ ## Running tests
44
+
45
+ ```bash
46
+ # All tests
47
+ npm test
48
+
49
+ # CI mode (retries enabled)
50
+ npm run test:ci
51
+
52
+ # Open HTML report
53
+ npm run report
54
+ ```
55
+
56
+ ## Writing tests
57
+
58
+ Import from `@playwright/test` and use the `request` fixture:
59
+
60
+ ```typescript
61
+ import { test, expect } from '@playwright/test';
62
+
63
+ test('GET /users returns 200', async ({ request }) => {
64
+ const response = await request.get('/users');
65
+ expect(response.status()).toBe(200);
66
+ });
67
+ ```
68
+
69
+ For token-based APIs that require an `Authorization` header, pass it per request:
70
+
71
+ ```typescript
72
+ const response = await request.get('/api/protected', {
73
+ headers: { Authorization: `Bearer ${process.env.QA_API_TOKEN}` },
74
+ });
75
+ ```
76
+
77
+ See `tests/example.spec.ts` for a starter test to replace.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * global-setup.ts - Integration (API-only) project
3
+ *
4
+ * Runs once before all test files to establish auth state.
5
+ *
6
+ * Two strategies, tried in order:
7
+ * 1. Token-based: QA_API_TOKEN is set -> write a minimal storageState with the
8
+ * token in localStorage so the `request` fixture picks it up automatically.
9
+ * 2. Browser login: QA_USER_EMAIL + QA_USER_PASSWORD are set -> launch Chromium,
10
+ * perform the login flow, and save the resulting storageState to
11
+ * .auth/api-session.json.
12
+ *
13
+ * Playwright's `storageState` mechanism handles cookies and localStorage for
14
+ * APIRequestContext automatically when `use.storageState` is configured.
15
+ * For token-only APIs that require an Authorization header, set the header
16
+ * directly in each test or extend the `request` fixture in fixtures/base.ts.
17
+ */
18
+
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ import { chromium } from '@playwright/test';
22
+ import * as dotenv from 'dotenv';
23
+
24
+ dotenv.config();
25
+
26
+ const AUTH_FILE = path.join(__dirname, '.auth', 'api-session.json');
27
+
28
+ export default async function globalSetup(): Promise<void> {
29
+ fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
30
+
31
+ if (process.env.QA_API_TOKEN) {
32
+ await setupWithToken(process.env.QA_API_TOKEN, process.env.QA_BASE_URL!);
33
+ return;
34
+ }
35
+
36
+ await setupWithBrowserLogin();
37
+ }
38
+
39
+ /**
40
+ * Write a minimal storageState that carries the bearer token in localStorage.
41
+ * Replace the localStorage key/value format if your app uses a different scheme
42
+ * (e.g. a cookie, or a different localStorage key name).
43
+ */
44
+ async function setupWithToken(token: string, baseURL: string): Promise<void> {
45
+ const origin = baseURL.replace(/\/$/, '');
46
+
47
+ const state = {
48
+ cookies: [],
49
+ origins: [
50
+ {
51
+ origin,
52
+ localStorage: [
53
+ { name: 'qa_api_token', value: token },
54
+ ],
55
+ },
56
+ ],
57
+ };
58
+
59
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2), 'utf-8');
60
+ console.log('[integration-setup] Token-based auth written to api-session.json');
61
+ }
62
+
63
+ /**
64
+ * Perform a browser login and save the resulting cookies + localStorage to disk.
65
+ *
66
+ * Configure login form selectors via env vars (see .env.example) or update the
67
+ * defaults below to match your application's login page.
68
+ */
69
+ async function setupWithBrowserLogin(): Promise<void> {
70
+ const baseURL = process.env.QA_BASE_URL!.replace(/\/$/, '');
71
+ const email = process.env.QA_USER_EMAIL!;
72
+ const password = process.env.QA_USER_PASSWORD!;
73
+ const loginPath = process.env.QA_LOGIN_PATH ?? '/login';
74
+ const emailSelector = process.env.QA_LOGIN_EMAIL_SELECTOR ?? 'input[type="email"]';
75
+ const passwordSelector = process.env.QA_LOGIN_PASSWORD_SELECTOR ?? 'input[type="password"]';
76
+ const submitSelector = process.env.QA_LOGIN_SUBMIT_SELECTOR ?? 'button[type="submit"]';
77
+ const successSelector = process.env.QA_LOGIN_SUCCESS_SELECTOR ?? '.dashboard, .main-content, [data-testid="app-shell"]';
78
+
79
+ console.log('[integration-setup] Performing browser login...');
80
+
81
+ const browser = await chromium.launch();
82
+ const context = await browser.newContext();
83
+ const page = await context.newPage();
84
+
85
+ try {
86
+ await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'load', timeout: 60_000 });
87
+
88
+ await page.locator(emailSelector).waitFor({ state: 'visible', timeout: 30_000 });
89
+ await page.locator(emailSelector).fill(email);
90
+ await page.locator(passwordSelector).fill(password);
91
+ await page.locator(submitSelector).click();
92
+ await page.waitForSelector(successSelector, { timeout: 60_000 });
93
+
94
+ await context.storageState({ path: AUTH_FILE });
95
+ console.log(`[integration-setup] Auth saved: ${AUTH_FILE}`);
96
+ } finally {
97
+ await browser.close();
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "qa-integration-tests",
3
+ "version": "1.0.0",
4
+ "description": "API integration tests - no browser required",
5
+ "private": true,
6
+ "scripts": {
7
+ "test": "playwright test",
8
+ "test:ci": "CI=true playwright test",
9
+ "report": "playwright show-report"
10
+ },
11
+ "devDependencies": {
12
+ "@playwright/test": "^1.44.0",
13
+ "dotenv": "^16.4.0"
14
+ }
15
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * playwright.config.ts - API integration test project (no browser)
3
+ *
4
+ * All tests use APIRequestContext directly. No browser projects are defined.
5
+ * Auth is handled by global-setup.ts, which writes .auth/api-session.json.
6
+ *
7
+ * Required env vars:
8
+ * QA_BASE_URL - Base URL of the API under test
9
+ *
10
+ * Auth (at least one of the following):
11
+ * QA_API_TOKEN - Bearer token (skips browser login in global-setup)
12
+ * QA_USER_EMAIL + QA_USER_PASSWORD - Credentials for browser-based login
13
+ */
14
+
15
+ import { defineConfig } from '@playwright/test';
16
+ import * as dotenv from 'dotenv';
17
+
18
+ dotenv.config();
19
+
20
+ const required = ['QA_BASE_URL'];
21
+ for (const key of required) {
22
+ if (!process.env[key]) {
23
+ throw new Error(`[integration] Missing required env var: ${key}. Check your .env file.`);
24
+ }
25
+ }
26
+
27
+ const hasToken = !!process.env.QA_API_TOKEN;
28
+ const hasCredentials = !!(process.env.QA_USER_EMAIL && process.env.QA_USER_PASSWORD);
29
+
30
+ if (!hasToken && !hasCredentials) {
31
+ throw new Error(
32
+ '[integration] No auth configured. Set QA_API_TOKEN, or both QA_USER_EMAIL and QA_USER_PASSWORD.',
33
+ );
34
+ }
35
+
36
+ // Optional: Azure DevOps reporter configuration
37
+ // To enable: set env vars ADO_ORG, ADO_PROJECT, ADO_PAT, ADO_PLAN_ID and CI=true,
38
+ // then uncomment the reporter entry in the `reporter` array below.
39
+ // Install: npm install @alex_neo/playwright-azure-reporter --save-dev
40
+ // const adoReporterConfig = {
41
+ // orgUrl: `https://dev.azure.com/${process.env.ADO_ORG}`,
42
+ // token: process.env.SYSTEM_ACCESSTOKEN ?? process.env.ADO_PAT,
43
+ // planId: Number(process.env.ADO_PLAN_ID),
44
+ // projectName: process.env.ADO_PROJECT!,
45
+ // testRunTitle: `[Auto API] ${new Date().toISOString().slice(0, 10)}`,
46
+ // publishTestResultsMode: 'testRun' as const,
47
+ // uploadAttachments: false,
48
+ // isDisabled: !process.env.CI,
49
+ // autoMarkTestCasesAsAutomated: {
50
+ // enabled: true,
51
+ // updateAutomatedTestName: true,
52
+ // updateAutomatedTestStorage: true,
53
+ // },
54
+ // };
55
+
56
+ export default defineConfig({
57
+ testDir: './tests',
58
+ fullyParallel: false,
59
+ workers: 1,
60
+ timeout: 30_000,
61
+ retries: process.env.CI ? 1 : 0,
62
+
63
+ globalSetup: './global-setup.ts',
64
+
65
+ reporter: [
66
+ ['list'],
67
+ ['html', { open: 'never', outputFolder: 'playwright-report' }],
68
+ // Uncomment for ADO:
69
+ // ['@alex_neo/playwright-azure-reporter', adoReporterConfig],
70
+ ],
71
+
72
+ use: {
73
+ baseURL: process.env.QA_BASE_URL,
74
+ storageState: '.auth/api-session.json',
75
+ },
76
+
77
+ outputDir: 'test-results/',
78
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * example.spec.ts - API integration test example
3
+ *
4
+ * This file shows the basic pattern for API tests using Playwright's
5
+ * APIRequestContext. Replace this with your real tests.
6
+ *
7
+ * The `request` fixture is pre-configured with:
8
+ * - baseURL from QA_BASE_URL
9
+ * - storageState from .auth/api-session.json (set by global-setup.ts)
10
+ *
11
+ * For token-based APIs that require an Authorization header, add it
12
+ * explicitly per request or extend the fixture in a fixtures/base.ts file.
13
+ */
14
+
15
+ import { test, expect } from '@playwright/test';
16
+
17
+ // Replace with the actual health/status endpoint of your API.
18
+ const HEALTH_ENDPOINT = '/health';
19
+
20
+ test.describe('API smoke tests', () => {
21
+
22
+ test('GET health endpoint returns 200', async ({ request }) => {
23
+ const response = await request.get(HEALTH_ENDPOINT);
24
+
25
+ expect(response.status()).toBe(200);
26
+ });
27
+
28
+ // Example of a test with an Authorization header when QA_API_TOKEN is set.
29
+ // Uncomment and adapt when your API uses Bearer token auth.
30
+ //
31
+ // test('GET protected resource returns 200 with bearer token', async ({ request }) => {
32
+ // const response = await request.get('/api/me', {
33
+ // headers: {
34
+ // Authorization: `Bearer ${process.env.QA_API_TOKEN}`,
35
+ // },
36
+ // });
37
+ //
38
+ // expect(response.status()).toBe(200);
39
+ // const body = await response.json();
40
+ // expect(body).toHaveProperty('id');
41
+ // });
42
+
43
+ });