@muuktest/amikoo-reporter 1.0.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/README.md +140 -0
- package/dist/controlHub/apiUtils.d.ts +26 -0
- package/dist/controlHub/apiUtils.d.ts.map +1 -0
- package/dist/controlHub/apiUtils.js +61 -0
- package/dist/controlHub/apiUtils.js.map +1 -0
- package/dist/controlHub/controlHubReporter.d.ts +28 -0
- package/dist/controlHub/controlHubReporter.d.ts.map +1 -0
- package/dist/controlHub/controlHubReporter.js +139 -0
- package/dist/controlHub/controlHubReporter.js.map +1 -0
- package/dist/controlHub/gitUtils.d.ts +8 -0
- package/dist/controlHub/gitUtils.d.ts.map +1 -0
- package/dist/controlHub/gitUtils.js +57 -0
- package/dist/controlHub/gitUtils.js.map +1 -0
- package/dist/controlHub/utils.d.ts +3 -0
- package/dist/controlHub/utils.d.ts.map +1 -0
- package/dist/controlHub/utils.js +60 -0
- package/dist/controlHub/utils.js.map +1 -0
- package/dist/controlHub/videoUtils.d.ts +16 -0
- package/dist/controlHub/videoUtils.d.ts.map +1 -0
- package/dist/controlHub/videoUtils.js +104 -0
- package/dist/controlHub/videoUtils.js.map +1 -0
- package/install.js +458 -0
- package/package.json +59 -0
- package/playwright.config.ts +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Amikoo Reporter
|
|
2
|
+
|
|
3
|
+
Playwright reporter for Amikoo - Automatically installs and configures test reporting to Amikoo platform.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Automatic Setup**: Installs and configures everything with one command
|
|
8
|
+
- 📊 **Comprehensive Reporting**: Captures test results, videos, and screenshots
|
|
9
|
+
- 🔧 **Zero Configuration**: Works out of the box with sensible defaults
|
|
10
|
+
- 🎯 **Git Integration**: Automatically captures branch, commit, and repository info
|
|
11
|
+
- 📹 **Media Support**: Uploads test videos and screenshots to Amikoo
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install the package in your Playwright project:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @muuktest/amikoo-reporter --save-dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it! The package will automatically:
|
|
22
|
+
- ✅ Configure or create your `playwright.config.ts` (or `.js`)
|
|
23
|
+
- ✅ Create or update your `.env` file with the required key
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
After installation, update your `.env` file with your Amikoo API key:
|
|
28
|
+
|
|
29
|
+
```dotenv
|
|
30
|
+
AMIKOO_KEY=your_amikoo_key_here
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### How to obtain the key
|
|
34
|
+
|
|
35
|
+
- **AMIKOO_KEY**: Obtain from Amikoo application under your account settings or API keys section.
|
|
36
|
+
|
|
37
|
+
URL: `https://app.amikoo.ai`
|
|
38
|
+
|
|
39
|
+
## Project Structure
|
|
40
|
+
|
|
41
|
+
After installation, your project will have:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
your-project/
|
|
45
|
+
├── tests/
|
|
46
|
+
│ └── ... your test files
|
|
47
|
+
├── playwright.config.ts (configured automatically)
|
|
48
|
+
├── .env (template created or updated with AMIKOO_KEY)
|
|
49
|
+
├── node-modules/
|
|
50
|
+
│ └── @muuktest/ (installation folder for amikoo-reporter)
|
|
51
|
+
└── package.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
Run your Playwright tests as usual:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx playwright test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Test results will automatically be reported to Amikoo!
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## What Gets Reported
|
|
66
|
+
|
|
67
|
+
The reporter automatically captures and sends:
|
|
68
|
+
|
|
69
|
+
- ✅ Test execution status (passed/failed/skipped)
|
|
70
|
+
- ✅ Test duration and timing
|
|
71
|
+
- ✅ Error messages and stack traces
|
|
72
|
+
- ✅ Git information (branch, commit, author)
|
|
73
|
+
- ✅ Test videos (when enabled)
|
|
74
|
+
- ✅ Screenshots (when enabled)
|
|
75
|
+
- ✅ Browser and environment details
|
|
76
|
+
|
|
77
|
+
## Playwright Configuration
|
|
78
|
+
|
|
79
|
+
The installer automatically configures your `playwright.config.ts`. If you created the config manually, ensure it includes `@muuktest/amikoo-reporter` as well as any other reporter you previously used in reporter property:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { defineConfig } from '@playwright/test';
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
reporter: [['@muuktest/amikoo-reporter']],
|
|
86
|
+
use: {
|
|
87
|
+
video: 'on', // Optional: capture videos
|
|
88
|
+
screenshot: 'on' // Optional: capture screenshots
|
|
89
|
+
},
|
|
90
|
+
// ... rest of your config
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Troubleshooting
|
|
95
|
+
|
|
96
|
+
### Reporter not found
|
|
97
|
+
Make sure the `@muuktest/amikoo-reporter` folder exists in your node-modules project root. Run `npm install @muuktest/amikoo-reporter --save-dev` to reinstall.
|
|
98
|
+
|
|
99
|
+
### API key errors
|
|
100
|
+
Verify your `.env` file contains a valid `AMIKOO_KEY`.
|
|
101
|
+
|
|
102
|
+
### Videos not uploading
|
|
103
|
+
Ensure `video: 'on'` is set in your Playwright config's `use` section.
|
|
104
|
+
|
|
105
|
+
## Advanced Configuration
|
|
106
|
+
|
|
107
|
+
### Multiple Reporters
|
|
108
|
+
|
|
109
|
+
You can use Amikoo reporter alongside other reporters:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
reporter: [
|
|
114
|
+
['list'],
|
|
115
|
+
['html'],
|
|
116
|
+
['@muuktest/amikoo-reporter']
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Test Directory
|
|
122
|
+
|
|
123
|
+
If your tests are not in the `./tests` folder:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export default defineConfig({
|
|
127
|
+
testDir: './e2e', // or your custom path
|
|
128
|
+
reporter: [['@muuktest/amikoo-reporter']],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Support
|
|
133
|
+
|
|
134
|
+
For issues and questions:
|
|
135
|
+
- GitHub Issues: https://github.com/muuklabs/controlhub-reporter/issues
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
ISC
|
|
140
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
2
|
+
interface ApiRequestOptions {
|
|
3
|
+
method: HttpMethod;
|
|
4
|
+
endpoint: string;
|
|
5
|
+
token: string;
|
|
6
|
+
body?: any;
|
|
7
|
+
}
|
|
8
|
+
interface ApiResponse<T = any> {
|
|
9
|
+
success: boolean;
|
|
10
|
+
data?: T;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Makes an API request to the amikoo-reporter backend
|
|
15
|
+
* @param options - The request options including method, endpoint, token, and optional body
|
|
16
|
+
* @returns The API response with success flag and data/error
|
|
17
|
+
*/
|
|
18
|
+
export declare function apiRequest<T = any>(options: ApiRequestOptions): Promise<ApiResponse<T>>;
|
|
19
|
+
export declare const api: {
|
|
20
|
+
get: <T = any>(endpoint: string, token: string) => Promise<ApiResponse<T>>;
|
|
21
|
+
post: <T = any>(endpoint: string, token: string, body?: any) => Promise<ApiResponse<T>>;
|
|
22
|
+
put: <T = any>(endpoint: string, token: string, body?: any) => Promise<ApiResponse<T>>;
|
|
23
|
+
delete: <T = any>(endpoint: string, token: string) => Promise<ApiResponse<T>>;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=apiUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiUtils.d.ts","sourceRoot":"","sources":["../../controlHub/apiUtils.ts"],"names":[],"mappings":"AAMA,KAAK,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D,UAAU,iBAAiB;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,UAAU,WAAW,CAAC,CAAC,GAAG,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CA2C7F;AAGD,eAAO,MAAM,GAAG;UACR,CAAC,kBAAkB,MAAM,SAAS,MAAM;WAGvC,CAAC,kBAAkB,MAAM,SAAS,MAAM,SAAS,GAAG;UAGrD,CAAC,kBAAkB,MAAM,SAAS,MAAM,SAAS,GAAG;aAGjD,CAAC,kBAAkB,MAAM,SAAS,MAAM;CAElD,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.api = void 0;
|
|
7
|
+
exports.apiRequest = apiRequest;
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
dotenv_1.default.config();
|
|
10
|
+
// Get the API URL and key from environment variables, with defaults for production
|
|
11
|
+
const BASE_URL = process.env.CONTROLHUB_URL || 'https://app.amikoo.ai';
|
|
12
|
+
/**
|
|
13
|
+
* Makes an API request to the amikoo-reporter backend
|
|
14
|
+
* @param options - The request options including method, endpoint, token, and optional body
|
|
15
|
+
* @returns The API response with success flag and data/error
|
|
16
|
+
*/
|
|
17
|
+
async function apiRequest(options) {
|
|
18
|
+
const { method, endpoint, token, body } = options;
|
|
19
|
+
const url = `${BASE_URL}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
|
|
20
|
+
const headers = {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
};
|
|
23
|
+
if (token) {
|
|
24
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const fetchOptions = {
|
|
28
|
+
method,
|
|
29
|
+
headers,
|
|
30
|
+
};
|
|
31
|
+
if (body && method !== 'GET') {
|
|
32
|
+
fetchOptions.body = JSON.stringify(body);
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(url, fetchOptions);
|
|
35
|
+
const responseData = await response.json();
|
|
36
|
+
if (!response.ok || !responseData.success) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: responseData.data || responseData.message || 'Request failed',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
data: responseData.data,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: error instanceof Error ? error.message : String(error),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Convenience methods
|
|
55
|
+
exports.api = {
|
|
56
|
+
get: (endpoint, token) => apiRequest({ method: 'GET', endpoint, token }),
|
|
57
|
+
post: (endpoint, token, body) => apiRequest({ method: 'POST', endpoint, token, body }),
|
|
58
|
+
put: (endpoint, token, body) => apiRequest({ method: 'PUT', endpoint, token, body }),
|
|
59
|
+
delete: (endpoint, token) => apiRequest({ method: 'DELETE', endpoint, token }),
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=apiUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiUtils.js","sourceRoot":"","sources":["../../controlHub/apiUtils.ts"],"names":[],"mappings":";;;;;;AA0BA,gCA2CC;AArED,oDAA4B;AAC5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,mFAAmF;AACnF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,uBAAuB,CAAC;AAiBvE;;;;GAIG;AACI,KAAK,UAAU,UAAU,CAAU,OAA0B;IAClE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAElD,MAAM,GAAG,GAAG,GAAG,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,EAAE,CAAC;IAEjF,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IAEF,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,YAAY,GAAgB;YAChC,MAAM;YACN,OAAO;SACR,CAAC;QAEF,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAC7B,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAChD,MAAM,YAAY,GAAQ,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAEhD,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;YAC1C,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,YAAY,CAAC,IAAI,IAAI,YAAY,CAAC,OAAO,IAAI,gBAAgB;aACrE,CAAC;QACJ,CAAC;QAED,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,YAAY,CAAC,IAAI;SACxB,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,sBAAsB;AACT,QAAA,GAAG,GAAG;IACjB,GAAG,EAAE,CAAU,QAAgB,EAAE,KAAa,EAAE,EAAE,CAChD,UAAU,CAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAEnD,IAAI,EAAE,CAAU,QAAgB,EAAE,KAAa,EAAE,IAAU,EAAE,EAAE,CAC7D,UAAU,CAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE1D,GAAG,EAAE,CAAU,QAAgB,EAAE,KAAa,EAAE,IAAU,EAAE,EAAE,CAC5D,UAAU,CAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAEzD,MAAM,EAAE,CAAU,QAAgB,EAAE,KAAa,EAAE,EAAE,CACnD,UAAU,CAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;CACvD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Reporter } from '@playwright/test/reporter';
|
|
2
|
+
declare class MyReporter implements Reporter {
|
|
3
|
+
testExecutionData: any[];
|
|
4
|
+
videos: any[];
|
|
5
|
+
authInfo: any;
|
|
6
|
+
organizationId: string;
|
|
7
|
+
executionNumber: number;
|
|
8
|
+
hashIds: any[];
|
|
9
|
+
startExecutionTime: number;
|
|
10
|
+
browser: String;
|
|
11
|
+
compilationError: boolean;
|
|
12
|
+
filesWithCompilationError: any[];
|
|
13
|
+
url: any;
|
|
14
|
+
testEndPromises: any[];
|
|
15
|
+
private repositoryId;
|
|
16
|
+
private branch;
|
|
17
|
+
private repositoryName;
|
|
18
|
+
private access_token;
|
|
19
|
+
private owner;
|
|
20
|
+
private key;
|
|
21
|
+
onBegin(config: any, suite: any): Promise<void>;
|
|
22
|
+
onTestBegin(test: any): Promise<void>;
|
|
23
|
+
onError(error: any): Promise<void>;
|
|
24
|
+
onTestEnd(test: any, result: any): Promise<void>;
|
|
25
|
+
onEnd(result: any): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export default MyReporter;
|
|
28
|
+
//# sourceMappingURL=controlHubReporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controlHubReporter.d.ts","sourceRoot":"","sources":["../../controlHub/controlHubReporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAQrD,cAAM,UAAW,YAAW,QAAQ;IAElC,iBAAiB,EAAE,GAAG,EAAE,CAAM;IAC9B,MAAM,EAAE,GAAG,EAAE,CAAM;IACnB,QAAQ,EAAE,GAAG,CAAC;IACd,cAAc,EAAE,MAAM,CAAM;IAC5B,eAAe,EAAE,MAAM,CAAK;IAC5B,OAAO,EAAE,GAAG,EAAE,CAAM;IACpB,kBAAkB,EAAE,MAAM,CAAK;IAC/B,OAAO,EAAE,MAAM,CAAgB;IAC/B,gBAAgB,EAAE,OAAO,CAAS;IAClC,yBAAyB,EAAE,GAAG,EAAE,CAAM;IACtC,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,GAAG,EAAE,CAAM;IAE5B,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,GAAG,CAAU;IAEf,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IAqC/B,WAAW,CAAC,IAAI,EAAE,GAAG;IAIrB,OAAO,CAAC,KAAK,EAAE,GAAG;IAYlB,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IA6ChC,KAAK,CAAC,MAAM,EAAE,GAAG;CAqCxB;AACD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const gitUtils_1 = require("./gitUtils");
|
|
7
|
+
const apiUtils_1 = require("./apiUtils");
|
|
8
|
+
const videoUtils_1 = require("./videoUtils");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
class MyReporter {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.testExecutionData = [];
|
|
15
|
+
this.videos = [];
|
|
16
|
+
this.organizationId = '';
|
|
17
|
+
this.executionNumber = 0;
|
|
18
|
+
this.hashIds = [];
|
|
19
|
+
this.startExecutionTime = 0;
|
|
20
|
+
this.browser = 'chromeTest';
|
|
21
|
+
this.compilationError = false;
|
|
22
|
+
this.filesWithCompilationError = [];
|
|
23
|
+
this.testEndPromises = [];
|
|
24
|
+
}
|
|
25
|
+
async onBegin(config, suite) {
|
|
26
|
+
// We need to obtain a token from control hub so we can send API requests.
|
|
27
|
+
this.key = process.env.AMIKOO_KEY || '';
|
|
28
|
+
const tokenReponse = await apiUtils_1.api.post('/validate_key', '', { "key": this.key });
|
|
29
|
+
if (tokenReponse.success && tokenReponse.data) {
|
|
30
|
+
this.access_token = tokenReponse.data.access_token;
|
|
31
|
+
}
|
|
32
|
+
const context = await (0, gitUtils_1.getGitContext)(this.access_token);
|
|
33
|
+
this.repositoryId = context.repositoryId;
|
|
34
|
+
this.repositoryName = context.repositoryName;
|
|
35
|
+
this.branch = context.branch;
|
|
36
|
+
this.owner = context.owner;
|
|
37
|
+
if (!this.access_token) {
|
|
38
|
+
console.warn('Warning: failed to obtain access token');
|
|
39
|
+
console.warn('Feedback data will not be sent to amikoo-reporter, and execution data will not be saved.');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// We need to call the API to get the execution number here, so we can include it in the feedback data.
|
|
43
|
+
const executionResponse = await apiUtils_1.api.get('/execution/execution_number', this.access_token);
|
|
44
|
+
if (executionResponse.success && executionResponse.data) {
|
|
45
|
+
this.executionNumber = executionResponse?.data?.executionNumber;
|
|
46
|
+
this.organizationId = executionResponse?.data?.organizationId;
|
|
47
|
+
console.log('Retrieved execution number:', this.executionNumber);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.error('Failed to retrieve execution number', executionResponse.error);
|
|
51
|
+
}
|
|
52
|
+
console.log(`Starting the test run for branch ${this.branch} in repository ${this.repositoryName}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async onTestBegin(test) {
|
|
56
|
+
console.log(`Starting test ${test.title}`);
|
|
57
|
+
}
|
|
58
|
+
async onError(error) {
|
|
59
|
+
console.log("Tests generated an error during execution. Error = ", error);
|
|
60
|
+
const message = error?.message?.replace(/^(.*: )+/, '').trim();
|
|
61
|
+
this.compilationError = true;
|
|
62
|
+
if (error?.location?.file) {
|
|
63
|
+
this.filesWithCompilationError.push({
|
|
64
|
+
"file": error.location.file,
|
|
65
|
+
"message": message,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async onTestEnd(test, result) {
|
|
70
|
+
console.log(`Finished test ${test.title} with status ${result.status}`);
|
|
71
|
+
const promise = new Promise(async (resolve, reject) => {
|
|
72
|
+
try {
|
|
73
|
+
// Get clean title path without project name and empty strings
|
|
74
|
+
const filePath = path_1.default.basename(test.location.file);
|
|
75
|
+
const fileLocation = path_1.default.relative(process.cwd(), test.location.file);
|
|
76
|
+
const fullTitle = await (0, utils_1.getSuiteNames)(test);
|
|
77
|
+
// Create a unique hash ID for the test using repository ID, file path, full title, and commit SHA
|
|
78
|
+
const rawIdentity = `${this.owner}/${this.repositoryId}:${fileLocation}:${fullTitle}`;
|
|
79
|
+
const testId = crypto_1.default.createHash('sha256').update(rawIdentity).digest('hex');
|
|
80
|
+
const duration = parseInt(result.duration) / 1000;
|
|
81
|
+
const payload = {
|
|
82
|
+
hashId: testId,
|
|
83
|
+
filePath,
|
|
84
|
+
fullTitle,
|
|
85
|
+
duration: duration,
|
|
86
|
+
executionAt: new Date().toISOString(),
|
|
87
|
+
result: result.status === "passed" ? true : false
|
|
88
|
+
};
|
|
89
|
+
this.testExecutionData.push(payload);
|
|
90
|
+
// Collect video for later batch processing
|
|
91
|
+
const videoPath = (0, videoUtils_1.getVideoPath)(result);
|
|
92
|
+
if (videoPath) {
|
|
93
|
+
this.videos.push({ path: videoPath, testId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.log("Error processing test end: ", error);
|
|
98
|
+
reject(error);
|
|
99
|
+
}
|
|
100
|
+
resolve(true);
|
|
101
|
+
});
|
|
102
|
+
// save the promise so the onEnd can wait for this code to complete.
|
|
103
|
+
this.testEndPromises.push(promise);
|
|
104
|
+
}
|
|
105
|
+
async onEnd(result) {
|
|
106
|
+
console.log('Wait for all tests to complete.');
|
|
107
|
+
await Promise.all(this.testEndPromises);
|
|
108
|
+
console.log('All tests have ended, sending execution report.');
|
|
109
|
+
if (this.executionNumber) {
|
|
110
|
+
// Process videos and get presigned URLs
|
|
111
|
+
const videoResult = await (0, videoUtils_1.processVideos)(this.videos, this.organizationId, this.executionNumber, this.access_token);
|
|
112
|
+
// Upload videos to S3
|
|
113
|
+
if (videoResult?.uploadUrls) {
|
|
114
|
+
await (0, videoUtils_1.uploadVideosToS3)(videoResult.videos, videoResult.uploadUrls);
|
|
115
|
+
}
|
|
116
|
+
const feedbackData = {
|
|
117
|
+
repositoryId: this.repositoryId,
|
|
118
|
+
branch: this.branch,
|
|
119
|
+
tests: this.testExecutionData,
|
|
120
|
+
executionNumber: this.executionNumber,
|
|
121
|
+
videos: videoResult?.videos.map(v => v.s3FileName) || [], // Include video file names in feedback
|
|
122
|
+
};
|
|
123
|
+
// Send API to BE here
|
|
124
|
+
const feedbackResponse = await apiUtils_1.api.post('/execution/feedback', this.access_token, feedbackData);
|
|
125
|
+
if (feedbackResponse.success) {
|
|
126
|
+
console.log('Execution report sent successfully');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.error('Failed to send execution report', feedbackResponse.error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.warn('Execution number not available. Execution report will not be sent to amikoo-reporter, and execution data will not be saved.');
|
|
134
|
+
}
|
|
135
|
+
(0, utils_1.checkForUpdates)();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.default = MyReporter;
|
|
139
|
+
//# sourceMappingURL=controlHubReporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controlHubReporter.js","sourceRoot":"","sources":["../../controlHub/controlHubReporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitUtils.d.ts","sourceRoot":"","sources":["../../controlHub/gitUtils.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAmCD,wBAAsB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB7E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getGitContext = getGitContext;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
const apiUtils_1 = require("./apiUtils");
|
|
10
|
+
dotenv_1.default.config();
|
|
11
|
+
/**
|
|
12
|
+
* Parses a git remote URL to extract owner and repo name.
|
|
13
|
+
* Supports both HTTPS and SSH formats:
|
|
14
|
+
* - https://github.com/owner/repo.git
|
|
15
|
+
* - git@github.com:owner/repo.git
|
|
16
|
+
*/
|
|
17
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
18
|
+
const response = {
|
|
19
|
+
owner: '',
|
|
20
|
+
repo: ''
|
|
21
|
+
};
|
|
22
|
+
// Remove trailing .git if present
|
|
23
|
+
const cleanUrl = remoteUrl.replace(/\.git$/, '');
|
|
24
|
+
// Match HTTPS format: https://github.com/owner/repo
|
|
25
|
+
const httpsMatch = cleanUrl.match(/https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)/);
|
|
26
|
+
if (httpsMatch) {
|
|
27
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
28
|
+
}
|
|
29
|
+
// Match SSH format: git@github.com:owner/repo
|
|
30
|
+
const sshMatch = cleanUrl.match(/git@[^:]+:([^\/]+)\/(.+)/);
|
|
31
|
+
if (sshMatch) {
|
|
32
|
+
response.owner = sshMatch[1];
|
|
33
|
+
response.repo = sshMatch[2];
|
|
34
|
+
}
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
// This function retrieves the current git context, including the repository ID, branch name, and commit SHA.
|
|
38
|
+
// It uses the GitHub API to fetch the repository ID based on the owner and repo name extracted from the git remote URL.
|
|
39
|
+
async function getGitContext(access_token) {
|
|
40
|
+
const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim() || 'unknown-branch';
|
|
41
|
+
// Get owner and repo from git remote URL
|
|
42
|
+
const remoteUrl = (0, child_process_1.execSync)('git config --get remote.origin.url').toString().trim();
|
|
43
|
+
const { owner, repo } = parseGitRemoteUrl(remoteUrl);
|
|
44
|
+
const body = {
|
|
45
|
+
"owner": owner,
|
|
46
|
+
"name": repo
|
|
47
|
+
};
|
|
48
|
+
const response = await apiUtils_1.api.post('/repository/get', access_token, body);
|
|
49
|
+
const repoData = await response?.data;
|
|
50
|
+
return {
|
|
51
|
+
repositoryId: repoData?.id?.toString() || "0",
|
|
52
|
+
repositoryName: repo,
|
|
53
|
+
branch,
|
|
54
|
+
owner: owner || 'unknown-owner'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=gitUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitUtils.js","sourceRoot":"","sources":["../../controlHub/gitUtils.ts"],"names":[],"mappings":";;;;;AA8CA,sCAmBC;AAjED,iDAAyC;AACzC,oDAA4B;AAC5B,yCAAiC;AAEjC,gBAAM,CAAC,MAAM,EAAE,CAAC;AAShB;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,SAAiB;IAE1C,MAAM,QAAQ,GAAG;QACf,KAAK,EAAE,EAAE;QACT,IAAI,EAAE,EAAE;KACT,CAAA;IACD,kCAAkC;IAClC,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAEjD,oDAAoD;IACpD,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3E,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,8CAA8C;IAC9C,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC5D,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC7B,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,6GAA6G;AAC7G,wHAAwH;AACjH,KAAK,UAAU,aAAa,CAAC,YAAoB;IACtD,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,gBAAgB,CAAC;IAEjG,yCAAyC;IACzC,MAAM,SAAS,GAAG,IAAA,wBAAQ,EAAC,oCAAoC,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IACnF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG;QACX,OAAO,EAAE,KAAK;QACd,MAAM,EAAE,IAAI;KACb,CAAA;IACD,MAAM,QAAQ,GAAG,MAAM,cAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,YAAY,EAAG,IAAI,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,QAAQ,EAAE,IAAI,CAAC;IACtC,OAAO;QACL,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,GAAG;QAC7C,cAAc,EAAE,IAAI;QACpB,MAAM;QACN,KAAK,EAAE,KAAK,IAAI,eAAe;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../controlHub/utils.ts"],"names":[],"mappings":"AAUA,wBAAsB,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAU9D;AAGD,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CA+B/C"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSuiteNames = getSuiteNames;
|
|
7
|
+
exports.checkForUpdates = checkForUpdates;
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
// getSuiteNames
|
|
12
|
+
// Builds the full test title path by traversing parent suites
|
|
13
|
+
// Parameters:
|
|
14
|
+
// test: any - Playwright test object with parent references
|
|
15
|
+
// Returns: string - Full title path joined by ' > ' (e.g., "Suite > Nested > Test Name")
|
|
16
|
+
async function getSuiteNames(test) {
|
|
17
|
+
const suites = [];
|
|
18
|
+
let parent = test.parent;
|
|
19
|
+
while (parent) {
|
|
20
|
+
if (parent._type === 'describe' && parent.title)
|
|
21
|
+
suites.unshift(parent.title);
|
|
22
|
+
parent = parent.parent;
|
|
23
|
+
}
|
|
24
|
+
return [...suites, test.title].join(' > ');
|
|
25
|
+
}
|
|
26
|
+
function checkForUpdates() {
|
|
27
|
+
const pkgPath = path_1.default.resolve(__dirname, '..', '..', 'package.json');
|
|
28
|
+
const currentVersion = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8')).version;
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
try {
|
|
31
|
+
const [curMajor, curMinor] = currentVersion.split('.').map(Number);
|
|
32
|
+
const req = https_1.default.get('https://registry.npmjs.org/@muuktest%2famikoo-reporter/latest', { timeout: 5000 }, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
try {
|
|
37
|
+
const latest = JSON.parse(data).version;
|
|
38
|
+
if (!latest) {
|
|
39
|
+
resolve();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const [latMajor, latMinor] = latest.split('.').map(Number);
|
|
43
|
+
if (latMajor > curMajor || (latMajor === curMajor && latMinor > curMinor)) {
|
|
44
|
+
console.log(`\n A new version of @muuktest/amikoo-reporter is available: ${currentVersion} → ${latest}`);
|
|
45
|
+
console.log(` Run "npm install @muuktest/amikoo-reporter@latest" to update.\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore parse errors */ }
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
req.on('error', () => resolve());
|
|
53
|
+
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../controlHub/utils.ts"],"names":[],"mappings":";;;;;AAUA,sCAUC;AAGD,0CA+BC;AAtDD,kDAA0B;AAC1B,gDAAwB;AACxB,4CAAoB;AAGpB,gBAAgB;AAChB,8DAA8D;AAC9D,cAAc;AACd,8DAA8D;AAC9D,yFAAyF;AAClF,KAAK,UAAU,aAAa,CAAC,IAAS;IAC3C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAEzB,OAAO,MAAM,EAAE,CAAC;QACd,IAAI,MAAM,CAAC,KAAK,KAAK,UAAU,IAAI,MAAM,CAAC,KAAK;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9E,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IACzB,CAAC;IAED,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7C,CAAC;AAGD,SAAgB,eAAe;IAC7B,MAAM,OAAO,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IACpE,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,YAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAE5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAEnE,MAAM,GAAG,GAAG,eAAK,CAAC,GAAG,CAAC,+DAA+D,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE;gBAChH,IAAI,IAAI,GAAG,EAAE,CAAC;gBACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;oBACjB,IAAI,CAAC;wBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;wBACxC,IAAI,CAAC,MAAM,EAAE,CAAC;4BAAC,OAAO,EAAE,CAAC;4BAAC,OAAO;wBAAC,CAAC;wBACnC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;wBAE3D,IAAI,QAAQ,GAAG,QAAQ,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;4BAC1E,OAAO,CAAC,GAAG,CAAC,gEAAgE,cAAc,MAAM,MAAM,EAAE,CAAC,CAAC;4BAC1G,OAAO,CAAC,GAAG,CAAC,mEAAmE,CAAC,CAAC;wBACnF,CAAC;oBACH,CAAC;oBAAC,MAAM,CAAC,CAAC,yBAAyB,CAAC,CAAC;oBACrC,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;YACjC,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets video path from test result attachments
|
|
3
|
+
*/
|
|
4
|
+
export declare function getVideoPath(result: any): string;
|
|
5
|
+
/**
|
|
6
|
+
* Processes videos and requests presigned URLs for upload
|
|
7
|
+
*/
|
|
8
|
+
export declare function processVideos(videos: any[], organizationId: string, executionNumber: number, token: string): Promise<{
|
|
9
|
+
videos: any[];
|
|
10
|
+
uploadUrls: any[] | null;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Uploads all processed videos to S3 using their presigned URLs
|
|
14
|
+
*/
|
|
15
|
+
export declare function uploadVideosToS3(videos: any[], uploadUrls: any[]): Promise<any[]>;
|
|
16
|
+
//# sourceMappingURL=videoUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"videoUtils.d.ts","sourceRoot":"","sources":["../../controlHub/videoUtils.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAGhD;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,GAAG,EAAE,EACb,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAAC,UAAU,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;CAAE,CAAC,CA0BtD;AA8BD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CA8BvF"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getVideoPath = getVideoPath;
|
|
7
|
+
exports.processVideos = processVideos;
|
|
8
|
+
exports.uploadVideosToS3 = uploadVideosToS3;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const apiUtils_1 = require("./apiUtils");
|
|
11
|
+
/**
|
|
12
|
+
* Gets video path from test result attachments
|
|
13
|
+
*/
|
|
14
|
+
function getVideoPath(result) {
|
|
15
|
+
const attachment = result.attachments?.find((a) => a.name === 'video' || a.path?.endsWith('.webm'));
|
|
16
|
+
return attachment?.path || '';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Processes videos and requests presigned URLs for upload
|
|
20
|
+
*/
|
|
21
|
+
async function processVideos(videos, organizationId, executionNumber, token) {
|
|
22
|
+
let processed = [];
|
|
23
|
+
let uploadUrls = null;
|
|
24
|
+
if (videos.length && organizationId && executionNumber) {
|
|
25
|
+
// Map videos to their S3 filenames (no local rename)
|
|
26
|
+
processed = videos.filter(v => v.path && fs_1.default.existsSync(v.path))
|
|
27
|
+
.map(v => ({
|
|
28
|
+
localPath: v.path,
|
|
29
|
+
s3FileName: `${executionNumber}_${v.testId}.webm`,
|
|
30
|
+
}));
|
|
31
|
+
if (processed.length) {
|
|
32
|
+
// Request presigned URLs with desired S3 filenames
|
|
33
|
+
const response = await apiUtils_1.api.post('/execution/batch_upload_urls', token, {
|
|
34
|
+
files: processed.map(v => ({ fileName: v.s3FileName, contentType: 'video/webm', folder: `videos/${organizationId}` })),
|
|
35
|
+
});
|
|
36
|
+
uploadUrls = response.success ? response.data : null;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log('No videos found to process');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log('Missing required parameters for video processing');
|
|
44
|
+
}
|
|
45
|
+
return { videos: processed, uploadUrls };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Uploads a single video file to S3 using a presigned URL
|
|
49
|
+
*/
|
|
50
|
+
async function uploadVideoToS3(filePath, uploadUrl) {
|
|
51
|
+
let success = false;
|
|
52
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
53
|
+
//try {
|
|
54
|
+
const fileBuffer = fs_1.default.readFileSync(filePath);
|
|
55
|
+
const response = await fetch(uploadUrl, {
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
body: fileBuffer,
|
|
58
|
+
headers: { 'Content-Type': 'video/webm' },
|
|
59
|
+
});
|
|
60
|
+
success = response.ok;
|
|
61
|
+
if (!success) {
|
|
62
|
+
console.log('Upload failed with status:', response.status);
|
|
63
|
+
}
|
|
64
|
+
/* } catch (error) {
|
|
65
|
+
console.log('Error uploading video:', error);
|
|
66
|
+
}*/
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log('Video file not found:', filePath);
|
|
70
|
+
}
|
|
71
|
+
return success;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Uploads all processed videos to S3 using their presigned URLs
|
|
75
|
+
*/
|
|
76
|
+
async function uploadVideosToS3(videos, uploadUrls) {
|
|
77
|
+
const results = [];
|
|
78
|
+
console.log(`Starting upload of ${videos.length} videos to S3`);
|
|
79
|
+
if (videos.length && uploadUrls?.length) {
|
|
80
|
+
// Create a map for quick lookup by S3 filename
|
|
81
|
+
const urlMap = new Map(uploadUrls.map(u => [u.fileName, u.uploadUrl]));
|
|
82
|
+
// Upload all videos in parallel
|
|
83
|
+
const uploadPromises = videos.map(async (video) => {
|
|
84
|
+
const uploadUrl = urlMap.get(video.s3FileName);
|
|
85
|
+
if (uploadUrl) {
|
|
86
|
+
const success = await uploadVideoToS3(video.localPath, uploadUrl);
|
|
87
|
+
return { fileName: video.s3FileName, success };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log('No upload URL found for:', video.s3FileName);
|
|
91
|
+
return { fileName: video.s3FileName, success: false, error: 'No upload URL' };
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const uploadResults = await Promise.all(uploadPromises);
|
|
95
|
+
results.push(...uploadResults);
|
|
96
|
+
const successCount = results.filter(r => r.success).length;
|
|
97
|
+
console.log(`Uploaded ${successCount}/${results.length} videos to S3`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log('No videos or upload URLs provided');
|
|
101
|
+
}
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=videoUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"videoUtils.js","sourceRoot":"","sources":["../../controlHub/videoUtils.ts"],"names":[],"mappings":"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"}
|
package/install.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const userProjectRoot = process.env.INIT_CWD || process.cwd();
|
|
7
|
+
const packageRoot = __dirname;
|
|
8
|
+
|
|
9
|
+
const REPORTER_MODULE = '@muuktest/amikoo-reporter';
|
|
10
|
+
const REPORTER_ENTRY = `['${REPORTER_MODULE}']`;
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function isInsideBlockComment(content, pos) {
|
|
17
|
+
let inBlock = false;
|
|
18
|
+
for (let i = 0; i < pos - 1; i++) {
|
|
19
|
+
if (content[i] === '/' && content[i + 1] === '*') { inBlock = true; i++; continue; }
|
|
20
|
+
if (content[i] === '*' && content[i + 1] === '/') { inBlock = false; i++; continue; }
|
|
21
|
+
}
|
|
22
|
+
return inBlock;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isLineComment(content, pos) {
|
|
26
|
+
const lineStart = content.lastIndexOf('\n', pos) + 1;
|
|
27
|
+
const before = content.substring(lineStart, pos);
|
|
28
|
+
return /\/\//.test(before);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isCommented(content, pos) {
|
|
32
|
+
return isLineComment(content, pos) || isInsideBlockComment(content, pos);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find the first uncommented `reporter` property (e.g. `reporter:` or `reporter :`)
|
|
37
|
+
*/
|
|
38
|
+
function findUncommentedReporter(content) {
|
|
39
|
+
const regex = /reporter\s*:/g;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = regex.exec(content)) !== null) {
|
|
42
|
+
if (!isCommented(content, match.index)) return match;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bracket-counting that respects strings to find the matching `]`.
|
|
49
|
+
* `openPos` must point at the opening `[`.
|
|
50
|
+
*/
|
|
51
|
+
function findMatchingBracket(content, openPos) {
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let stringChar = '';
|
|
55
|
+
|
|
56
|
+
for (let i = openPos; i < content.length; i++) {
|
|
57
|
+
const ch = content[i];
|
|
58
|
+
|
|
59
|
+
if (inString) {
|
|
60
|
+
if (ch === '\\') { i++; continue; }
|
|
61
|
+
if (ch === stringChar) inString = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
66
|
+
inString = true;
|
|
67
|
+
stringChar = ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (ch === '[') depth++;
|
|
72
|
+
if (ch === ']') {
|
|
73
|
+
depth--;
|
|
74
|
+
if (depth === 0) return i;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Bracket-counting that respects strings to find the matching `}`.
|
|
82
|
+
* `openPos` must point at the opening `{`.
|
|
83
|
+
*/
|
|
84
|
+
function findMatchingBrace(content, openPos) {
|
|
85
|
+
let depth = 0;
|
|
86
|
+
let inString = false;
|
|
87
|
+
let stringChar = '';
|
|
88
|
+
|
|
89
|
+
for (let i = openPos; i < content.length; i++) {
|
|
90
|
+
const ch = content[i];
|
|
91
|
+
|
|
92
|
+
if (inString) {
|
|
93
|
+
if (ch === '\\') { i++; continue; }
|
|
94
|
+
if (ch === stringChar) inString = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
99
|
+
inString = true;
|
|
100
|
+
stringChar = ch;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ch === '{') depth++;
|
|
105
|
+
if (ch === '}') {
|
|
106
|
+
depth--;
|
|
107
|
+
if (depth === 0) return i;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return -1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Find the first uncommented top-level `use` property (e.g. `use: {`)
|
|
115
|
+
*/
|
|
116
|
+
function findUncommentedUse(content) {
|
|
117
|
+
const regex = /\buse\s*:\s*\{/g;
|
|
118
|
+
let match;
|
|
119
|
+
while ((match = regex.exec(content)) !== null) {
|
|
120
|
+
if (!isCommented(content, match.index)) return match;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detect the indentation used for top-level config properties.
|
|
127
|
+
*/
|
|
128
|
+
function detectConfigIndent(content) {
|
|
129
|
+
const match = content.match(/^(\s+)(testDir|reporter|use|workers|retries|fullyParallel|forbidOnly|projects)\s*:/m);
|
|
130
|
+
return match ? match[1] : ' ';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Strategy 1 – reporter is an array: append our entry
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function tryArrayAppend(content) {
|
|
138
|
+
const match = findUncommentedReporter(content);
|
|
139
|
+
if (!match) return null;
|
|
140
|
+
|
|
141
|
+
// Look for `[` after `reporter:`
|
|
142
|
+
const afterReporter = content.substring(match.index + match[0].length);
|
|
143
|
+
const bracketOffset = afterReporter.search(/\S/);
|
|
144
|
+
if (bracketOffset === -1 || afterReporter[bracketOffset] !== '[') return null;
|
|
145
|
+
|
|
146
|
+
const absOpenPos = match.index + match[0].length + bracketOffset;
|
|
147
|
+
const absClosePos = findMatchingBracket(content, absOpenPos);
|
|
148
|
+
if (absClosePos === -1) return null;
|
|
149
|
+
|
|
150
|
+
const indent = detectConfigIndent(content);
|
|
151
|
+
const innerIndent = indent + ' ';
|
|
152
|
+
|
|
153
|
+
const before = content.substring(0, absClosePos).trimEnd();
|
|
154
|
+
const after = content.substring(absClosePos);
|
|
155
|
+
|
|
156
|
+
// Determine if we need a comma after existing entries
|
|
157
|
+
const needsComma = /[\]\}'"`\w]$/.test(before);
|
|
158
|
+
|
|
159
|
+
return before + (needsComma ? ',' : '') + '\n' + innerIndent + REPORTER_ENTRY + '\n' + indent + after;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Strategy 2 – reporter is a single string: convert to array
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
function tryStringToArray(content) {
|
|
167
|
+
const match = findUncommentedReporter(content);
|
|
168
|
+
if (!match) return null;
|
|
169
|
+
|
|
170
|
+
// After `reporter:`, expect a string value (not `[`)
|
|
171
|
+
const afterReporter = content.substring(match.index + match[0].length);
|
|
172
|
+
const firstNonSpace = afterReporter.search(/\S/);
|
|
173
|
+
if (firstNonSpace === -1) return null;
|
|
174
|
+
const ch = afterReporter[firstNonSpace];
|
|
175
|
+
if (ch !== "'" && ch !== '"') return null;
|
|
176
|
+
|
|
177
|
+
// Extract the string value
|
|
178
|
+
const strMatch = afterReporter.match(/^\s*(['"])([^'"]+)\1/);
|
|
179
|
+
if (!strMatch) return null;
|
|
180
|
+
|
|
181
|
+
const existing = strMatch[2];
|
|
182
|
+
const indent = detectConfigIndent(content);
|
|
183
|
+
const innerIndent = indent + ' ';
|
|
184
|
+
|
|
185
|
+
const fullMatchStr = match[0] + strMatch[0];
|
|
186
|
+
const replacement = match[0].trimEnd() + ' [\n' +
|
|
187
|
+
innerIndent + `['${existing}'],\n` +
|
|
188
|
+
innerIndent + REPORTER_ENTRY + '\n' +
|
|
189
|
+
indent + ']';
|
|
190
|
+
|
|
191
|
+
return content.substring(0, match.index) + replacement + content.substring(match.index + fullMatchStr.length);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Strategy 3 – reporter is commented out (only commented occurrences exist)
|
|
196
|
+
// Strategy 4 – no reporter property at all
|
|
197
|
+
// Both delegate to addReporterProperty()
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
function addReporterProperty(content) {
|
|
201
|
+
const indent = detectConfigIndent(content);
|
|
202
|
+
const reporterLine = `${indent}reporter: [${REPORTER_ENTRY}],`;
|
|
203
|
+
|
|
204
|
+
// Try to insert after known config properties
|
|
205
|
+
const knownProps = ['workers', 'retries', 'forbidOnly', 'fullyParallel', 'testDir'];
|
|
206
|
+
for (const prop of knownProps) {
|
|
207
|
+
const regex = new RegExp(`^([ \\t]*${prop}\\s*:.*?,?)[ \\t]*$`, 'm');
|
|
208
|
+
const m = content.match(regex);
|
|
209
|
+
if (m && !isCommented(content, m.index)) {
|
|
210
|
+
const insertPos = m.index + m[0].length;
|
|
211
|
+
return content.substring(0, insertPos) + '\n' + reporterLine + content.substring(insertPos);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Try inserting after defineConfig({ or module.exports = { or export default {
|
|
216
|
+
const openers = [
|
|
217
|
+
/defineConfig\s*\(\s*\{/,
|
|
218
|
+
/module\.exports\s*=\s*\{/,
|
|
219
|
+
/export\s+default\s*\{/,
|
|
220
|
+
];
|
|
221
|
+
for (const opener of openers) {
|
|
222
|
+
const m = content.match(opener);
|
|
223
|
+
if (m) {
|
|
224
|
+
const insertPos = m.index + m[0].length;
|
|
225
|
+
return content.substring(0, insertPos) + '\n' + reporterLine + content.substring(insertPos);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function tryCommentedReporter(content) {
|
|
233
|
+
// If there IS an uncommented reporter, this strategy doesn't apply
|
|
234
|
+
if (findUncommentedReporter(content)) return null;
|
|
235
|
+
|
|
236
|
+
// Check that a commented-out reporter actually exists
|
|
237
|
+
const hasCommented = /\/\/.*reporter\s*:|\/\*[\s\S]*?reporter\s*:[\s\S]*?\*\//.test(content);
|
|
238
|
+
if (!hasCommented) return null;
|
|
239
|
+
|
|
240
|
+
return addReporterProperty(content);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function tryNoReporter(content) {
|
|
244
|
+
if (findUncommentedReporter(content)) return null;
|
|
245
|
+
return addReporterProperty(content);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Fallback – insert before the last closing of the config object
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
function tryFallback(content) {
|
|
253
|
+
const indent = detectConfigIndent(content);
|
|
254
|
+
const reporterLine = `${indent}reporter: [${REPORTER_ENTRY}],`;
|
|
255
|
+
|
|
256
|
+
// Try to find the last }); (defineConfig) or }; (module.exports)
|
|
257
|
+
const patterns = [/\}\s*\)\s*;?\s*$/, /\}\s*;?\s*$/];
|
|
258
|
+
for (const pattern of patterns) {
|
|
259
|
+
const m = content.match(pattern);
|
|
260
|
+
if (m) {
|
|
261
|
+
return content.substring(0, m.index) + reporterLine + '\n' + content.substring(m.index);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Absolute last resort: print manual instructions
|
|
266
|
+
console.error('\n==============================================');
|
|
267
|
+
console.error(' MANUAL CONFIGURATION REQUIRED');
|
|
268
|
+
console.error(' Add this to your playwright config reporter array:');
|
|
269
|
+
console.error(` reporter: [['${REPORTER_MODULE}']]`);
|
|
270
|
+
console.error('==============================================\n');
|
|
271
|
+
return content;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// ensureVideoOn – make sure video: 'on' is set inside the use: {} block
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function ensureVideoOn(content) {
|
|
279
|
+
const useMatch = findUncommentedUse(content);
|
|
280
|
+
|
|
281
|
+
if (useMatch) {
|
|
282
|
+
const bracePos = content.indexOf('{', useMatch.index + 3);
|
|
283
|
+
const closeBrace = findMatchingBrace(content, bracePos);
|
|
284
|
+
if (closeBrace === -1) return content;
|
|
285
|
+
|
|
286
|
+
const useBlock = content.substring(bracePos, closeBrace + 1);
|
|
287
|
+
|
|
288
|
+
// Look for an uncommented video property inside the use block
|
|
289
|
+
const videoRegex = /video\s*:\s*(['"])([^'"]*)\1/g;
|
|
290
|
+
let videoMatch;
|
|
291
|
+
let foundUncommented = false;
|
|
292
|
+
|
|
293
|
+
while ((videoMatch = videoRegex.exec(useBlock)) !== null) {
|
|
294
|
+
const absPos = bracePos + videoMatch.index;
|
|
295
|
+
if (!isCommented(content, absPos)) {
|
|
296
|
+
foundUncommented = true;
|
|
297
|
+
// Scenario A: video exists with a value — replace if not already 'on'
|
|
298
|
+
if (videoMatch[2] === 'on') return content;
|
|
299
|
+
return content.substring(0, absPos) + "video: 'on'" + content.substring(absPos + videoMatch[0].length);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!foundUncommented) {
|
|
304
|
+
// Scenario B (commented) or C (missing): insert uncommented video: 'on'
|
|
305
|
+
const indent = detectConfigIndent(content);
|
|
306
|
+
const innerIndent = indent + ' ';
|
|
307
|
+
return content.substring(0, bracePos + 1) + '\n' + innerIndent + "video: 'on'," + content.substring(bracePos + 1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return content;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Scenario D: no use block at all — create one
|
|
314
|
+
const indent = detectConfigIndent(content);
|
|
315
|
+
const innerIndent = indent + ' ';
|
|
316
|
+
const useBlock = `${indent}use: {\n${innerIndent}video: 'on',\n${indent}},`;
|
|
317
|
+
|
|
318
|
+
const knownProps = ['reporter', 'workers', 'retries', 'forbidOnly', 'fullyParallel', 'testDir'];
|
|
319
|
+
for (const prop of knownProps) {
|
|
320
|
+
const regex = new RegExp(`^([ \\t]*${prop}\\s*:.*?,?)[ \\t]*$`, 'm');
|
|
321
|
+
const m = content.match(regex);
|
|
322
|
+
if (m && !isCommented(content, m.index)) {
|
|
323
|
+
const insertPos = m.index + m[0].length;
|
|
324
|
+
return content.substring(0, insertPos) + '\n' + useBlock + content.substring(insertPos);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const openers = [
|
|
329
|
+
/defineConfig\s*\(\s*\{/,
|
|
330
|
+
/module\.exports\s*=\s*\{/,
|
|
331
|
+
/export\s+default\s*\{/,
|
|
332
|
+
];
|
|
333
|
+
for (const opener of openers) {
|
|
334
|
+
const m = content.match(opener);
|
|
335
|
+
if (m) {
|
|
336
|
+
const insertPos = m.index + m[0].length;
|
|
337
|
+
return content.substring(0, insertPos) + '\n' + useBlock + content.substring(insertPos);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return content;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// configurePlaywright
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
function findPlaywrightConfig() {
|
|
349
|
+
const tsPath = path.join(userProjectRoot, 'playwright.config.ts');
|
|
350
|
+
if (fs.existsSync(tsPath)) return tsPath;
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function createDefaultConfig() {
|
|
355
|
+
const configPath = path.join(userProjectRoot, 'playwright.config.ts');
|
|
356
|
+
const templatePath = path.join(packageRoot, 'playwright.config.ts');
|
|
357
|
+
|
|
358
|
+
// Read the template from the actual playwright.config.ts in the package
|
|
359
|
+
const template = fs.readFileSync(templatePath, 'utf8')
|
|
360
|
+
// Update testDir to './tests' instead of './test' for user projects
|
|
361
|
+
.replace(/testDir:\s*['"]\.\/test['"]/g, "testDir: './tests'");
|
|
362
|
+
|
|
363
|
+
fs.writeFileSync(configPath, template, 'utf8');
|
|
364
|
+
console.log(' Created playwright.config.ts with amikoo-reporter configured.\n');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function configurePlaywright() {
|
|
368
|
+
const configPath = findPlaywrightConfig();
|
|
369
|
+
|
|
370
|
+
if (!configPath) {
|
|
371
|
+
console.log(' No playwright config found.');
|
|
372
|
+
createDefaultConfig();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ext = path.extname(configPath);
|
|
377
|
+
console.log(` Found playwright.config${ext}`);
|
|
378
|
+
|
|
379
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
380
|
+
|
|
381
|
+
// Already configured?
|
|
382
|
+
if (content.includes(REPORTER_MODULE)) {
|
|
383
|
+
console.log(' amikoo-reporter already configured. Skipping.\n');
|
|
384
|
+
// Still ensure video is on
|
|
385
|
+
const withVideo = ensureVideoOn(content);
|
|
386
|
+
if (withVideo !== content) {
|
|
387
|
+
fs.writeFileSync(configPath, withVideo, 'utf8');
|
|
388
|
+
console.log(" Ensured video: 'on' in playwright config.\n");
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Try strategies in order
|
|
394
|
+
const modified =
|
|
395
|
+
tryArrayAppend(content) ||
|
|
396
|
+
tryStringToArray(content) ||
|
|
397
|
+
tryCommentedReporter(content) ||
|
|
398
|
+
tryNoReporter(content) ||
|
|
399
|
+
tryFallback(content);
|
|
400
|
+
|
|
401
|
+
// Ensure video: 'on' in use block
|
|
402
|
+
const withVideo = ensureVideoOn(modified);
|
|
403
|
+
|
|
404
|
+
fs.writeFileSync(configPath, withVideo, 'utf8');
|
|
405
|
+
console.log(' amikoo-reporter added to playwright config.\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// configureEnv
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
function configureEnv() {
|
|
413
|
+
const envPath = path.join(userProjectRoot, '.env');
|
|
414
|
+
const keyLine = 'AMIKOO_KEY=your_amikoo_key_here';
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(envPath)) {
|
|
417
|
+
fs.writeFileSync(envPath, `# amikoo-reporter Configuration\n${keyLine}\n`, 'utf8');
|
|
418
|
+
console.log(' Created .env — please update AMIKOO_KEY with your actual key.\n');
|
|
419
|
+
} else {
|
|
420
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
421
|
+
if (content.includes('AMIKOO_KEY')) {
|
|
422
|
+
console.log(' AMIKOO_KEY already present in .env. Skipping.\n');
|
|
423
|
+
} else {
|
|
424
|
+
const separator = content.endsWith('\n') ? '' : '\n';
|
|
425
|
+
fs.appendFileSync(envPath, `${separator}\n# amikoo-reporter Configuration\n${keyLine}\n`, 'utf8');
|
|
426
|
+
console.log(' Appended AMIKOO_KEY to existing .env.\n');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Main
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
function install() {
|
|
436
|
+
try {
|
|
437
|
+
if (userProjectRoot === packageRoot) {
|
|
438
|
+
console.log(' Running in package development mode. Skipping installation.\n');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log('Installing amikoo-reporter...\n');
|
|
443
|
+
|
|
444
|
+
configurePlaywright();
|
|
445
|
+
configureEnv();
|
|
446
|
+
|
|
447
|
+
console.log('amikoo-reporter installation complete!\n');
|
|
448
|
+
console.log('Next steps:');
|
|
449
|
+
console.log(' 1. Update your .env file with your AMIKOO_KEY');
|
|
450
|
+
console.log(' 2. Run your tests: npx playwright test\n');
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('Installation error:', error.message);
|
|
453
|
+
console.error(`You may need to manually add the reporter to your playwright config:`);
|
|
454
|
+
console.error(` reporter: [['${REPORTER_MODULE}']]\n`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
install();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@muuktest/amikoo-reporter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Playwright reporter for Amikoo - automatically installs and configures test reporting to Amikoo AI",
|
|
5
|
+
"main": "dist/controlHub/controlHubReporter.js",
|
|
6
|
+
"types": "dist/controlHub/controlHubReporter.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/controlHub/controlHubReporter.d.ts",
|
|
10
|
+
"default": "./dist/controlHub/controlHubReporter.js"
|
|
11
|
+
},
|
|
12
|
+
"./controlHub/controlHubReporter": {
|
|
13
|
+
"types": "./dist/controlHub/controlHubReporter.d.ts",
|
|
14
|
+
"default": "./dist/controlHub/controlHubReporter.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"postinstall": "node install.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/**/*",
|
|
24
|
+
"install.js",
|
|
25
|
+
"playwright.config.ts",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/muuklabs/controlhub-reporter.git"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"playwright",
|
|
34
|
+
"reporter",
|
|
35
|
+
"controlhub",
|
|
36
|
+
"testing",
|
|
37
|
+
"test-automation",
|
|
38
|
+
"playwright-reporter"
|
|
39
|
+
],
|
|
40
|
+
"author": "MuukLabs",
|
|
41
|
+
"license": "ISC",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/muuklabs/controlhub-reporter/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/muuklabs/controlhub-reporter#readme",
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@playwright/test": "^1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.3.5",
|
|
54
|
+
"typescript": "^5.8.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"dotenv": "^17.3.1"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
5
|
+
*/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: './test',
|
|
8
|
+
/* Run tests in files in parallel */
|
|
9
|
+
fullyParallel: true,
|
|
10
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
11
|
+
forbidOnly: !!process.env.CI,
|
|
12
|
+
/* Retry on CI only */
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
/* Opt out of parallel tests on CI. */
|
|
15
|
+
workers: process.env.CI ? 1 : undefined,
|
|
16
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
17
|
+
reporter: [['@muuktest/amikoo-reporter']],
|
|
18
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
19
|
+
use: {
|
|
20
|
+
/* Base URL to use in actions like `await page.goto('')`. */
|
|
21
|
+
// baseURL: 'http://localhost:3000',
|
|
22
|
+
|
|
23
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
24
|
+
trace: 'on-first-retry',
|
|
25
|
+
video: 'on',
|
|
26
|
+
screenshot: 'on'
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/* Configure projects for major browsers */
|
|
30
|
+
projects: [
|
|
31
|
+
{
|
|
32
|
+
name: 'chromium',
|
|
33
|
+
use: { ...devices['Desktop Chrome'] },
|
|
34
|
+
},
|
|
35
|
+
/** Uncomment to run tests in Firefox */
|
|
36
|
+
// {
|
|
37
|
+
// name: 'firefox',
|
|
38
|
+
// use: { ...devices['Desktop Firefox'] },
|
|
39
|
+
// },
|
|
40
|
+
|
|
41
|
+
/** Uncomment to run tests in Safari */
|
|
42
|
+
// {
|
|
43
|
+
// name: 'webkit',
|
|
44
|
+
// use: { ...devices['Desktop Safari'] },
|
|
45
|
+
// },
|
|
46
|
+
],
|
|
47
|
+
});
|