agentq-webdriverio 1.0.1
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/LICENSE +21 -0
- package/README.md +1 -0
- package/agentq.config.json +3 -0
- package/dist/commandExecutor.d.ts +4 -0
- package/dist/commandExecutor.js +85 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +32 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +38 -0
- package/dist/pageContext.d.ts +8 -0
- package/dist/pageContext.js +24 -0
- package/dist/pageManager.d.ts +8 -0
- package/dist/pageManager.js +21 -0
- package/dist/pull-testcase.d.ts +2 -0
- package/dist/pull-testcase.js +255 -0
- package/dist/testResult.d.ts +4 -0
- package/dist/testResult.js +121 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +2 -0
- package/dist/wsClient.d.ts +13 -0
- package/dist/wsClient.js +109 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 agentq-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# agentq-webdriverio
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommandExecutor = void 0;
|
|
4
|
+
class CommandExecutor {
|
|
5
|
+
static async execute(command, browser) {
|
|
6
|
+
if (command.action === 'fill') {
|
|
7
|
+
const el = await browser.$(command.target);
|
|
8
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
9
|
+
await el.setValue(command.value);
|
|
10
|
+
}
|
|
11
|
+
else if (command.action === 'click') {
|
|
12
|
+
const el = await browser.$(command.target);
|
|
13
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
14
|
+
await el.click();
|
|
15
|
+
}
|
|
16
|
+
else if (command.action === 'visit' || command.action === 'goto' || command.action === 'navigate' || command.action === 'open') {
|
|
17
|
+
await browser.url(command.value);
|
|
18
|
+
}
|
|
19
|
+
else if (command.action === 'select') {
|
|
20
|
+
const el = await browser.$(command.target);
|
|
21
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
22
|
+
if (typeof command.value === 'object' && command.value !== null && 'value' in command.value) {
|
|
23
|
+
if (command.value.value) {
|
|
24
|
+
await el.selectByAttribute('value', command.value.value);
|
|
25
|
+
}
|
|
26
|
+
else if (command.value.label) {
|
|
27
|
+
await el.selectByVisibleText(command.value.label);
|
|
28
|
+
}
|
|
29
|
+
else if (command.value.index !== undefined) {
|
|
30
|
+
await el.selectByIndex(command.value.index);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (typeof command.value === 'string') {
|
|
34
|
+
await el.selectByVisibleText(command.value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (command.action === 'check') {
|
|
38
|
+
const el = await browser.$(command.target);
|
|
39
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
40
|
+
if (!(await el.isSelected())) {
|
|
41
|
+
await el.click();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (command.action === 'uncheck') {
|
|
45
|
+
const el = await browser.$(command.target);
|
|
46
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
47
|
+
if (await el.isSelected()) {
|
|
48
|
+
await el.click();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (command.action === 'type') {
|
|
52
|
+
const el = await browser.$(command.target);
|
|
53
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
54
|
+
await el.addValue(command.value);
|
|
55
|
+
}
|
|
56
|
+
else if (command.action === 'wait') {
|
|
57
|
+
await browser.pause(command.value);
|
|
58
|
+
}
|
|
59
|
+
else if (command.action === 'assertVisible') {
|
|
60
|
+
const el = await browser.$(command.target);
|
|
61
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
62
|
+
}
|
|
63
|
+
else if (command.action === 'assertNotVisible') {
|
|
64
|
+
const el = await browser.$(command.target);
|
|
65
|
+
await el.waitForDisplayed({ timeout: 10000, reverse: true });
|
|
66
|
+
}
|
|
67
|
+
else if (command.action === 'assertEqual') {
|
|
68
|
+
const el = await browser.$(command.target);
|
|
69
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
70
|
+
const textContent = await el.getText();
|
|
71
|
+
if (textContent !== command.value) {
|
|
72
|
+
throw new Error(`Expected text "${command.value}" but found "${textContent}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (command.action === 'assertContain') {
|
|
76
|
+
const el = await browser.$(command.target);
|
|
77
|
+
await el.waitForDisplayed({ timeout: 10000 });
|
|
78
|
+
const textContent = await el.getText();
|
|
79
|
+
if (!textContent.includes(command.value)) {
|
|
80
|
+
throw new Error(`Expected text to contain "${command.value}" but found "${textContent}"`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.CommandExecutor = CommandExecutor;
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getToken = exports.getServiceUrl = void 0;
|
|
4
|
+
exports.getConfig = getConfig;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const dotenv_1 = require("dotenv");
|
|
8
|
+
(0, dotenv_1.config)();
|
|
9
|
+
function getConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const configPath = (0, path_1.join)(process.cwd(), 'agentq.config.json');
|
|
12
|
+
const configFile = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
13
|
+
return JSON.parse(configFile);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
// Config file not found or invalid, continue with env vars
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
TOKEN: process.env.AGENTQ_TOKEN,
|
|
20
|
+
SERVICE_URL: process.env.AGENTQ_SERVICE_URL,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const getServiceUrl = () => {
|
|
24
|
+
const config = getConfig();
|
|
25
|
+
return config.SERVICE_URL || 'wss://websocket-ai-automation-test-api.agentq.id';
|
|
26
|
+
};
|
|
27
|
+
exports.getServiceUrl = getServiceUrl;
|
|
28
|
+
const getToken = () => {
|
|
29
|
+
const config = getConfig();
|
|
30
|
+
return config.TOKEN;
|
|
31
|
+
};
|
|
32
|
+
exports.getToken = getToken;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function q(userPrompt: string): Promise<void>;
|
|
2
|
+
export declare function initAgentQ(browser: WebdriverIO.Browser): void;
|
|
3
|
+
export declare function handleTestConclusion(testTitle: string, status: 'passed' | 'failed', startTime: number, errorDetails?: string): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.q = q;
|
|
4
|
+
exports.initAgentQ = initAgentQ;
|
|
5
|
+
exports.handleTestConclusion = handleTestConclusion;
|
|
6
|
+
process.env.DOTENV_CONFIG_QUIET = 'true';
|
|
7
|
+
const wsClient_1 = require("./wsClient");
|
|
8
|
+
const pageContext_1 = require("./pageContext");
|
|
9
|
+
require('dotenv').config({ quiet: true });
|
|
10
|
+
const testResult_1 = require("./testResult");
|
|
11
|
+
const testResult_2 = require("./testResult");
|
|
12
|
+
const stripAnsi = (str) => str.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
13
|
+
async function q(userPrompt) {
|
|
14
|
+
const browser = pageContext_1.PageContext.getInstance().getBrowser();
|
|
15
|
+
return wsClient_1.WSClient.getInstance().sendCommand(userPrompt, browser);
|
|
16
|
+
}
|
|
17
|
+
function initAgentQ(browser) {
|
|
18
|
+
pageContext_1.PageContext.getInstance().setBrowser(browser);
|
|
19
|
+
}
|
|
20
|
+
let cachedAccessToken = null;
|
|
21
|
+
async function handleTestConclusion(testTitle, status, startTime, errorDetails) {
|
|
22
|
+
const tcId = parseInt(testTitle.split('-')[0]);
|
|
23
|
+
if (!isNaN(tcId) && process.env.AGENTQ_TESTRUN_ID) {
|
|
24
|
+
if (!cachedAccessToken) {
|
|
25
|
+
cachedAccessToken = await (0, testResult_2.getAccessToken)();
|
|
26
|
+
}
|
|
27
|
+
const executionTime = (Date.now() - startTime) / 1000;
|
|
28
|
+
const isPassed = status === 'passed';
|
|
29
|
+
await (0, testResult_1.exportTestResult)(tcId.toString(), process.env.AGENTQ_TESTRUN_ID, {
|
|
30
|
+
status: status,
|
|
31
|
+
actualResult: isPassed ? `Test "${testTitle}" passed successfully`
|
|
32
|
+
: `Test failed`,
|
|
33
|
+
executionTime,
|
|
34
|
+
notes: isPassed ? 'Test completed without errors' : stripAnsi(errorDetails || 'Test failed')
|
|
35
|
+
});
|
|
36
|
+
// Note: Artifact upload (screenshots/videos) would need to be handled via WebdriverIO attachments if available
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PageContext = void 0;
|
|
4
|
+
class PageContext {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.currentBrowser = null;
|
|
7
|
+
}
|
|
8
|
+
static getInstance() {
|
|
9
|
+
if (!PageContext.instance) {
|
|
10
|
+
PageContext.instance = new PageContext();
|
|
11
|
+
}
|
|
12
|
+
return PageContext.instance;
|
|
13
|
+
}
|
|
14
|
+
setBrowser(browser) {
|
|
15
|
+
this.currentBrowser = browser;
|
|
16
|
+
}
|
|
17
|
+
getBrowser() {
|
|
18
|
+
if (!this.currentBrowser) {
|
|
19
|
+
throw new Error('Browser context not set. Make sure to initialize AgentQ with the webdriverio browser object.');
|
|
20
|
+
}
|
|
21
|
+
return this.currentBrowser;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.PageContext = PageContext;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PageManager = void 0;
|
|
4
|
+
class PageManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.currentBrowser = null;
|
|
7
|
+
}
|
|
8
|
+
static getInstance() {
|
|
9
|
+
if (!PageManager.instance) {
|
|
10
|
+
PageManager.instance = new PageManager();
|
|
11
|
+
}
|
|
12
|
+
return PageManager.instance;
|
|
13
|
+
}
|
|
14
|
+
setBrowser(browser) {
|
|
15
|
+
this.currentBrowser = browser;
|
|
16
|
+
}
|
|
17
|
+
getBrowser() {
|
|
18
|
+
return this.currentBrowser;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.PageManager = PageManager;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
+
dotenv_1.default.config();
|
|
12
|
+
// Configuration
|
|
13
|
+
const config = {
|
|
14
|
+
apiBaseUrl: process.env.AGENTQ_API_URL || 'https://backend-app.agentq.id',
|
|
15
|
+
projectId: `${process.env.AGENTQ_PROJECT_ID}`,
|
|
16
|
+
testRunId: `${process.env.AGENTQ_TESTRUN_ID}`,
|
|
17
|
+
authEndpoint: '/auth/login',
|
|
18
|
+
authData: {
|
|
19
|
+
email: `${process.env.AGENTQ_EMAIL}`,
|
|
20
|
+
password: `${process.env.AGENTQ_PASSWORD}`
|
|
21
|
+
},
|
|
22
|
+
outputDir: path_1.default.join(process.cwd(), 'tests')
|
|
23
|
+
};
|
|
24
|
+
// Parse command line arguments more robustly
|
|
25
|
+
function parseArgs() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
let tcId = null;
|
|
28
|
+
let testRunId = null;
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
const tcIdMatch = arg.match(/--tcid=(\d+)/);
|
|
31
|
+
if (tcIdMatch) {
|
|
32
|
+
tcId = tcIdMatch[1];
|
|
33
|
+
}
|
|
34
|
+
const testRunIdMatch = arg.match(/--testrunid=([\w-]+)/); // Allow alphanumeric and hyphens
|
|
35
|
+
if (testRunIdMatch) {
|
|
36
|
+
testRunId = testRunIdMatch[1];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Check environment variables as fallback
|
|
40
|
+
if (!tcId && process.env.TCID) {
|
|
41
|
+
tcId = process.env.TCID;
|
|
42
|
+
}
|
|
43
|
+
if (!testRunId && process.env.TESTRUNID) {
|
|
44
|
+
testRunId = process.env.TESTRUNID;
|
|
45
|
+
}
|
|
46
|
+
return { tcId, testRunId };
|
|
47
|
+
}
|
|
48
|
+
const { tcId, testRunId } = parseArgs();
|
|
49
|
+
if (!tcId && !testRunId) {
|
|
50
|
+
console.error('Error: Either --tcid or --testrunid is required.');
|
|
51
|
+
console.error('Usage for test case:');
|
|
52
|
+
console.error(' npm run agentq-pull-testcase -- --tcid=<number>');
|
|
53
|
+
console.error(' or');
|
|
54
|
+
console.error(' node agentq-pull-testcase.js --tcid=<number>');
|
|
55
|
+
console.error('Usage for test suite:');
|
|
56
|
+
console.error(' npm run agentq-pull-testsuite -- --testrunid=<uuid>');
|
|
57
|
+
console.error(' or');
|
|
58
|
+
console.error(' node agentq-pull-testcase.js -- --testrunid=<uuid>');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
// Create the output directory if it doesn't exist
|
|
62
|
+
if (!fs_1.default.existsSync(config.outputDir)) {
|
|
63
|
+
fs_1.default.mkdirSync(config.outputDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
async function getAccessToken() {
|
|
66
|
+
try {
|
|
67
|
+
console.log(`Authenticating with: ${config.apiBaseUrl}${config.authEndpoint}`);
|
|
68
|
+
const response = await axios_1.default.post(`${config.apiBaseUrl}${config.authEndpoint}`, config.authData, {
|
|
69
|
+
headers: {
|
|
70
|
+
'accept': 'application/json',
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return response.data.access_token;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error('Authentication failed:');
|
|
78
|
+
if (error.response) {
|
|
79
|
+
console.error(`Status: ${error.response.status}`);
|
|
80
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
81
|
+
}
|
|
82
|
+
else if (error.request) {
|
|
83
|
+
console.error('No response received during authentication. Check if the server is running.');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error(`Error during authentication: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function fetchTestCase(tcId, accessToken) {
|
|
92
|
+
try {
|
|
93
|
+
console.log(`Fetching test case from: ${config.apiBaseUrl}/projects/${config.projectId}/test-cases/tcId/${tcId}`);
|
|
94
|
+
const response = await axios_1.default.get(`${config.apiBaseUrl}/projects/${config.projectId}/test-cases/tcId/${tcId}`, {
|
|
95
|
+
headers: {
|
|
96
|
+
'accept': 'application/json',
|
|
97
|
+
'Authorization': `Bearer ${accessToken}`
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return response.data;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error('API request failed:');
|
|
104
|
+
if (error.response) {
|
|
105
|
+
console.error(`Status: ${error.response.status}`);
|
|
106
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
107
|
+
}
|
|
108
|
+
else if (error.request) {
|
|
109
|
+
console.error('No response received from the test case endpoint. Check if the server is running.');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.error(`Error fetching test case: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function fetchTestSuite(testRunId, accessToken) {
|
|
118
|
+
try {
|
|
119
|
+
const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results?page=1&limit=10000`;
|
|
120
|
+
console.log(`Fetching test suite results from: ${apiUrl}`);
|
|
121
|
+
const response = await axios_1.default.get(apiUrl, {
|
|
122
|
+
headers: {
|
|
123
|
+
'accept': 'application/json',
|
|
124
|
+
'Authorization': `Bearer ${accessToken}`
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return response.data;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(`Failed to fetch test suite results for testRunId: ${testRunId}`);
|
|
131
|
+
if (error.response) {
|
|
132
|
+
console.error(`Status: ${error.response.status}`);
|
|
133
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
134
|
+
}
|
|
135
|
+
else if (error.request) {
|
|
136
|
+
console.error('No response received from the test suite endpoint. Check if the server is running.');
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.error(`Error fetching test suite: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function generateTestSuiteSpecFile(testRunId, testResults) {
|
|
145
|
+
const fileName = `testrun-${testRunId}.spec.ts`;
|
|
146
|
+
const filePath = path_1.default.join(config.outputDir, fileName);
|
|
147
|
+
const testsCode = testResults
|
|
148
|
+
.map(result => {
|
|
149
|
+
const sanitizedTitle = sanitizeFilename(result.testCase.title);
|
|
150
|
+
const stepsCode = result.testCase.steps
|
|
151
|
+
.split('\n')
|
|
152
|
+
.filter(step => step.trim() !== '')
|
|
153
|
+
.map(step => ` await q(\`${step}\`);`)
|
|
154
|
+
.join('\n');
|
|
155
|
+
return ` it(\`${result.testCase.tcId}-${sanitizedTitle}\`, async () => {
|
|
156
|
+
${stepsCode}
|
|
157
|
+
});`;
|
|
158
|
+
})
|
|
159
|
+
.join('\n\n');
|
|
160
|
+
const fileContent = `import { q, initAgentQ } from 'agentq-webdriverio';
|
|
161
|
+
|
|
162
|
+
describe('${testRunId}-testrun', () => {
|
|
163
|
+
before(() => {
|
|
164
|
+
initAgentQ(browser);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
${testsCode}
|
|
168
|
+
});
|
|
169
|
+
`;
|
|
170
|
+
fs_1.default.writeFileSync(filePath, fileContent);
|
|
171
|
+
console.log(`✅ Created: ${filePath}`);
|
|
172
|
+
return filePath;
|
|
173
|
+
}
|
|
174
|
+
function sanitizeFilename(title) {
|
|
175
|
+
// Replace characters that are not allowed in filenames
|
|
176
|
+
return title
|
|
177
|
+
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
178
|
+
.replace(/\s+/g, '-')
|
|
179
|
+
.substring(0, 100); // Limit length to avoid potential issues
|
|
180
|
+
}
|
|
181
|
+
function generateSpecFile(testCase) {
|
|
182
|
+
const sanitizedTitle = sanitizeFilename(testCase.title);
|
|
183
|
+
const fileName = `${sanitizedTitle}.spec.ts`;
|
|
184
|
+
const filePath = path_1.default.join(config.outputDir, fileName);
|
|
185
|
+
// Process precondition
|
|
186
|
+
const preconditionCode = testCase.precondition
|
|
187
|
+
? testCase.precondition
|
|
188
|
+
.split('\n')
|
|
189
|
+
.filter(line => line.trim() !== '')
|
|
190
|
+
.map(line => ` await q(\`User is on the ${line.trim()}.\`);`) // Adjusted for clarity
|
|
191
|
+
.join('\n')
|
|
192
|
+
: '';
|
|
193
|
+
// Process steps
|
|
194
|
+
const stepsCode = testCase.steps
|
|
195
|
+
.split('\n')
|
|
196
|
+
.filter(step => step.trim() !== '')
|
|
197
|
+
.map(step => ` await q(\`${step}\`);`)
|
|
198
|
+
.join('\n');
|
|
199
|
+
// Process expected result
|
|
200
|
+
const expectationCode = testCase.expectation
|
|
201
|
+
? testCase.expectation
|
|
202
|
+
.split('\n')
|
|
203
|
+
.filter(line => line.trim() !== '')
|
|
204
|
+
.map(line => ` await q(\`${line.trim()}\`);`)
|
|
205
|
+
.join('\n')
|
|
206
|
+
: '';
|
|
207
|
+
const fileContent = `import { q, initAgentQ } from 'agentq-webdriverio';
|
|
208
|
+
|
|
209
|
+
describe(\`${testCase.tcId}-${testCase.title}\`, () => {
|
|
210
|
+
before(() => {
|
|
211
|
+
initAgentQ(browser);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('Execute test steps', async () => {
|
|
215
|
+
// Precondition:
|
|
216
|
+
${preconditionCode}
|
|
217
|
+
// Steps:
|
|
218
|
+
${stepsCode}
|
|
219
|
+
// Expected Result:
|
|
220
|
+
${expectationCode}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
`;
|
|
224
|
+
fs_1.default.writeFileSync(filePath, fileContent);
|
|
225
|
+
console.log(`✅ Created: ${filePath}`);
|
|
226
|
+
return filePath;
|
|
227
|
+
}
|
|
228
|
+
// Main execution
|
|
229
|
+
(async () => {
|
|
230
|
+
if (tcId) {
|
|
231
|
+
console.log(`🔍 Fetching test case with tcId: ${tcId}`);
|
|
232
|
+
// Get the access token
|
|
233
|
+
const accessToken = await getAccessToken();
|
|
234
|
+
console.log(`🔑 Successfully obtained access token.`);
|
|
235
|
+
// Fetch the test case using the access token
|
|
236
|
+
const testCase = await fetchTestCase(tcId, accessToken);
|
|
237
|
+
console.log(`📄 Found test case: ${testCase.title}`);
|
|
238
|
+
// Generate the Playwright spec file
|
|
239
|
+
const filePath = generateSpecFile(testCase);
|
|
240
|
+
console.log(`\n🚀 Success! Test case converted to Playwright spec file.`);
|
|
241
|
+
}
|
|
242
|
+
else if (testRunId) {
|
|
243
|
+
console.log(`🔍 Fetching test suite results for testRunId: ${testRunId}`);
|
|
244
|
+
// Get the access token
|
|
245
|
+
const accessToken = await getAccessToken();
|
|
246
|
+
console.log(`🔑 Successfully obtained access token.`);
|
|
247
|
+
// Fetch the test suite results
|
|
248
|
+
const testSuiteResponse = await fetchTestSuite(testRunId, accessToken);
|
|
249
|
+
const testResults = testSuiteResponse.results;
|
|
250
|
+
console.log(`📄 Found ${testResults.length} test cases in test run: ${testRunId}`);
|
|
251
|
+
// Generate the Playwright spec file for the test suite
|
|
252
|
+
const filePath = generateTestSuiteSpecFile(testRunId, testResults);
|
|
253
|
+
console.log(`\n🚀 Success! Test suite converted to Playwright spec file.`);
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TestResult } from './types';
|
|
2
|
+
export declare function getAccessToken(): Promise<string>;
|
|
3
|
+
export declare function exportTestResult(tcId: string, testRunId: string, result: Partial<TestResult>): Promise<any>;
|
|
4
|
+
export declare function uploadArtifact(testRunId: string, testResultId: string, type: 'screenshot' | 'video', filePath: string): Promise<any>;
|
|
@@ -0,0 +1,121 @@
|
|
|
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.getAccessToken = getAccessToken;
|
|
7
|
+
exports.exportTestResult = exportTestResult;
|
|
8
|
+
exports.uploadArtifact = uploadArtifact;
|
|
9
|
+
process.env.DOTENV_CONFIG_QUIET = 'true';
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
const dotenv = require('dotenv');
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
dotenv.config({ quiet: true });
|
|
15
|
+
// Configuration
|
|
16
|
+
const config = {
|
|
17
|
+
apiBaseUrl: process.env.AGENTQ_API_URL || 'https://backend-app.agentq.id',
|
|
18
|
+
projectId: `${process.env.AGENTQ_PROJECT_ID}`,
|
|
19
|
+
authEndpoint: '/auth/login',
|
|
20
|
+
authData: {
|
|
21
|
+
email: `${process.env.AGENTQ_EMAIL}`,
|
|
22
|
+
password: `${process.env.AGENTQ_PASSWORD}`
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
let cachedToken = null;
|
|
26
|
+
async function getAccessToken() {
|
|
27
|
+
if (cachedToken) {
|
|
28
|
+
return cachedToken;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
console.log(`Authenticating with: ${config.apiBaseUrl}${config.authEndpoint}`);
|
|
32
|
+
const response = await axios_1.default.post(`${config.apiBaseUrl}${config.authEndpoint}`, config.authData, {
|
|
33
|
+
headers: {
|
|
34
|
+
'accept': 'application/json',
|
|
35
|
+
'Content-Type': 'application/json'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
cachedToken = response.data.access_token;
|
|
39
|
+
return cachedToken;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('Authentication failed:');
|
|
43
|
+
if (error.response) {
|
|
44
|
+
console.error(`Status: ${error.response.status}`);
|
|
45
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
46
|
+
}
|
|
47
|
+
else if (error.request) {
|
|
48
|
+
console.error('No response received during authentication. Check if the server is running.');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error(`Error during authentication: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function exportTestResult(tcId, testRunId, result) {
|
|
57
|
+
const accessToken = await getAccessToken();
|
|
58
|
+
const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results/tcId/${tcId}`;
|
|
59
|
+
try {
|
|
60
|
+
console.log(`Pushing test result to: ${apiUrl}`);
|
|
61
|
+
const response = await axios_1.default.patch(apiUrl, result, {
|
|
62
|
+
headers: {
|
|
63
|
+
'accept': 'application/json',
|
|
64
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
console.log('✅ Test result pushed successfully');
|
|
69
|
+
return response.data;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Failed to export test result:');
|
|
73
|
+
if (error.response) {
|
|
74
|
+
console.error(`Status: ${error.response.status}`);
|
|
75
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
76
|
+
}
|
|
77
|
+
else if (error.request) {
|
|
78
|
+
console.error('No response received while pushing test result. Check if the server is running.');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.error(`Error pushing test result: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function uploadArtifact(testRunId, testResultId, type, filePath) {
|
|
87
|
+
const accessToken = await getAccessToken();
|
|
88
|
+
const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results/${testResultId}/${type}`;
|
|
89
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
90
|
+
console.warn(`⚠️ Artifact file not found: ${filePath}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const formData = new FormData();
|
|
95
|
+
const fileBuffer = fs_1.default.readFileSync(filePath);
|
|
96
|
+
const fileName = path_1.default.basename(filePath);
|
|
97
|
+
const mimetype = type === 'screenshot' ? 'image/png' : 'video/webm';
|
|
98
|
+
const blob = new Blob([fileBuffer], { type: mimetype });
|
|
99
|
+
formData.append('file', blob, fileName);
|
|
100
|
+
console.log(`Uploading ${type} to: ${apiUrl}`);
|
|
101
|
+
const response = await axios_1.default.post(apiUrl, formData, {
|
|
102
|
+
headers: {
|
|
103
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
104
|
+
'Content-Type': 'multipart/form-data'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
console.log(`✅ ${type} uploaded successfully`);
|
|
108
|
+
return response.data;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(`Failed to upload ${type}:`);
|
|
112
|
+
if (error.response) {
|
|
113
|
+
console.error(`Status: ${error.response.status}`);
|
|
114
|
+
console.error(`Response: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`Error uploading ${type}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
// We don't throw here to avoid failing the test just because artifact upload failed
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface AICommand {
|
|
2
|
+
action: 'fill' | 'click' | 'visit' | 'goto' | 'navigate' | 'open' | 'select' | 'check' | 'uncheck' | 'type' | 'wait' | 'assertVisible' | 'assertNotVisible' | 'assertEqual' | 'assertContain';
|
|
3
|
+
target: string;
|
|
4
|
+
value?: string | number | {
|
|
5
|
+
value?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
index?: number;
|
|
8
|
+
} | string[];
|
|
9
|
+
}
|
|
10
|
+
export interface WSQueueItem {
|
|
11
|
+
resolve: (value: void) => void;
|
|
12
|
+
reject: (reason: any) => void;
|
|
13
|
+
command: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface TestResult {
|
|
16
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
17
|
+
actualResult: string;
|
|
18
|
+
executionTime: number;
|
|
19
|
+
notes: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TestResultResponse {
|
|
22
|
+
id: string;
|
|
23
|
+
testRunId: string;
|
|
24
|
+
testCaseId: string;
|
|
25
|
+
status: string;
|
|
26
|
+
actualResult: string;
|
|
27
|
+
executionTime: number;
|
|
28
|
+
notes: string;
|
|
29
|
+
screenshotUrl: string | null;
|
|
30
|
+
videoUrl: string | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
testCase: {
|
|
34
|
+
id: string;
|
|
35
|
+
tcId: number;
|
|
36
|
+
title: string;
|
|
37
|
+
precondition: string;
|
|
38
|
+
steps: string;
|
|
39
|
+
expectation: string;
|
|
40
|
+
priority: string;
|
|
41
|
+
type: string;
|
|
42
|
+
platform: string;
|
|
43
|
+
testCaseType: string;
|
|
44
|
+
projectId: string;
|
|
45
|
+
folderId: string;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class WSClient {
|
|
2
|
+
private static instance;
|
|
3
|
+
private ws;
|
|
4
|
+
private connected;
|
|
5
|
+
private token;
|
|
6
|
+
private commandQueue;
|
|
7
|
+
private constructor();
|
|
8
|
+
static getInstance(): WSClient;
|
|
9
|
+
private connect;
|
|
10
|
+
private setupWebSocket;
|
|
11
|
+
private processQueue;
|
|
12
|
+
sendCommand(userPrompt: string, browser: WebdriverIO.Browser): Promise<void>;
|
|
13
|
+
}
|
package/dist/wsClient.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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.WSClient = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const commandExecutor_1 = require("./commandExecutor");
|
|
10
|
+
class WSClient {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ws = null;
|
|
13
|
+
this.connected = false;
|
|
14
|
+
this.commandQueue = [];
|
|
15
|
+
this.token = (0, config_1.getToken)() || '';
|
|
16
|
+
if (!this.token) {
|
|
17
|
+
throw new Error('AgentQ token is required. Please provide it via agentq.config.json or AGENTQ_TOKEN environment variable.');
|
|
18
|
+
}
|
|
19
|
+
this.connect();
|
|
20
|
+
}
|
|
21
|
+
static getInstance() {
|
|
22
|
+
if (!WSClient.instance) {
|
|
23
|
+
WSClient.instance = new WSClient();
|
|
24
|
+
}
|
|
25
|
+
return WSClient.instance;
|
|
26
|
+
}
|
|
27
|
+
connect() {
|
|
28
|
+
if (this.ws)
|
|
29
|
+
return;
|
|
30
|
+
this.ws = new ws_1.default((0, config_1.getServiceUrl)());
|
|
31
|
+
this.setupWebSocket();
|
|
32
|
+
}
|
|
33
|
+
setupWebSocket() {
|
|
34
|
+
if (!this.ws)
|
|
35
|
+
return;
|
|
36
|
+
this.ws.on('open', () => {
|
|
37
|
+
this.connected = true;
|
|
38
|
+
this.processQueue();
|
|
39
|
+
});
|
|
40
|
+
this.ws.on('close', () => {
|
|
41
|
+
this.connected = false;
|
|
42
|
+
this.ws = null;
|
|
43
|
+
setTimeout(() => this.connect(), 5000);
|
|
44
|
+
});
|
|
45
|
+
this.ws.on('error', (error) => {
|
|
46
|
+
console.error('WebSocket error:', error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async processQueue() {
|
|
50
|
+
while (this.commandQueue.length > 0) {
|
|
51
|
+
const item = this.commandQueue.shift();
|
|
52
|
+
if (item) {
|
|
53
|
+
try {
|
|
54
|
+
await item.command();
|
|
55
|
+
item.resolve();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
item.reject(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async sendCommand(userPrompt, browser) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const command = async () => {
|
|
66
|
+
if (!this.ws) {
|
|
67
|
+
throw new Error('WebSocket connection not available');
|
|
68
|
+
}
|
|
69
|
+
const pageSource = await browser.getPageSource();
|
|
70
|
+
return new Promise((wsResolve, wsReject) => {
|
|
71
|
+
const messageHandler = async (message) => {
|
|
72
|
+
try {
|
|
73
|
+
const response = JSON.parse(message.toString());
|
|
74
|
+
if (response.type === 'error') {
|
|
75
|
+
wsReject(new Error(response.message));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (response.command) {
|
|
79
|
+
await commandExecutor_1.CommandExecutor.execute(response.command, browser);
|
|
80
|
+
wsResolve();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
wsReject(new Error('Invalid command received'));
|
|
84
|
+
}
|
|
85
|
+
this.ws?.removeListener('message', messageHandler);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
wsReject(error);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
this.ws?.on('message', messageHandler);
|
|
92
|
+
this.ws?.send(JSON.stringify({
|
|
93
|
+
type: 'command',
|
|
94
|
+
prompt: userPrompt,
|
|
95
|
+
pageSource,
|
|
96
|
+
token: this.token,
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
if (this.connected) {
|
|
101
|
+
command().then(resolve).catch(reject);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.commandQueue.push({ resolve, reject, command });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.WSClient = WSClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentq-webdriverio",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentq-pull-testcase": "./dist/pull-testcase.js",
|
|
8
|
+
"agentq-pull-testsuite": "./dist/pull-testcase.js"
|
|
9
|
+
},
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "agentq.id",
|
|
12
|
+
"email": "support@agentq.id"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"description": "",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"agentq-pull-testcase": "node ./dist/pull-testcase.js"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/agentq-ai/agentq-webdriverio.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [],
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/agentq-ai/agentq-webdriverio/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/agentq-ai/agentq-webdriverio#readme",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"webdriverio": "^9.0.0",
|
|
33
|
+
"axios": "^1.7.2",
|
|
34
|
+
"dotenv": "^16.4.5",
|
|
35
|
+
"ws": "^8.17.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.14.2",
|
|
39
|
+
"@types/ws": "^8.5.10",
|
|
40
|
+
"@wdio/cli": "^9.0.0",
|
|
41
|
+
"@wdio/globals": "^9.0.0",
|
|
42
|
+
"typescript": "^5.4.5"
|
|
43
|
+
}
|
|
44
|
+
}
|