@appliqation/automation-sdk 2.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/package.json +107 -0
  4. package/src/AppliqationClient.js +562 -0
  5. package/src/constants.js +245 -0
  6. package/src/core/AuthManager.js +353 -0
  7. package/src/core/HttpClient.js +475 -0
  8. package/src/index.d.ts +333 -0
  9. package/src/index.js +26 -0
  10. package/src/playwright/JwtBrowserAuth.js +240 -0
  11. package/src/playwright/fixture.js +92 -0
  12. package/src/playwright/global-setup.js +243 -0
  13. package/src/playwright/helpers/jwt-browser-auth.js +227 -0
  14. package/src/playwright/index.js +16 -0
  15. package/src/reporters/cypress/CypressReporter.js +387 -0
  16. package/src/reporters/cypress/UuidExtractor.js +139 -0
  17. package/src/reporters/cypress/index.js +30 -0
  18. package/src/reporters/jest/JestReporter.js +361 -0
  19. package/src/reporters/jest/UuidExtractor.js +174 -0
  20. package/src/reporters/jest/index.js +28 -0
  21. package/src/reporters/playwright/AppliqationReporter.js +654 -0
  22. package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
  23. package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
  24. package/src/reporters/playwright/index.d.ts +96 -0
  25. package/src/reporters/playwright/index.js +14 -0
  26. package/src/services/OrphanTestService.js +74 -0
  27. package/src/services/ResultService.js +252 -0
  28. package/src/services/RunMatrixService.js +309 -0
  29. package/src/utils/PayloadBuilder.js +280 -0
  30. package/src/utils/RunDataNormalizer.js +335 -0
  31. package/src/utils/UuidValidator.js +102 -0
  32. package/src/utils/errors.js +217 -0
  33. package/src/utils/index.js +17 -0
  34. package/src/utils/logger.js +124 -0
  35. package/src/utils/mapAppqUuid.js +83 -0
  36. package/src/utils/validator.js +157 -0
