@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 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,8 @@
1
+ export interface GitContext {
2
+ repositoryId: string;
3
+ repositoryName: string;
4
+ branch: string;
5
+ owner: string;
6
+ }
7
+ export declare function getGitContext(access_token: string): Promise<GitContext>;
8
+ //# sourceMappingURL=gitUtils.d.ts.map
@@ -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,3 @@
1
+ export declare function getSuiteNames(test: any): Promise<string>;
2
+ export declare function checkForUpdates(): Promise<void>;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -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
+ });