@muuktest/amikoo-reporter 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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;IAwDhC,KAAK,CAAC,MAAM,EAAE,GAAG;CAqCxB;AACD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,149 @@
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
+ // Get the file location relative to the repository
76
+ const fullPath = test.location.file;
77
+ const repoIndex = fullPath.indexOf(this.repositoryName);
78
+ const fileLocation = fullPath.slice(repoIndex + this.repositoryName.length + 1);
79
+ // Get the full test title including parent suites
80
+ const fullTitle = await (0, utils_1.getSuiteNames)(test);
81
+ // Create a unique hash ID for the test using repository ID, file path, full title, and commit SHA
82
+ const rawIdentity = `${this.owner}/${this.repositoryId}:${fileLocation}:${fullTitle}`;
83
+ const testId = crypto_1.default.createHash('sha256').update(rawIdentity).digest('hex');
84
+ const duration = parseInt(result.duration) / 1000;
85
+ const payload = {
86
+ hashId: testId,
87
+ hashObject: {
88
+ owner: this.owner,
89
+ repositoryId: this.repositoryId,
90
+ fileLocation: fileLocation,
91
+ fullTitle: fullTitle,
92
+ },
93
+ filePath,
94
+ fullTitle,
95
+ duration: duration,
96
+ executionAt: new Date().toISOString(),
97
+ result: result.status === "passed" ? true : false
98
+ };
99
+ this.testExecutionData.push(payload);
100
+ // Collect video for later batch processing
101
+ const videoPath = (0, videoUtils_1.getVideoPath)(result);
102
+ if (videoPath) {
103
+ this.videos.push({ path: videoPath, testId });
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.log("Error processing test end: ", error);
108
+ reject(error);
109
+ }
110
+ resolve(true);
111
+ });
112
+ // save the promise so the onEnd can wait for this code to complete.
113
+ this.testEndPromises.push(promise);
114
+ }
115
+ async onEnd(result) {
116
+ console.log('Wait for all tests to complete.');
117
+ await Promise.all(this.testEndPromises);
118
+ console.log('All tests have ended, sending execution report.');
119
+ if (this.executionNumber) {
120
+ // Process videos and get presigned URLs
121
+ const videoResult = await (0, videoUtils_1.processVideos)(this.videos, this.organizationId, this.executionNumber, this.access_token);
122
+ // Upload videos to S3
123
+ if (videoResult?.uploadUrls) {
124
+ await (0, videoUtils_1.uploadVideosToS3)(videoResult.videos, videoResult.uploadUrls);
125
+ }
126
+ const feedbackData = {
127
+ repositoryId: this.repositoryId,
128
+ branch: this.branch,
129
+ tests: this.testExecutionData,
130
+ executionNumber: this.executionNumber,
131
+ videos: videoResult?.videos.map(v => v.s3FileName) || [], // Include video file names in feedback
132
+ };
133
+ // Send API to BE here
134
+ const feedbackResponse = await apiUtils_1.api.post('/execution/feedback', this.access_token, feedbackData);
135
+ if (feedbackResponse.success) {
136
+ console.log('Execution report sent successfully');
137
+ }
138
+ else {
139
+ console.error('Failed to send execution report', feedbackResponse.error);
140
+ }
141
+ }
142
+ else {
143
+ console.warn('Execution number not available. Execution report will not be sent to amikoo-reporter, and execution data will not be saved.');
144
+ }
145
+ (0, utils_1.checkForUpdates)();
146
+ }
147
+ }
148
+ exports.default = MyReporter;
149
+ //# 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,CA2B7E"}
@@ -0,0 +1,65 @@
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
+ let branch;
41
+ let remoteUrl;
42
+ try {
43
+ branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim() || 'unknown-branch';
44
+ remoteUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
45
+ }
46
+ catch {
47
+ console.warn('Warning: Not a git repository. Repository context will not be available.');
48
+ return { repositoryId: '0', repositoryName: 'unknown', branch: 'unknown-branch', owner: 'unknown-owner' };
49
+ }
50
+ // Get owner and repo from git remote URL
51
+ const { owner, repo } = parseGitRemoteUrl(remoteUrl);
52
+ const body = {
53
+ "owner": owner,
54
+ "name": repo
55
+ };
56
+ const response = await apiUtils_1.api.post('/repository/get', access_token, body);
57
+ const repoData = await response?.data;
58
+ return {
59
+ repositoryId: repoData?.id?.toString() || "0",
60
+ repositoryName: repo,
61
+ branch,
62
+ owner: owner || 'unknown-owner'
63
+ };
64
+ }
65
+ //# sourceMappingURL=gitUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitUtils.js","sourceRoot":"","sources":["../controlHub/gitUtils.ts"],"names":[],"mappings":";;;;;AA8CA,sCA2BC;AAzED,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,IAAI,MAAc,CAAC;IACnB,IAAI,SAAiB,CAAC;IAEtB,IAAI,CAAC;QACH,MAAM,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,gBAAgB,CAAC;QAChI,SAAS,GAAG,IAAA,wBAAQ,EAAC,oCAAoC,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IACpH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACzF,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAC5G,CAAC;IAED,yCAAyC;IACzC,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"}
package/dist/utils.js ADDED
@@ -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,cAAc,CAAC,CAAC;IAC9D,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":";;;;;AAMA,oCAGC;AAKD,sCA+BC;AAiCD,4CA8BC;AA5GD,4CAAoB;AACpB,yCAAiC;AAEjC;;GAEG;AACH,SAAgB,YAAY,CAAC,MAAW;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACzG,OAAO,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC;AAChC,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,aAAa,CACjC,MAAa,EACb,cAAsB,EACtB,eAAuB,EACvB,KAAa;IAEb,IAAI,SAAS,GAAU,EAAE,CAAC;IAC1B,IAAI,UAAU,GAAiB,IAAI,CAAC;IAEpC,IAAI,MAAM,CAAC,MAAM,IAAI,cAAc,IAAI,eAAe,EAAE,CAAC;QACvD,qDAAqD;QACrD,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,YAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aAC5D,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACT,SAAS,EAAE,CAAC,CAAC,IAAI;YACjB,UAAU,EAAE,GAAG,eAAe,IAAI,CAAC,CAAC,MAAM,OAAO;SAClD,CAAC,CAAC,CAAC;QAEN,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;YACrB,mDAAmD;YACnD,MAAM,QAAQ,GAAG,MAAM,cAAG,CAAC,IAAI,CAAC,8BAA8B,EAAE,KAAK,EAAE;gBACrE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,cAAc,EAAE,EAAE,CAAC,CAAC;aACvH,CAAC,CAAC;YACH,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,eAAe,CAAC,QAAgB,EAAE,SAAiB;IAChE,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,OAAO;QACL,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;YACtC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;SAC1C,CAAC,CAAC;QACH,OAAO,GAAG,QAAQ,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC7D,CAAC;QACJ;;YAEI;IACL,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,gBAAgB,CAAC,MAAa,EAAE,UAAiB;IACrE,MAAM,OAAO,GAAU,EAAE,CAAC;IAE1B,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,MAAM,eAAe,CAAC,CAAC;IAChE,IAAI,MAAM,CAAC,MAAM,IAAI,UAAU,EAAE,MAAM,EAAE,CAAC;QACxC,+CAA+C;QAC/C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAEvE,gCAAgC;QAChC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YAChD,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/C,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBAClE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;YACjD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;gBAC1D,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;YAChF,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;QAE/B,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC3D,OAAO,CAAC,GAAG,CAAC,YAAY,YAAY,IAAI,OAAO,CAAC,MAAM,eAAe,CAAC,CAAC;IACzE,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "@muuktest/amikoo-reporter",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Playwright reporter for Amikoo - automatically installs and configures test reporting to Amikoo AI",
5
- "main": "controlHub/controlHubReporter.ts",
5
+ "main": "dist/controlHubReporter.js",
6
+ "types": "dist/controlHubReporter.d.ts",
6
7
  "exports": {
7
- ".": "./controlHub/controlHubReporter.ts",
8
- "./controlHub/controlHubReporter": "./controlHub/controlHubReporter.ts",
9
- "./controlHub/controlHubReporter.ts": "./controlHub/controlHubReporter.ts"
8
+ ".": {
9
+ "types": "./dist/controlHubReporter.d.ts",
10
+ "default": "./dist/controlHubReporter.js"
11
+ }
10
12
  },