package/src/index.d.ts ADDED
@@ -0,0 +1,333 @@
1
+ /**
2
+ * TypeScript definitions for @appliqation/automation-sdk
3
+ * @version 2.1.0
4
+ */
5
+
6
+ // ============================================================================
7
+ // Core Types
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Test result status
12
+ */
13
+ export type TestStatus = 'pass' | 'fail' | 'skip';
14
+
15
+ /**
16
+ * Log levels
17
+ */
18
+ export type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
19
+
20
+ /**
21
+ * Browser types
22
+ */
23
+ export type Browser = 'Chrome' | 'Firefox' | 'Safari' | 'Edge' | 'Chromium' | 'WebKit' | string;
24
+
25
+ /**
26
+ * Device types
27
+ */
28
+ export type Device = 'Desktop' | 'Mobile' | 'Tablet' | string;
29
+
30
+ /**
31
+ * Operating systems
32
+ */
33
+ export type OperatingSystem = 'Windows' | 'macOS' | 'Linux' | 'iOS' | 'Android' | string;
34
+
35
+ // ============================================================================
36
+ // Configuration Interfaces
37
+ // ============================================================================
38
+
39
+ /**
40
+ * SDK configuration options
41
+ */
42
+ export interface AppliqationConfig {
43
+ /** Base URL of the Appliqation instance */
44
+ baseUrl: string;
45
+
46
+ /** API key for authentication (recommended) */
47
+ apiKey?: string;
48
+
49
+ /** Project key */
50
+ projectKey?: string;
51
+
52
+ /** Username for legacy CSRF authentication */
53
+ username?: string;
54
+
55
+ /** Password for legacy CSRF authentication */
56
+ password?: string;
57
+
58
+ /** Scenario ID (optional, 0 for generic automation runs) */
59
+ scenarioId?: number;
60
+
61
+ /** Test set ID (alternative to scenarioId) */
62
+ testSetId?: number;
63
+
64
+ /** Environment name (e.g., 'Local', 'Development', 'Staging', 'Production') */
65
+ environment?: string;
66
+
67
+ /** Custom run title */
68
+ title?: string;
69
+
70
+ /** Alternative to title */
71
+ runTitle?: string;
72
+
73
+ /** Browser list */
74
+ browsers?: Browser[];
75
+
76
+ /** Device type */
77
+ device?: Device;
78
+
79
+ /** Operating system */
80
+ os?: OperatingSystem;
81
+
82
+ /** Automatically create run on initialization */
83
+ autoCreateRun?: boolean;
84
+
85
+ /** Enable batch submission of results */
86
+ batchSubmit?: boolean;
87
+
88
+ /** Batch size for result submission */
89
+ batchSize?: number;
90
+
91
+ /** SSL certificate verification (default: true) */
92
+ rejectUnauthorized?: boolean;
93
+
94
+ /** Additional options */
95
+ options?: {
96
+ /** Request timeout in milliseconds (default: 30000) */
97
+ timeout?: number;
98
+
99
+ /** Number of retries for failed requests (default: 3) */
100
+ retries?: number;
101
+
102
+ /** Base retry delay in milliseconds (default: 1000) */
103
+ retryDelay?: number;
104
+
105
+ /** Maximum retry delay in milliseconds (default: 30000) */
106
+ maxRetryDelay?: number;
107
+
108
+ /** Retry jitter factor (default: 0.1) */
109
+ retryJitter?: number;
110
+
111
+ /** Log orphan tests (tests without UUID mapping) (default: true) */
112
+ logOrphans?: boolean;
113
+
114
+ /** Log level (default: 'INFO') */
115
+ logLevel?: LogLevel;
116
+
117
+ /** Enable HTTP keep-alive (default: true) */
118
+ keepAlive?: boolean;
119
+
120
+ /** Maximum number of sockets (default: 50) */
121
+ maxSockets?: number;
122
+
123
+ /** Maximum number of free sockets (default: 10) */
124
+ maxFreeSockets?: number;
125
+
126
+ /** Keep-alive timeout in milliseconds (default: 1000) */
127
+ keepAliveMsecs?: number;
128
+ };
129
+ }
130
+
131
+ // ============================================================================
132
+ // Result Interfaces
133
+ // ============================================================================
134
+
135
+ /**
136
+ * Test result object
137
+ */
138
+ export interface TestResult {
139
+ /** Test UUID (format: "{testSetId}-{uuid}") */
140
+ uuid: string;
141
+
142
+ /** Test status */
143
+ status: TestStatus;
144
+
145
+ /** Optional comment */
146
+ comment?: string;
147
+
148
+ /** Optional attachments */
149
+ attachments?: string[];
150
+
151
+ /** Optional metadata */
152
+ metadata?: Record<string, any>;
153
+ }
154
+
155
+ /**
156
+ * API response
157
+ */
158
+ export interface ApiResponse<T = any> {
159
+ /** Success status */
160
+ success: boolean;
161
+
162
+ /** Response data */
163
+ data?: T;
164
+
165
+ /** HTTP status code */
166
+ status?: number;
167
+
168
+ /** Response headers */
169
+ headers?: Record<string, string>;
170
+
171
+ /** Error message (if success is false) */
172
+ message?: string;
173
+
174
+ /** Error details */
175
+ error?: string;
176
+ }
177
+
178
+ /**
179
+ * Run matrix response
180
+ */
181
+ export interface RunMatrixResponse {
182
+ /** Run ID */
183
+ runId: string;
184
+
185
+ /** Run timestamp */
186
+ timestamp: number;
187
+
188
+ /** Environment */
189
+ environment: string;
190
+
191
+ /** Browser list */
192
+ browsers: Browser[];
193
+
194
+ /** Device type */
195
+ device?: Device;
196
+
197
+ /** Operating system */
198
+ os?: OperatingSystem;
199
+ }
200
+
201
+ /**
202
+ * Batch submission summary
203
+ */
204
+ export interface BatchSubmissionSummary {
205
+ /** Number of successful submissions */
206
+ success: number;
207
+
208
+ /** Number of failed submissions */
209
+ failed: number;
210
+
211
+ /** Total number of results */
212
+ total: number;
213
+
214
+ /** Number of invalid results */
215
+ invalid?: number;
216
+
217
+ /** Invalid results details */
218
+ invalidResults?: Array<{ result: TestResult; error: string }>;
219
+ }
220
+
221
+ // ============================================================================
222
+ // AppliqationClient Class
223
+ // ============================================================================
224
+
225
+ /**
226
+ * Main SDK client class
227
+ */
228
+ export class AppliqationClient {
229
+ /**
230
+ * Create a new Appliqation client
231
+ */
232
+ constructor(config: AppliqationConfig);
233
+
234
+ /**
235
+ * Create a new automation run
236
+ */
237
+ createRun(config?: Partial<AppliqationConfig>): Promise<RunMatrixResponse>;
238
+
239
+ /**
240
+ * Submit a single test result
241
+ */
242
+ reportResult(
243
+ uuid: string,
244
+ status: TestStatus,
245
+ options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
246
+ ): Promise<ApiResponse>;
247
+
248
+ /**
249
+ * Submit multiple test results in batch
250
+ */
251
+ reportBatchResults(results: TestResult[]): Promise<BatchSubmissionSummary>;
252
+
253
+ /**
254
+ * Log orphan tests (tests without UUID mappings)
255
+ */
256
+ logOrphanTests(runId: string, orphanTests: any[]): Promise<ApiResponse>;
257
+
258
+ /**
259
+ * Get current run ID
260
+ */
261
+ getRunId(): string | null;
262
+
263
+ /**
264
+ * Test connection to Appliqation instance
265
+ */
266
+ testConnection(): Promise<boolean>;
267
+
268
+ /**
269
+ * Get SDK version
270
+ */
271
+ getVersion(): string;
272
+ }
273
+
274
+ // ============================================================================
275
+ // Exports
276
+ // ============================================================================
277
+
278
+ /**
279
+ * Main client class (default export)
280
+ */
281
+ export default AppliqationClient;
282
+
283
+ /**
284
+ * Named exports
285
+ */
286
+ export { AppliqationClient };
287
+
288
+ /**
289
+ * Utility exports
290
+ */
291
+ export class UuidValidator {
292
+ static validate(uuid: string): { valid: boolean; error?: string };
293
+ static isValid(uuid: string): boolean;
294
+ }
295
+
296
+ export class PayloadBuilder {
297
+ static buildResultPayload(
298
+ uuid: string,
299
+ status: TestStatus,
300
+ options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
301
+ ): TestResult;
302
+ }
303
+
304
+ export const logger: {
305
+ error(message: string, meta?: Record<string, any>): void;
306
+ warn(message: string, meta?: Record<string, any>): void;
307
+ info(message: string, meta?: Record<string, any>): void;
308
+ debug(message: string, meta?: Record<string, any>): void;
309
+ setLevel(level: LogLevel): void;
310
+ setConsoleEnabled(enabled: boolean): void;
311
+ };
312
+
313
+ /**
314
+ * Map a test UUID to Playwright testInfo for result reporting
315
+ *
316
+ * @param testInfo - Playwright test info object from test function
317
+ * @param uuid - Appliqation test UUID (format: nid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
318
+ *
319
+ * @throws {Error} If testInfo is not provided or invalid
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * import { test } from '@playwright/test';
324
+ * import { mapAppqUuid } from '@appliqation/automation-sdk/utils';
325
+ *
326
+ * test('login functionality', async ({ page }, testInfo) => {
327
+ * mapAppqUuid(testInfo, '1141-6d2827f3-1bc2-405c-95b4-6a09e8b64148');
328
+ * await page.goto('/login');
329
+ * // ... test implementation
330
+ * });
331
+ * ```
332
+ */
333
+ export function mapAppqUuid(testInfo: any, uuid: string): void;
package/src/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Appliqation Automation SDK
3
+ *
4
+ * Main entry point for the SDK - New Architecture with API Key Authentication
5
+ */
6
+
7
+ const AppliqationClient = require('./AppliqationClient');
8
+ const UuidValidator = require('./utils/UuidValidator');
9
+ const PayloadBuilder = require('./utils/PayloadBuilder');
10
+ const logger = require('./utils/logger');
11
+ const playwright = require('./playwright');
12
+
13
+ // Export main client class
14
+ module.exports = AppliqationClient;
15
+
16
+ // Export utilities as named exports
17
+ module.exports.AppliqationClient = AppliqationClient;
18
+ module.exports.UuidValidator = UuidValidator;
19
+ module.exports.PayloadBuilder = PayloadBuilder;
20
+ module.exports.logger = logger;
21
+
22
+ // Export Playwright utilities
23
+ module.exports.playwright = playwright;
24
+
25
+ // Export default
26
+ module.exports.default = AppliqationClient;
@@ -0,0 +1,240 @@
1
+ const axios = require('axios');
2
+ const https = require('https');
3
+ const fs = require('fs');
4
+ const jwt = require('jsonwebtoken');
5
+ const logger = require('../utils/logger');
6
+
7
+ /**
8
+ * JWT Browser Authentication Manager
9
+ *
10
+ * Handles browser JWT authentication with automatic token refresh
11
+ * for Playwright test frameworks.
12
+ *
13
+ * Features:
14
+ * - Automatic JWT token refresh when approaching expiry
15
+ * - Browser cookie management
16
+ * - Storage state synchronization
17
+ * - Zero-configuration setup
18
+ */
19
+ class JwtBrowserAuth {
20
+ constructor(config) {
21
+ this.baseUrl = config.baseUrl;
22
+ this.apiKey = config.apiKey;
23
+ this.projectKey = config.projectKey;
24
+ this.storageStatePath = config.storageStatePath || '.auth/user.json';
25
+ this.refreshThresholdMinutes = config.refreshThresholdMinutes || 5;
26
+ this.rejectUnauthorized = config.rejectUnauthorized !== undefined ? config.rejectUnauthorized : true;
27
+
28
+ this.jwtToken = null;
29
+ this.jwtExpiry = null;
30
+ }
31
+
32
+ /**
33
+ * Decode JWT token to extract expiry time
34
+ */
35
+ decodeToken(token) {
36
+ try {
37
+ return jwt.decode(token);
38
+ } catch (error) {
39
+ logger.error('Failed to decode JWT token', { error: error.message });
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get browser JWT token from API
46
+ */
47
+ async getBrowserJwt() {
48
+ try {
49
+ logger.info('Requesting browser JWT token...');
50
+
51
+ const response = await axios.post(`${this.baseUrl}/api/auth/jwt/browser`, {
52
+ api_key: this.apiKey
53
+ }, {
54
+ headers: { 'Content-Type': 'application/json' },
55
+ timeout: 30000,
56
+ httpsAgent: new https.Agent({ rejectUnauthorized: this.rejectUnauthorized })
57
+ });
58
+
59
+ if (!response.data || !response.data.jwt_token) {
60
+ throw new Error('No JWT token received from server');
61
+ }
62
+
63
+ this.jwtToken = response.data.jwt_token;
64
+
65
+ // Decode token to get actual expiry
66
+ const decoded = this.decodeToken(this.jwtToken);
67
+ if (decoded && decoded.exp) {
68
+ this.jwtExpiry = decoded.exp * 1000;
69
+ } else {
70
+ const expiresIn = response.data.expires_in || 3600;
71
+ this.jwtExpiry = Date.now() + (expiresIn * 1000);
72
+ }
73
+
74
+ logger.info('Browser JWT token received', {
75
+ expiresAt: new Date(this.jwtExpiry).toISOString(),
76
+ minutesUntilExpiry: this.getTimeUntilExpiry()
77
+ });
78
+
79
+ return response.data;
80
+ } catch (error) {
81
+ logger.error('Failed to get browser JWT token', { error: error.message });
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if JWT token should be refreshed
88
+ */
89
+ shouldRefresh() {
90
+ if (!this.jwtExpiry) return true;
91
+ const thresholdMs = this.refreshThresholdMinutes * 60 * 1000;
92
+ const timeUntilExpiry = this.jwtExpiry - Date.now();
93
+ return timeUntilExpiry <= thresholdMs;
94
+ }
95
+
96
+ /**
97
+ * Get time until token expiry in minutes
98
+ */
99
+ getTimeUntilExpiry() {
100
+ if (!this.jwtExpiry) return 0;
101
+ return Math.max(0, Math.floor((this.jwtExpiry - Date.now()) / 1000 / 60));
102
+ }
103
+
104
+ /**
105
+ * Refresh browser JWT token in the page context
106
+ */
107
+ async refreshBrowserJwt(page) {
108
+ try {
109
+ logger.debug('Checking JWT token expiry...', {
110
+ minutesUntilExpiry: this.getTimeUntilExpiry()
111
+ });
112
+
113
+ if (!this.shouldRefresh()) {
114
+ logger.debug('JWT token is still valid, no refresh needed');
115
+ return;
116
+ }
117
+
118
+ logger.info('Refreshing browser JWT token...');
119
+
120
+ // Get new JWT token
121
+ const response = await this.getBrowserJwt();
122
+ const newToken = response.jwt_token;
123
+
124
+ // Update cookie in the browser context
125
+ const context = page.context();
126
+ const domain = new URL(this.baseUrl).hostname;
127
+
128
+ // Get existing cookies
129
+ const cookies = await context.cookies();
130
+ const otherCookies = cookies.filter(c => c.name !== 'appliqation_jwt');
131
+
132
+ // Clear all cookies and re-add them with new JWT
133
+ await context.clearCookies();
134
+
135
+ if (otherCookies.length > 0) {
136
+ await context.addCookies(otherCookies);
137
+ }
138
+
139
+ // Add new JWT cookie
140
+ await context.addCookies([{
141
+ name: 'appliqation_jwt',
142
+ value: newToken,
143
+ domain: domain,
144
+ path: '/',
145
+ httpOnly: true,
146
+ secure: this.baseUrl.startsWith('https'),
147
+ sameSite: 'Lax'
148
+ }]);
149
+
150
+ logger.info('Browser JWT token refreshed successfully', {
151
+ minutesUntilExpiry: this.getTimeUntilExpiry()
152
+ });
153
+
154
+ // Update storage state file
155
+ await this.updateStorageState(context);
156
+
157
+ } catch (error) {
158
+ logger.error('Failed to refresh browser JWT token', { error: error.message });
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Update the storage state file with new cookies
165
+ */
166
+ async updateStorageState(context) {
167
+ try {
168
+ await context.storageState({ path: this.storageStatePath });
169
+ logger.debug('Updated storage state', { path: this.storageStatePath });
170
+ } catch (error) {
171
+ logger.warn('Failed to update storage state', { error: error.message });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Load JWT info from storage state file
177
+ */
178
+ loadFromStorageState() {
179
+ try {
180
+ if (!fs.existsSync(this.storageStatePath)) {
181
+ logger.debug('No storage state file found', { path: this.storageStatePath });
182
+ return;
183
+ }
184
+
185
+ const storageState = JSON.parse(fs.readFileSync(this.storageStatePath, 'utf8'));
186
+ const jwtCookie = storageState.cookies?.find(c => c.name === 'appliqation_jwt');
187
+
188
+ if (jwtCookie) {
189
+ this.jwtToken = jwtCookie.value;
190
+ const decoded = this.decodeToken(this.jwtToken);
191
+ if (decoded && decoded.exp) {
192
+ this.jwtExpiry = decoded.exp * 1000;
193
+ logger.debug('Loaded JWT from storage state', {
194
+ minutesUntilExpiry: this.getTimeUntilExpiry()
195
+ });
196
+ }
197
+ }
198
+ } catch (error) {
199
+ logger.warn('Failed to load JWT from storage state', { error: error.message });
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Setup initial browser authentication
205
+ * Creates JWT token and saves to storage state
206
+ */
207
+ async setupInitialAuth(browserContext) {
208
+ try {
209
+ logger.info('Setting up initial browser JWT authentication...');
210
+
211
+ // Get browser JWT token
212
+ const response = await this.getBrowserJwt();
213
+ const jwtToken = response.jwt_token;
214
+
215
+ // Set JWT token as cookie
216
+ const domain = new URL(this.baseUrl).hostname;
217
+ await browserContext.addCookies([{
218
+ name: 'appliqation_jwt',
219
+ value: jwtToken,
220
+ domain: domain,
221
+ path: '/',
222
+ httpOnly: true,
223
+ secure: this.baseUrl.startsWith('https'),
224
+ sameSite: 'Lax'
225
+ }]);
226
+
227
+ logger.info('Browser JWT authentication setup complete', {
228
+ domain,
229
+ minutesUntilExpiry: this.getTimeUntilExpiry()
230
+ });
231
+
232
+ return jwtToken;
233
+ } catch (error) {
234
+ logger.error('Failed to setup initial browser authentication', { error: error.message });
235
+ throw error;
236
+ }
237
+ }
238
+ }
239
+
240
+ module.exports = JwtBrowserAuth;
@@ -0,0 +1,92 @@
1
+ // Import Playwright test dynamically to avoid circular dependencies
2
+ let baseTest;
3
+ try {
4
+ baseTest = require('@playwright/test').test;
5
+ } catch (error) {
6
+ // Fallback if @playwright/test is not available
7
+ console.warn('⚠️ @playwright/test not found. JWT auto-refresh will not be available.');
8
+ baseTest = null;
9
+ }
10
+
11
+ const JwtBrowserAuth = require('./JwtBrowserAuth');
12
+ require('dotenv').config();
13
+
14
+ /**
15
+ * Appliqation Playwright Fixture with Auto JWT Refresh
16
+ *
17
+ * This fixture automatically handles JWT browser authentication with auto-refresh.
18
+ * Simply import this instead of @playwright/test and authentication is handled automatically.
19
+ *
20
+ * USAGE:
21
+ * ------
22
+ * // In your test file:
23
+ * const { test, expect } = require('@appliqation/automation-sdk/playwright');
24
+ *
25
+ * test('my test', async ({ page }) => {
26
+ * // JWT is automatically refreshed before each test if needed
27
+ * await page.goto('/dashboard');
28
+ * });
29
+ *
30
+ * CONFIGURATION:
31
+ * --------------
32
+ * Set these environment variables in your .env file:
33
+ * - APPLIQATION_BASE_URL
34
+ * - APPLIQATION_API_KEY
35
+ * - APPLIQATION_PROJECT_KEY
36
+ */
37
+
38
+ // Create the fixture only if Playwright is available
39
+ let test, expect;
40
+
41
+ if (baseTest) {
42
+ test = baseTest.extend({
43
+ page: async ({ page }, use) => {
44
+ // Get configuration from environment
45
+ const baseUrl = process.env.APPLIQATION_BASE_URL;
46
+ const apiKey = process.env.APPLIQATION_API_KEY;
47
+ const projectKey = process.env.APPLIQATION_PROJECT_KEY;
48
+
49
+ // If API key is not configured, skip JWT refresh
50
+ if (!apiKey) {
51
+ await use(page);
52
+ return;
53
+ }
54
+
55
+ // Initialize JWT browser auth manager
56
+ const jwtAuth = new JwtBrowserAuth({
57
+ baseUrl,
58
+ apiKey,
59
+ projectKey,
60
+ storageStatePath: '.auth/user.json',
61
+ refreshThresholdMinutes: 5
62
+ });
63
+
64
+ // Load existing JWT from storage state
65
+ jwtAuth.loadFromStorageState();
66
+
67
+ // Refresh JWT if needed before test runs
68
+ try {
69
+ await jwtAuth.refreshBrowserJwt(page);
70
+ } catch (error) {
71
+ // Log error but don't fail the test
72
+ // The test might still work with existing token
73
+ console.warn('⚠️ JWT refresh failed, continuing with existing token:', error.message);
74
+ }
75
+
76
+ // Run the test
77
+ await use(page);
78
+
79
+ // No cleanup needed - JWT is managed automatically
80
+ },
81
+ });
82
+
83
+ expect = require('@playwright/test').expect;
84
+ } else {
85
+ test = null;
86
+ expect = null;
87
+ }
88
+
89
+ module.exports = {
90
+ test,
91
+ expect
92
+ };