@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.
- package/dist/apiUtils.d.ts +26 -0
- package/dist/apiUtils.d.ts.map +1 -0
- package/dist/apiUtils.js +61 -0
- package/dist/apiUtils.js.map +1 -0
- package/dist/controlHubReporter.d.ts +28 -0
- package/dist/controlHubReporter.d.ts.map +1 -0
- package/dist/controlHubReporter.js +149 -0
- package/dist/controlHubReporter.js.map +1 -0
- package/dist/gitUtils.d.ts +8 -0
- package/dist/gitUtils.d.ts.map +1 -0
- package/dist/gitUtils.js +65 -0
- package/dist/gitUtils.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +60 -0
- package/dist/utils.js.map +1 -0
- package/dist/videoUtils.d.ts +16 -0
- package/dist/videoUtils.d.ts.map +1 -0
- package/dist/videoUtils.js +104 -0
- package/dist/videoUtils.js.map +1 -0
- package/package.json +12 -7
- package/controlHub/apiUtils.ts +0 -85
- package/controlHub/controlHubReporter.ts +0 -178
- package/controlHub/gitUtils.ts +0 -66
- package/controlHub/utils.ts +0 -56
- package/controlHub/videoUtils.ts +0 -109
|
@@ -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"}
|
package/dist/apiUtils.js
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/gitUtils.js
ADDED
|
@@ -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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muuktest/amikoo-reporter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Playwright reporter for Amikoo - automatically installs and configures test reporting to Amikoo AI",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/controlHubReporter.js",
|
|
6
|
+
"types": "dist/controlHubReporter.d.ts",
|
|
6
7
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
"
|
|
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"
|
package/controlHub/apiUtils.ts
DELETED
|
@@ -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;
|
package/controlHub/gitUtils.ts
DELETED
|
@@ -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
|
-
}
|
package/controlHub/utils.ts
DELETED
|
@@ -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
|
-
|
package/controlHub/videoUtils.ts
DELETED
|
@@ -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
|
-
}
|