endorphin-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +209 -0
- package/README.md +474 -0
- package/bin/endorphin.js +256 -0
- package/examples/endorphin.config.js +22 -0
- package/examples/tests/QE-001-basic-login-test.js +18 -0
- package/examples/tests/sample-test.js +9 -0
- package/examples/tools/.gitkeep +0 -0
- package/framework/config/agent-config.js +53 -0
- package/framework/config/browser-config.js +53 -0
- package/framework/config/paths.js +40 -0
- package/framework/core/agent-setup.js +75 -0
- package/framework/core/browser-framework.js +766 -0
- package/framework/core/config-loader.js +309 -0
- package/framework/core/test-discovery.js +402 -0
- package/framework/core/test-manager.js +343 -0
- package/framework/core/test-recorder.js +302 -0
- package/framework/core/test-runner.js +133 -0
- package/framework/core/test-session.js +98 -0
- package/framework/index.js +44 -0
- package/framework/interactive/enhanced-interactive-recorder.js +223 -0
- package/framework/interactive/index.js +18 -0
- package/framework/interactive/interactive-test-clean.js +33 -0
- package/framework/interactive/interactive-test.js +33 -0
- package/framework/test-framework.js +29 -0
- package/framework/testing/index.js +15 -0
- package/framework/testing/test-interactive-recorder.js +83 -0
- package/framework/testing/test-modular-framework.js +47 -0
- package/framework/testing/verify-test-format.js +58 -0
- package/framework/tools/content.js +67 -0
- package/framework/tools/index.js +52 -0
- package/framework/tools/interaction.js +180 -0
- package/framework/tools/navigation.js +43 -0
- package/framework/tools/utilities.js +99 -0
- package/framework/tools/verification.js +84 -0
- package/package.json +84 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
// Simple test to validate the enhanced interactive recorder setup
|
|
18
|
+
import { EnhancedBrowserTestFramework } from '../index.js';
|
|
19
|
+
import { TestRecorder } from '../core/test-recorder.js';
|
|
20
|
+
|
|
21
|
+
async function testRecorderSetup() {
|
|
22
|
+
console.log('๐งช Testing Enhanced Interactive Recorder Setup...');
|
|
23
|
+
console.log('โ'.repeat(50));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Test 1: Framework instantiation
|
|
27
|
+
console.log('1๏ธโฃ Testing framework instantiation...');
|
|
28
|
+
const framework = new EnhancedBrowserTestFramework();
|
|
29
|
+
console.log('โ
Framework created successfully');
|
|
30
|
+
|
|
31
|
+
// Test 2: Test recorder instantiation
|
|
32
|
+
console.log('2๏ธโฃ Testing recorder instantiation...');
|
|
33
|
+
const testData = {
|
|
34
|
+
id: 'TEST-001',
|
|
35
|
+
name: 'Validation Test',
|
|
36
|
+
description: 'Testing recorder setup',
|
|
37
|
+
site: 'https://example.com'
|
|
38
|
+
};
|
|
39
|
+
const recorder = new TestRecorder(framework, testData);
|
|
40
|
+
console.log('โ
Recorder created successfully');
|
|
41
|
+
|
|
42
|
+
// Test 3: Natural language command parsing
|
|
43
|
+
console.log('3๏ธโฃ Testing command parsing...');
|
|
44
|
+
|
|
45
|
+
// Mock the framework with basic structure
|
|
46
|
+
framework.tools = {
|
|
47
|
+
click: async (params) => `Clicked ${params.selector}`,
|
|
48
|
+
fill: async (params) => `Filled ${params.selector} with ${params.value}`,
|
|
49
|
+
navigate: async (params) => `Navigated to ${params.url}`,
|
|
50
|
+
screenshot: async () => 'Screenshot taken'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Test command parsing
|
|
54
|
+
const clickResult = framework.extractSelectorFromCommand('click login button');
|
|
55
|
+
console.log(` Click parsing: "${clickResult}"`);
|
|
56
|
+
|
|
57
|
+
const fillResult = framework.extractFillFromCommand('fill email with test@example.com');
|
|
58
|
+
console.log(` Fill parsing: selector="${fillResult.selector}", value="${fillResult.value}"`);
|
|
59
|
+
|
|
60
|
+
const urlResult = framework.extractUrlFromCommand('navigate to https://google.com');
|
|
61
|
+
console.log(` URL parsing: "${urlResult}"`);
|
|
62
|
+
|
|
63
|
+
console.log('โ
Command parsing works correctly');
|
|
64
|
+
|
|
65
|
+
console.log('\n๐ All tests passed! Enhanced Interactive Recorder is ready to use.');
|
|
66
|
+
console.log('\nTo start recording:');
|
|
67
|
+
console.log(' npm run interactive');
|
|
68
|
+
console.log('\nOr run directly:');
|
|
69
|
+
console.log(' node framework/interactive/enhanced-interactive-recorder.js');
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('โ Test failed:', error.message);
|
|
73
|
+
console.error(error.stack);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Run if this file is executed directly
|
|
79
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
80
|
+
testRecorderSetup().catch(console.error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { testRecorderSetup };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { EnhancedBrowserTestFramework } from '../index.js';
|
|
18
|
+
|
|
19
|
+
async function testModularFramework() {
|
|
20
|
+
console.log('๐งช Testing Modular Framework...');
|
|
21
|
+
|
|
22
|
+
const framework = new EnhancedBrowserTestFramework();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Initialize framework
|
|
26
|
+
await framework.initialize();
|
|
27
|
+
console.log('โ
Framework initialized successfully');
|
|
28
|
+
|
|
29
|
+
// Test simple navigation
|
|
30
|
+
const result = await framework.runTask(
|
|
31
|
+
"Navigate to https://httpbin.org. Take a screenshot. Get simple page content to verify the site loaded. STOP - test completed.",
|
|
32
|
+
"Modular Framework Test"
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
console.log('โ
Test completed:', result.status);
|
|
36
|
+
console.log('๐ Results saved to:', result.sessionDir);
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('โ Test failed:', error.message);
|
|
40
|
+
} finally {
|
|
41
|
+
await framework.cleanup();
|
|
42
|
+
console.log('๐งน Framework cleanup completed');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Run the test
|
|
47
|
+
testModularFramework().catch(console.error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
// Verification script to test the updated test format and test-manager
|
|
18
|
+
import { TestManager } from '../core/test-manager.js';
|
|
19
|
+
|
|
20
|
+
async function verifyTestFormat() {
|
|
21
|
+
console.log("๐ง Verifying Test Format Updates...");
|
|
22
|
+
console.log("โ".repeat(50));
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const testManager = new TestManager();
|
|
26
|
+
const tests = await testManager.loadTests();
|
|
27
|
+
|
|
28
|
+
console.log(`โ
Successfully loaded ${tests.size} test cases`);
|
|
29
|
+
console.log("\n๐ Test Summary:");
|
|
30
|
+
|
|
31
|
+
// Verify each test has the new format
|
|
32
|
+
for (const [testId, test] of tests) {
|
|
33
|
+
const hasUID = test.testData && test.testData.uid;
|
|
34
|
+
const hasSimpleTask = test.task && !test.task.includes("Step-by-step");
|
|
35
|
+
const hasExportConst = true; // Already loaded, so format is correct
|
|
36
|
+
|
|
37
|
+
console.log(`\n๐น ${testId}: ${test.name}`);
|
|
38
|
+
console.log(` โ
Export Format: ${hasExportConst ? 'Updated (export const)' : 'Old (export default)'}`);
|
|
39
|
+
console.log(` โ
Task Format: ${hasSimpleTask ? 'Simplified' : 'Detailed Steps'}`);
|
|
40
|
+
console.log(` โ
Test Data: ${hasUID ? 'Enhanced with UID' : 'Basic'}`);
|
|
41
|
+
console.log(` ๐ฏ Priority: ${test.priority}`);
|
|
42
|
+
console.log(` ๐ท๏ธ Tags: ${test.tags.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log("\n๐ All tests verified successfully!");
|
|
46
|
+
console.log("โ
Export format updated to 'export const'");
|
|
47
|
+
console.log("โ
Task format simplified to natural language");
|
|
48
|
+
console.log("โ
Test data enhanced with unique identifiers");
|
|
49
|
+
console.log("โ
Test-manager handles both export formats");
|
|
50
|
+
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("โ Verification failed:", error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run verification
|
|
58
|
+
verifyTestFormat().catch(console.error);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { tool } from '@langchain/core/tools';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
export function createGetPageContentTool(framework) {
|
|
21
|
+
return tool(async ({ includeTitle = true, maxLength = 8000 }) => {
|
|
22
|
+
const stepDesc = "Get page content for analysis";
|
|
23
|
+
console.log(`๐ ${stepDesc}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
let content = '';
|
|
27
|
+
|
|
28
|
+
if (includeTitle) {
|
|
29
|
+
const title = await framework.page.title();
|
|
30
|
+
const url = framework.page.url();
|
|
31
|
+
content += `Page Title: ${title}\nURL: ${url}\n\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const htmlContent = await framework.page.content();
|
|
35
|
+
const truncatedContent = htmlContent.length > maxLength
|
|
36
|
+
? htmlContent.substring(0, maxLength) + '\n... (truncated for brevity)'
|
|
37
|
+
: htmlContent;
|
|
38
|
+
content += truncatedContent;
|
|
39
|
+
|
|
40
|
+
framework.logTestStep(stepDesc, 'getPageContent', { includeTitle, maxLength }, `Retrieved ${content.length} characters`, true);
|
|
41
|
+
return content;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
framework.logTestStep(stepDesc, 'getPageContent', { includeTitle, maxLength }, error.message, false);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}, {
|
|
47
|
+
name: 'getPageContent',
|
|
48
|
+
description: 'Get HTML content of the current page for analysis.',
|
|
49
|
+
schema: z.object({
|
|
50
|
+
includeTitle: z.boolean().optional(),
|
|
51
|
+
maxLength: z.number().optional(),
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createGetSimplePageContentTool(framework) {
|
|
57
|
+
return tool(async () => {
|
|
58
|
+
console.log("๐ Getting page content");
|
|
59
|
+
const content = await framework.page.content();
|
|
60
|
+
framework.logTestStep("Get simple page content", 'getSimplePageContent', {}, `Retrieved ${content.length} characters`, true);
|
|
61
|
+
return content;
|
|
62
|
+
}, {
|
|
63
|
+
name: 'getSimplePageContent',
|
|
64
|
+
description: 'Get the current HTML content of the page to analyze its structure.',
|
|
65
|
+
schema: z.object({})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { createNavigationTool } from './navigation.js';
|
|
18
|
+
import { createGetPageContentTool, createGetSimplePageContentTool } from './content.js';
|
|
19
|
+
import { createClickTool, createFillTool, createClearFieldTool } from './interaction.js';
|
|
20
|
+
import { createVerifyElementTool, createGetElementInfoTool } from './verification.js';
|
|
21
|
+
import { createWaitTool, createScreenshotTool } from './utilities.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create all browser automation tools for the framework
|
|
25
|
+
* @param {EnhancedBrowserTestFramework} framework - Framework instance
|
|
26
|
+
* @returns {Array} Array of all configured tools
|
|
27
|
+
*/
|
|
28
|
+
export function createAllTools(framework) {
|
|
29
|
+
return [
|
|
30
|
+
// Navigation tools
|
|
31
|
+
createNavigationTool(framework),
|
|
32
|
+
|
|
33
|
+
// Content analysis tools
|
|
34
|
+
createGetPageContentTool(framework),
|
|
35
|
+
createGetSimplePageContentTool(framework),
|
|
36
|
+
|
|
37
|
+
// Interaction tools
|
|
38
|
+
createClickTool(framework),
|
|
39
|
+
createFillTool(framework),
|
|
40
|
+
createClearFieldTool(framework),
|
|
41
|
+
|
|
42
|
+
// Verification tools
|
|
43
|
+
createVerifyElementTool(framework),
|
|
44
|
+
createGetElementInfoTool(framework),
|
|
45
|
+
|
|
46
|
+
// Utility tools
|
|
47
|
+
createWaitTool(framework),
|
|
48
|
+
createScreenshotTool(framework)
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default createAllTools;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { tool } from '@langchain/core/tools';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
export function createClickTool(framework) {
|
|
21
|
+
return tool(async ({ selector, strategy = 'css', timeout = 10000, force = false }) => {
|
|
22
|
+
const stepDesc = `Click ${selector} using ${strategy} strategy`;
|
|
23
|
+
console.log(`๐ ${stepDesc}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
let locator;
|
|
27
|
+
|
|
28
|
+
switch (strategy) {
|
|
29
|
+
case 'text':
|
|
30
|
+
locator = framework.page.getByText(selector, { exact: false });
|
|
31
|
+
break;
|
|
32
|
+
case 'exact-text':
|
|
33
|
+
locator = framework.page.getByText(selector, { exact: true });
|
|
34
|
+
break;
|
|
35
|
+
case 'role':
|
|
36
|
+
const [role, name] = selector.split(':');
|
|
37
|
+
locator = framework.page.getByRole(role, { name });
|
|
38
|
+
break;
|
|
39
|
+
case 'placeholder':
|
|
40
|
+
locator = framework.page.getByPlaceholder(selector);
|
|
41
|
+
break;
|
|
42
|
+
case 'label':
|
|
43
|
+
locator = framework.page.getByLabel(selector);
|
|
44
|
+
break;
|
|
45
|
+
case 'title':
|
|
46
|
+
locator = framework.page.getByTitle(selector);
|
|
47
|
+
break;
|
|
48
|
+
case 'alt':
|
|
49
|
+
locator = framework.page.getByAltText(selector);
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
locator = framework.page.locator(selector);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if element exists first
|
|
56
|
+
const count = await locator.count();
|
|
57
|
+
if (count === 0) {
|
|
58
|
+
const result = `Element ${selector} not found on page`;
|
|
59
|
+
framework.logTestStep(stepDesc, 'click', { selector, strategy, timeout, force }, result, false);
|
|
60
|
+
return `โ ${result}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await locator.waitFor({ state: 'visible', timeout });
|
|
64
|
+
await locator.click({ force });
|
|
65
|
+
await framework.takeStepScreenshot(`After clicking ${selector}`);
|
|
66
|
+
|
|
67
|
+
const result = `Successfully clicked ${selector} using ${strategy} strategy`;
|
|
68
|
+
framework.logTestStep(stepDesc, 'click', { selector, strategy, timeout, force }, result, true);
|
|
69
|
+
return result;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await framework.takeStepScreenshot(`Failed to click ${selector}`);
|
|
72
|
+
framework.logTestStep(stepDesc, 'click', { selector, strategy, timeout, force }, error.message, false);
|
|
73
|
+
return `โ Error clicking ${selector}: ${error.message}`;
|
|
74
|
+
}
|
|
75
|
+
}, {
|
|
76
|
+
name: 'click',
|
|
77
|
+
description: 'Click any element with multiple selection strategies. For buttons with text like "Log In", "Sign In", "Submit", use strategy="text" and selector="Log In". For CSS selectors use strategy="css".',
|
|
78
|
+
schema: z.object({
|
|
79
|
+
selector: z.string().describe("Element selector - for buttons use the button text (e.g. 'Log In', 'Sign In'), for CSS use actual selector"),
|
|
80
|
+
strategy: z.enum(['css', 'text', 'exact-text', 'role', 'placeholder', 'label', 'title', 'alt']).optional().describe("Selection strategy - use 'text' for button text, 'css' for CSS selectors"),
|
|
81
|
+
timeout: z.number().optional(),
|
|
82
|
+
force: z.boolean().optional(),
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createFillTool(framework) {
|
|
88
|
+
return tool(async ({ selector, value, strategy = 'fill', clearFirst = true, pressEnter = false }) => {
|
|
89
|
+
const stepDesc = `Fill ${selector} with "${value}"`;
|
|
90
|
+
console.log(`๐ ${stepDesc}`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await framework.page.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
94
|
+
|
|
95
|
+
if (clearFirst) {
|
|
96
|
+
// Focus and clear the field properly
|
|
97
|
+
await framework.page.focus(selector);
|
|
98
|
+
await framework.page.keyboard.press('Control+a');
|
|
99
|
+
await framework.page.keyboard.press('Delete');
|
|
100
|
+
|
|
101
|
+
// Wait a moment for the field to clear
|
|
102
|
+
await framework.page.waitForTimeout(100);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (strategy === 'type') {
|
|
106
|
+
await framework.page.type(selector, value, { delay: 50 });
|
|
107
|
+
} else {
|
|
108
|
+
await framework.page.fill(selector, value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (pressEnter) {
|
|
112
|
+
await framework.page.keyboard.press('Enter');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await framework.takeStepScreenshot(`After filling ${selector}`);
|
|
116
|
+
|
|
117
|
+
// Verify the value was set correctly
|
|
118
|
+
const actualValue = await framework.page.locator(selector).inputValue();
|
|
119
|
+
const success = actualValue === value;
|
|
120
|
+
const result = success
|
|
121
|
+
? `Successfully filled ${selector} with "${value}"`
|
|
122
|
+
: `Filled ${selector} but value is "${actualValue}" instead of "${value}"`;
|
|
123
|
+
|
|
124
|
+
framework.logTestStep(stepDesc, 'fill', { selector, value, strategy, clearFirst, pressEnter }, result, success);
|
|
125
|
+
return success ? `โ
${result}` : `โ ๏ธ ${result}`;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await framework.takeStepScreenshot(`Failed to fill ${selector}`);
|
|
128
|
+
framework.logTestStep(stepDesc, 'fill', { selector, value, strategy, clearFirst, pressEnter }, error.message, false);
|
|
129
|
+
return `โ Error filling ${selector}: ${error.message}`;
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
name: 'fill',
|
|
133
|
+
description: 'Fill any input field with text. Use common selectors like input[type="email"], input[name="email"], #email for email fields, input[type="password"], input[name="password"], #password for password fields.',
|
|
134
|
+
schema: z.object({
|
|
135
|
+
selector: z.string().describe("CSS selector of input field - use input[type='email'] for email, input[type='password'] for password"),
|
|
136
|
+
value: z.string().describe("Text to enter"),
|
|
137
|
+
strategy: z.enum(['fill', 'type']).optional(),
|
|
138
|
+
clearFirst: z.boolean().optional(),
|
|
139
|
+
pressEnter: z.boolean().optional(),
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createClearFieldTool(framework) {
|
|
145
|
+
return tool(async ({ selector }) => {
|
|
146
|
+
const stepDesc = `Clear field: ${selector}`;
|
|
147
|
+
console.log(`๐งน ${stepDesc}`);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await framework.page.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
151
|
+
|
|
152
|
+
// Focus the field first
|
|
153
|
+
await framework.page.focus(selector);
|
|
154
|
+
|
|
155
|
+
// Select all content and delete
|
|
156
|
+
await framework.page.keyboard.press('Control+a');
|
|
157
|
+
await framework.page.keyboard.press('Delete');
|
|
158
|
+
|
|
159
|
+
// Verify field is cleared
|
|
160
|
+
const value = await framework.page.locator(selector).inputValue();
|
|
161
|
+
const isCleared = value === '';
|
|
162
|
+
|
|
163
|
+
const result = isCleared
|
|
164
|
+
? `Successfully cleared field ${selector}`
|
|
165
|
+
: `Field ${selector} still contains: "${value}"`;
|
|
166
|
+
|
|
167
|
+
framework.logTestStep(stepDesc, 'clearField', { selector }, result, isCleared);
|
|
168
|
+
return isCleared ? `โ
${result}` : `โ ๏ธ ${result}`;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
framework.logTestStep(stepDesc, 'clearField', { selector }, error.message, false);
|
|
171
|
+
return `โ Error clearing field ${selector}: ${error.message}`;
|
|
172
|
+
}
|
|
173
|
+
}, {
|
|
174
|
+
name: 'clearField',
|
|
175
|
+
description: 'Clear an input field completely.',
|
|
176
|
+
schema: z.object({
|
|
177
|
+
selector: z.string().describe("CSS selector of the input field to clear"),
|
|
178
|
+
})
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { tool } from '@langchain/core/tools';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
export function createNavigationTool(framework) {
|
|
21
|
+
return tool(async ({ location, waitUntil = 'domcontentloaded' }) => {
|
|
22
|
+
const stepDesc = `Navigate to: ${location}`;
|
|
23
|
+
console.log(`๐ ${stepDesc}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await framework.page.goto(location, { waitUntil, timeout: 60000 });
|
|
27
|
+
await framework.takeStepScreenshot(`Page loaded: ${location}`);
|
|
28
|
+
|
|
29
|
+
framework.logTestStep(stepDesc, 'navigate', { location, waitUntil }, `Successfully navigated to: ${location}`, true);
|
|
30
|
+
return `Successfully navigated to: ${location}`;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
framework.logTestStep(stepDesc, 'navigate', { location, waitUntil }, error.message, false);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}, {
|
|
36
|
+
name: 'navigate',
|
|
37
|
+
description: 'Navigate to a URL with enhanced options.',
|
|
38
|
+
schema: z.object({
|
|
39
|
+
location: z.string().describe("URL to navigate to"),
|
|
40
|
+
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Endorphin e2e AI test framework>
|
|
2
|
+
// Copyright (C) 2025 Redstudio Agency
|
|
3
|
+
|
|
4
|
+
// This program is free software: you can redistribute it and/or modify
|
|
5
|
+
// it under the terms of the GNU Affero General Public License as
|
|
6
|
+
// published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
// License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
// This program is distributed in the hope that it will be useful,
|
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
// GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import { tool } from '@langchain/core/tools';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
|
|
21
|
+
export function createWaitTool(framework) {
|
|
22
|
+
return tool(async ({ milliseconds = 500, reason, selector, state = 'visible' }) => {
|
|
23
|
+
const stepDesc = selector
|
|
24
|
+
? `Wait for ${selector} to be ${state}`
|
|
25
|
+
: `Wait for ${milliseconds}ms${reason ? ` (${reason})` : ''}`;
|
|
26
|
+
console.log(`โฑ๏ธ ${stepDesc}`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (selector) {
|
|
30
|
+
await framework.page.waitForSelector(selector, { state, timeout: milliseconds });
|
|
31
|
+
const result = `Element ${selector} is now ${state}`;
|
|
32
|
+
framework.logTestStep(stepDesc, 'wait', { milliseconds, reason, selector, state }, result, true);
|
|
33
|
+
return `โ
${result}`;
|
|
34
|
+
} else {
|
|
35
|
+
await framework.page.waitForTimeout(milliseconds);
|
|
36
|
+
const result = `Waited for ${milliseconds}ms${reason ? ` - ${reason}` : ''}`;
|
|
37
|
+
framework.logTestStep(stepDesc, 'wait', { milliseconds, reason, selector, state }, result, true);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
framework.logTestStep(stepDesc, 'wait', { milliseconds, reason, selector, state }, error.message, false);
|
|
42
|
+
return `โ Timeout waiting for ${selector} to be ${state}`;
|
|
43
|
+
}
|
|
44
|
+
}, {
|
|
45
|
+
name: 'wait',
|
|
46
|
+
description: 'Wait for time or element state.',
|
|
47
|
+
schema: z.object({
|
|
48
|
+
milliseconds: z.number().optional(),
|
|
49
|
+
reason: z.string().optional(),
|
|
50
|
+
selector: z.string().optional(),
|
|
51
|
+
state: z.enum(['visible', 'hidden', 'attached', 'detached']).optional(),
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createScreenshotTool(framework) {
|
|
57
|
+
return tool(async ({ name, selector, fullPage = false }) => {
|
|
58
|
+
if (!framework.currentTestSession) {
|
|
59
|
+
framework.currentTestSession.screenshotCounter++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const filename = name || `manual-screenshot-${Date.now()}.png`;
|
|
63
|
+
const stepDesc = `Take screenshot: ${filename}`;
|
|
64
|
+
console.log(`๐ธ ${stepDesc}`);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
let filePath;
|
|
68
|
+
if (framework.currentTestSession) {
|
|
69
|
+
filePath = path.join(framework.currentTestSession.screenshotsDir, filename);
|
|
70
|
+
} else {
|
|
71
|
+
filePath = filename;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (selector) {
|
|
75
|
+
await framework.page.locator(selector).screenshot({ path: filePath });
|
|
76
|
+
} else {
|
|
77
|
+
await framework.page.screenshot({ path: filePath, fullPage });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = selector
|
|
81
|
+
? `Screenshot of ${selector} saved as ${filename}`
|
|
82
|
+
: `${fullPage ? 'Full page' : 'Viewport'} screenshot saved as ${filename}`;
|
|
83
|
+
|
|
84
|
+
framework.logTestStep(stepDesc, 'screenshot', { name, selector, fullPage }, result, true);
|
|
85
|
+
return `๐ธ ${result}`;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
framework.logTestStep(stepDesc, 'screenshot', { name, selector, fullPage }, error.message, false);
|
|
88
|
+
return `โ Error taking screenshot: ${error.message}`;
|
|
89
|
+
}
|
|
90
|
+
}, {
|
|
91
|
+
name: 'screenshot',
|
|
92
|
+
description: 'Take screenshot for documentation/debugging.',
|
|
93
|
+
schema: z.object({
|
|
94
|
+
name: z.string().optional(),
|
|
95
|
+
selector: z.string().optional(),
|
|
96
|
+
fullPage: z.boolean().optional(),
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
}
|