11
13
  "scripts": {
14
+ "build": "rm -rf dist && tsc",
15
+ "prepublishOnly": "npm run build",
12
16
  "postinstall": "node install.js"
13
17
  },
14
18
  "files": [
15
- "controlHub/**/*",
19
+ "dist/**/*",
16
20
  "install.js",
17
21
  "playwright.config.ts",
18
22
  "README.md"
@@ -39,7 +43,8 @@
39
43
  "@playwright/test": "^1.0.0"
40
44
  },
41
45
  "devDependencies": {
42
- "@types/node": "^25.3.5"
46
+ "@types/node": "^25.3.5",
47
+ "typescript": "^6.0.2"
43
48
  },
44
49
  "dependencies": {
45
50
  "dotenv": "^17.3.1"
@@ -1,85 +0,0 @@
1
- import dotenv from 'dotenv';
2
- dotenv.config();
3
-
4
- // Get the API URL and key from environment variables, with defaults for production
5
- const BASE_URL = process.env.CONTROLHUB_URL || 'https://app.amikoo.ai';
6
-
7
- type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
8
-
9
- interface ApiRequestOptions {
10
- method: HttpMethod;
11
- endpoint: string;
12
- token: string;
13
- body?: any;
14
- }
15
-
16
- interface ApiResponse<T = any> {
17
- success: boolean;
18
- data?: T;
19
- error?: string;
20
- }
21
-
22
- /**
23
- * Makes an API request to the amikoo-reporter backend
24
- * @param options - The request options including method, endpoint, token, and optional body
25
- * @returns The API response with success flag and data/error
26
- */
27
- export async function apiRequest<T = any>(options: ApiRequestOptions): Promise<ApiResponse<T>> {
28
- const { method, endpoint, token, body } = options;
29
-
30
- const url = `${BASE_URL}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
31
-
32
- const headers: Record<string, string> = {
33
- 'Content-Type': 'application/json',
34
- };
35
-
36
- if (token) {
37
- headers['Authorization'] = `Bearer ${token}`;
38
- }
39
-
40
- try {
41
- const fetchOptions: RequestInit = {
42
- method,
43
- headers,
44
- };
45
-
46
- if (body && method !== 'GET') {
47
- fetchOptions.body = JSON.stringify(body);
48
- }
49
-
50
- const response = await fetch(url, fetchOptions);
51
- const responseData = await response.json();
52
-
53
- if (!response.ok || !responseData.success) {
54
- return {
55
- success: false,
56
- error: responseData.data || responseData.message || 'Request failed',
57
- };
58
- }
59
-
60
- return {
61
- success: true,
62
- data: responseData.data,
63
- };
64
- } catch (error) {
65
- return {
66
- success: false,
67
- error: error instanceof Error ? error.message : String(error),
68
- };
69
- }
70
- }
71
-
72
- // Convenience methods
73
- export const api = {
74
- get: <T = any>(endpoint: string, token: string) =>
75
- apiRequest<T>({ method: 'GET', endpoint, token }),
76
-
77
- post: <T = any>(endpoint: string, token: string, body?: any) =>
78
- apiRequest<T>({ method: 'POST', endpoint, token, body }),
79
-
80
- put: <T = any>(endpoint: string, token: string, body?: any) =>
81
- apiRequest<T>({ method: 'PUT', endpoint, token, body }),
82
-
83
- delete: <T = any>(endpoint: string, token: string) =>
84
- apiRequest<T>({ method: 'DELETE', endpoint, token }),
85
- };
@@ -1,178 +0,0 @@
1
- import { Reporter } from '@playwright/test/reporter';
2
- import { getGitContext } from './gitUtils';
3
- import { api } from './apiUtils';
4
- import { getVideoPath, processVideos, uploadVideosToS3 } from './videoUtils';
5
- import { getSuiteNames, checkForUpdates } from './utils';
6
- import path from 'path';
7
- import crypto from 'crypto';
8
-
9
- class MyReporter implements Reporter {
10
-
11
- testExecutionData: any[] = [];
12
- videos: any[] = [];
13
- authInfo: any;
14
- organizationId: string = '';
15
- executionNumber: number = 0;
16
- hashIds: any[] = [];
17
- startExecutionTime: number = 0;
18
- browser: String = 'chromeTest';
19
- compilationError: boolean = false;
20
- filesWithCompilationError: any[] = [];
21
- url: any;
22
- testEndPromises: any[] = [];
23
-
24
- private repositoryId!: string;
25
- private branch!: string;
26
- private repositoryName!: string;
27
- private access_token!: string;
28
- private owner!: string;
29
- private key!: string;
30
-
31
- async onBegin(config: any, suite: any) {
32
-
33
- // We need to obtain a token from control hub so we can send API requests.
34
- this.key = process.env.AMIKOO_KEY || '';
35
- const tokenReponse = await api.post('/validate_key', '', {"key": this.key});
36
- if(tokenReponse.success && tokenReponse.data){
37
- this.access_token = tokenReponse.data.access_token;
38
- }
39
-
40
- const context = await getGitContext(this.access_token);
41
- this.repositoryId = context.repositoryId;
42
- this.repositoryName = context.repositoryName;
43
- this.branch = context.branch;
44
- this.owner = context.owner;
45
-
46
-
47
- if (!this.access_token) {
48
- console.warn('Warning: failed to obtain access token');
49
- console.warn('Feedback data will not be sent to amikoo-reporter, and execution data will not be saved.');
50
- }
51
- else{
52
- // We need to call the API to get the execution number here, so we can include it in the feedback data.
53
- const executionResponse = await api.get('/execution/execution_number', this.access_token);
54
- if (executionResponse.success && executionResponse.data) {
55
- this.executionNumber = executionResponse?.data?.executionNumber;
56
- this.organizationId = executionResponse?.data?.organizationId;
57
- console.log('Retrieved execution number:', this.executionNumber);
58
- }
59
- else {
60
- console.error('Failed to retrieve execution number', executionResponse.error);
61
- }
62
-
63
- console.log(`Starting the test run for branch ${this.branch} in repository ${this.repositoryName}`);
64
- }
65
-
66
- }
67
-
68
- async onTestBegin(test: any) {
69
- console.log(`Starting test ${test.title}`);
70
- }
71
-
72
- async onError(error: any){
73
- console.log("Tests generated an error during execution. Error = ", error);
74
- const message = error?.message?.replace(/^(.*: )+/, '').trim();
75
- this.compilationError = true;
76
- if(error?.location?.file){
77
- this.filesWithCompilationError.push({
78
- "file": error.location.file,
79
- "message": message,
80
- });
81
- }
82
- }
83
-
84
- async onTestEnd(test: any, result: any) {
85
- console.log(`Finished test ${test.title} with status ${result.status}`);
86
-
87
- const promise = new Promise(async (resolve, reject) => {
88
- try {
89
-
90
- // Get clean title path without project name and empty strings
91
- const filePath = path.basename(test.location.file);
92
-
93
- // Get the file location relative to the repository
94
- const fullPath = test.location.file;
95
- const repoIndex = fullPath.indexOf(this.repositoryName);
96
- const fileLocation = fullPath.slice(repoIndex + this.repositoryName.length + 1);
97
-
98
- // Get the full test title including parent suites
99
- const fullTitle = await getSuiteNames(test);
100
-
101
- // Create a unique hash ID for the test using repository ID, file path, full title, and commit SHA
102
- const rawIdentity = `${this.owner}/${this.repositoryId}:${fileLocation}:${fullTitle}`;
103
- const testId = crypto.createHash('sha256').update(rawIdentity).digest('hex');
104
-
105
- const duration = parseInt(result.duration) / 1000;
106
- const payload = {
107
- hashId: testId,
108
- hashObject: {
109
- owner: this.owner,
110
- repositoryId: this.repositoryId,
111
- fileLocation: fileLocation,
112
- fullTitle: fullTitle,
113
- },
114
- filePath,
115
- fullTitle,
116
- duration: duration,
117
- executionAt: new Date().toISOString(),
118
- result: result.status === "passed" ? true : false
119
- };
120
-
121
- this.testExecutionData.push(payload);
122
-
123
- // Collect video for later batch processing
124
- const videoPath = getVideoPath(result);
125
- if (videoPath) {
126
- this.videos.push({ path: videoPath, testId });
127
- }
128
-
129
- } catch (error) {
130
- console.log("Error processing test end: ", error);
131
- reject(error);
132
- }
133
- resolve(true);
134
- });
135
-
136
- // save the promise so the onEnd can wait for this code to complete.
137
- this.testEndPromises.push(promise);
138
- }
139
-
140
- async onEnd(result: any) {
141
- console.log('Wait for all tests to complete.');
142
- await Promise.all(this.testEndPromises);
143
- console.log('All tests have ended, sending execution report.');
144
-
145
- if(this.executionNumber){
146
- // Process videos and get presigned URLs
147
- const videoResult = await processVideos(this.videos, this.organizationId, this.executionNumber, this.access_token);
148
-
149
- // Upload videos to S3
150
- if (videoResult?.uploadUrls) {
151
- await uploadVideosToS3(videoResult.videos, videoResult.uploadUrls);
152
- }
153
-
154
- const feedbackData = {
155
- repositoryId: this.repositoryId,
156
- branch: this.branch,
157
- tests: this.testExecutionData,
158
- executionNumber: this.executionNumber,
159
- videos: videoResult?.videos.map(v => v.s3FileName) || [], // Include video file names in feedback
160
- };
161
-
162
- // Send API to BE here
163
- const feedbackResponse = await api.post('/execution/feedback', this.access_token, feedbackData);
164
- if (feedbackResponse.success) {
165
- console.log('Execution report sent successfully');
166
- } else {
167
- console.error('Failed to send execution report', feedbackResponse.error);
168
- }
169
- }
170
- else{
171
- console.warn('Execution number not available. Execution report will not be sent to amikoo-reporter, and execution data will not be saved.');
172
- }
173
-
174
- checkForUpdates();
175
- }
176
-
177
- }
178
- export default MyReporter;
@@ -1,66 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import dotenv from 'dotenv';
3
- import { api } from './apiUtils';
4
-
5
- dotenv.config();
6
-
7
- export interface GitContext {
8
- repositoryId: string;
9
- repositoryName: string;
10
- branch: string;
11
- owner: string;
12
- }
13
-
14
- /**
15
- * Parses a git remote URL to extract owner and repo name.
16
- * Supports both HTTPS and SSH formats:
17
- * - https://github.com/owner/repo.git
18
- * - git@github.com:owner/repo.git
19
- */
20
- function parseGitRemoteUrl(remoteUrl: string): { owner: string; repo: string } {
21
-
22
- const response = {
23
- owner: '',
24
- repo: ''
25
- }
26
- // Remove trailing .git if present
27
- const cleanUrl = remoteUrl.replace(/\.git$/, '');
28
-
29
- // Match HTTPS format: https://github.com/owner/repo
30
- const httpsMatch = cleanUrl.match(/https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)/);
31
- if (httpsMatch) {
32
- return { owner: httpsMatch[1], repo: httpsMatch[2] };
33
- }
34
-
35
- // Match SSH format: git@github.com:owner/repo
36
- const sshMatch = cleanUrl.match(/git@[^:]+:([^\/]+)\/(.+)/);
37
- if (sshMatch) {
38
- response.owner = sshMatch[1];
39
- response.repo = sshMatch[2];
40
- }
41
-
42
- return response
43
- }
44
-
45
- // This function retrieves the current git context, including the repository ID, branch name, and commit SHA.
46
- // It uses the GitHub API to fetch the repository ID based on the owner and repo name extracted from the git remote URL.
47
- export async function getGitContext(access_token: string): Promise<GitContext> {
48
- const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'unknown-branch';
49
-
50
- // Get owner and repo from git remote URL
51
- const remoteUrl = execSync('git config --get remote.origin.url').toString().trim();
52
- const { owner, repo } = parseGitRemoteUrl(remoteUrl);
53
-
54
- const body = {
55
- "owner": owner,
56
- "name": repo
57
- }
58
- const response = await api.post('/repository/get', access_token , body);
59
- const repoData = await response?.data;
60
- return {
61
- repositoryId: repoData?.id?.toString() || "0",
62
- repositoryName: repo,
63
- branch,
64
- owner: owner || 'unknown-owner'
65
- };
66
- }
@@ -1,56 +0,0 @@
1
- import https from 'https';
2
- import path from 'path';
3
- import fs from 'fs';
4
-
5
-
6
- // getSuiteNames
7
- // Builds the full test title path by traversing parent suites
8
- // Parameters:
9
- // test: any - Playwright test object with parent references
10
- // Returns: string - Full title path joined by ' > ' (e.g., "Suite > Nested > Test Name")
11
- export async function getSuiteNames(test: any): Promise<string> {
12
- const suites: string[] = [];
13
- let parent = test.parent;
14
-
15
- while (parent) {
16
- if (parent._type === 'describe' && parent.title) suites.unshift(parent.title);
17
- parent = parent.parent;
18
- }
19
-
20
- return [...suites, test.title].join(' > ');
21
- }
22
-
23
-
24
- export function checkForUpdates(): Promise<void> {
25
- const pkgPath = path.resolve(__dirname, '..', 'package.json');
26
- const currentVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
27
-
28
- return new Promise((resolve) => {
29
- try {
30
- const [curMajor, curMinor] = currentVersion.split('.').map(Number);
31
-
32
- const req = https.get('https://registry.npmjs.org/@muuktest%2famikoo-reporter/latest', { timeout: 5000 }, (res) => {
33
- let data = '';
34
- res.on('data', (chunk: string) => { data += chunk; });
35
- res.on('end', () => {
36
- try {
37
- const latest = JSON.parse(data).version;
38
- if (!latest) { resolve(); return; }
39
- const [latMajor, latMinor] = latest.split('.').map(Number);
40
-
41
- if (latMajor > curMajor || (latMajor === curMajor && latMinor > curMinor)) {
42
- console.log(`\n A new version of @muuktest/amikoo-reporter is available: ${currentVersion} → ${latest}`);
43
- console.log(` Run "npm install @muuktest/amikoo-reporter@latest" to update.\n`);
44
- }
45
- } catch { /* ignore parse errors */ }
46
- resolve();
47
- });
48
- });
49
- req.on('error', () => resolve());
50
- req.on('timeout', () => { req.destroy(); resolve(); });
51
- } catch {
52
- resolve();
53
- }
54
- });
55
- }
56
-
@@ -1,109 +0,0 @@
1
- import fs from 'fs';
2
- import { api } from './apiUtils';
3
-
4
- /**
5
- * Gets video path from test result attachments
6
- */
7
- export function getVideoPath(result: any): string {
8
- const attachment = result.attachments?.find((a: any) => a.name === 'video' || a.path?.endsWith('.webm'));
9
- return attachment?.path || '';
10
- }
11
-
12
- /**
13
- * Processes videos and requests presigned URLs for upload
14
- */
15
- export async function processVideos(
16
- videos: any[],
17
- organizationId: string,
18
- executionNumber: number,
19
- token: string
20
- ): Promise<{ videos: any[]; uploadUrls: any[] | null }> {
21
- let processed: any[] = [];
22
- let uploadUrls: any[] | null = null;
23
-
24
- if (videos.length && organizationId && executionNumber) {
25
- // Map videos to their S3 filenames (no local rename)
26
- processed = videos.filter(v => v.path && fs.existsSync(v.path))
27
- .map(v => ({
28
- localPath: v.path,
29
- s3FileName: `${executionNumber}_${v.testId}.webm`,
30
- }));
31
-
32
- if (processed.length) {
33
- // Request presigned URLs with desired S3 filenames
34
- const response = await api.post('/execution/batch_upload_urls', token, {
35
- files: processed.map(v => ({ fileName: v.s3FileName, contentType: 'video/webm', folder: `videos/${organizationId}` })),
36
- });
37
- uploadUrls = response.success ? response.data : null;
38
- } else {
39
- console.log('No videos found to process');
40
- }
41
- } else {
42
- console.log('Missing required parameters for video processing');
43
- }
44
-
45
- return { videos: processed, uploadUrls };
46
- }
47
-
48
- /**
49
- * Uploads a single video file to S3 using a presigned URL
50
- */
51
- async function uploadVideoToS3(filePath: string, uploadUrl: string): Promise<boolean> {
52
- let success = false;
53
-
54
- if (fs.existsSync(filePath)) {
55
- //try {
56
- const fileBuffer = fs.readFileSync(filePath);
57
- const response = await fetch(uploadUrl, {
58
- method: 'PUT',
59
- body: fileBuffer,
60
- headers: { 'Content-Type': 'video/webm' },
61
- });
62
- success = response.ok;
63
- if (!success) {
64
- console.log('Upload failed with status:', response.status);
65
- }
66
- /* } catch (error) {
67
- console.log('Error uploading video:', error);
68
- }*/
69
- } else {
70
- console.log('Video file not found:', filePath);
71
- }
72
-
73
- return success;
74
- }
75
-
76
- /**
77
- * Uploads all processed videos to S3 using their presigned URLs
78
- */
79
- export async function uploadVideosToS3(videos: any[], uploadUrls: any[]): Promise<any[]> {
80
- const results: any[] = [];
81
-
82
- console.log(`Starting upload of ${videos.length} videos to S3`);
83
- if (videos.length && uploadUrls?.length) {
84
- // Create a map for quick lookup by S3 filename
85
- const urlMap = new Map(uploadUrls.map(u => [u.fileName, u.uploadUrl]));
86
-
87
- // Upload all videos in parallel
88
- const uploadPromises = videos.map(async (video) => {
89
- const uploadUrl = urlMap.get(video.s3FileName);
90
- if (uploadUrl) {
91
- const success = await uploadVideoToS3(video.localPath, uploadUrl);
92
- return { fileName: video.s3FileName, success };
93
- } else {
94
- console.log('No upload URL found for:', video.s3FileName);
95
- return { fileName: video.s3FileName, success: false, error: 'No upload URL' };
96
- }
97
- });
98
-
99
- const uploadResults = await Promise.all(uploadPromises);
100
- results.push(...uploadResults);
101
-
102
- const successCount = results.filter(r => r.success).length;
103
- console.log(`Uploaded ${successCount}/${results.length} videos to S3`);
104
- } else {
105
- console.log('No videos or upload URLs provided');
106
- }
107
-
108
- return results;
109
- }