chrometools-mcp 2.4.2 → 3.1.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/CHANGELOG.md +540 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +494 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/element-finder-utils.js +138 -28
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/figma-tools.js +120 -0
- package/index.js +3347 -2518
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -656
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +367 -296
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/SelectModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for select (dropdown) elements.
|
|
5
|
+
* Supports selection by value, text, or index.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
9
|
+
|
|
10
|
+
export class SelectModel extends BaseInputModel {
|
|
11
|
+
static get inputTypes() {
|
|
12
|
+
return ['select', 'select-one', 'select-multiple'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this model handles the given element
|
|
17
|
+
* Override to check tagName for select elements
|
|
18
|
+
*/
|
|
19
|
+
static handlesElement(tagName, inputType) {
|
|
20
|
+
return tagName?.toLowerCase() === 'select';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Select option by value, text, or index
|
|
25
|
+
* @param {string|number} value - Value to select
|
|
26
|
+
* @param {object} options - { by: 'value'|'text'|'index' }
|
|
27
|
+
*/
|
|
28
|
+
async setValue(value, options = {}) {
|
|
29
|
+
const { by = 'value' } = options;
|
|
30
|
+
|
|
31
|
+
await this.element.evaluate((el, val, selectBy) => {
|
|
32
|
+
let found = false;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < el.options.length; i++) {
|
|
35
|
+
const option = el.options[i];
|
|
36
|
+
let match = false;
|
|
37
|
+
|
|
38
|
+
switch (selectBy) {
|
|
39
|
+
case 'index':
|
|
40
|
+
match = i === parseInt(val, 10);
|
|
41
|
+
break;
|
|
42
|
+
case 'text':
|
|
43
|
+
match = option.text === val || option.text.includes(val);
|
|
44
|
+
break;
|
|
45
|
+
case 'value':
|
|
46
|
+
default:
|
|
47
|
+
match = option.value === val;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (match) {
|
|
52
|
+
el.selectedIndex = i;
|
|
53
|
+
found = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!found) {
|
|
59
|
+
throw new Error(`Option not found: ${val} (by ${selectBy})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
63
|
+
}, value, by);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all available options
|
|
68
|
+
*/
|
|
69
|
+
async getOptions() {
|
|
70
|
+
return await this.element.evaluate(el => {
|
|
71
|
+
return Array.from(el.options).map((opt, i) => ({
|
|
72
|
+
index: i,
|
|
73
|
+
value: opt.value,
|
|
74
|
+
text: opt.text,
|
|
75
|
+
selected: opt.selected,
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getMetadata() {
|
|
81
|
+
const base = await super.getMetadata();
|
|
82
|
+
const extra = await this.element.evaluate(el => ({
|
|
83
|
+
multiple: el.multiple,
|
|
84
|
+
selectedIndex: el.selectedIndex,
|
|
85
|
+
options: Array.from(el.options).map(opt => ({
|
|
86
|
+
value: opt.value,
|
|
87
|
+
text: opt.text,
|
|
88
|
+
selected: opt.selected,
|
|
89
|
+
})),
|
|
90
|
+
}));
|
|
91
|
+
return { ...base, ...extra };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getActionDescription(value, identifier) {
|
|
95
|
+
return `Selected "${value}" on ${identifier}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/TextInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for standard text-based inputs.
|
|
5
|
+
* Handles: text, email, tel, password, search, url, number
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
9
|
+
|
|
10
|
+
export class TextInputModel extends BaseInputModel {
|
|
11
|
+
static get inputTypes() {
|
|
12
|
+
return ['text', 'email', 'tel', 'password', 'search', 'url', 'number', null];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type text into the input using keyboard simulation
|
|
17
|
+
* @param {string} value - Text to type
|
|
18
|
+
* @param {object} options - { delay, clearFirst }
|
|
19
|
+
*/
|
|
20
|
+
async setValue(value, options = {}) {
|
|
21
|
+
const { delay = 0, clearFirst = true } = options;
|
|
22
|
+
|
|
23
|
+
if (clearFirst) {
|
|
24
|
+
await this.element.click({ clickCount: 3 });
|
|
25
|
+
await this.page.keyboard.press('Backspace');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await this.element.type(value, { delay });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getActionDescription(value, identifier) {
|
|
32
|
+
return `Typed "${value}" into ${identifier}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/TextareaModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for textarea elements.
|
|
5
|
+
* Similar to TextInputModel but handles multiline text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
9
|
+
|
|
10
|
+
export class TextareaModel extends BaseInputModel {
|
|
11
|
+
static get inputTypes() {
|
|
12
|
+
return ['textarea'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this model handles the given element
|
|
17
|
+
* Override to check tagName for textarea elements
|
|
18
|
+
*/
|
|
19
|
+
static handlesElement(tagName, inputType) {
|
|
20
|
+
return tagName?.toLowerCase() === 'textarea';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type text into the textarea
|
|
25
|
+
* @param {string} value - Text to type (can include newlines)
|
|
26
|
+
* @param {object} options - { delay, clearFirst }
|
|
27
|
+
*/
|
|
28
|
+
async setValue(value, options = {}) {
|
|
29
|
+
const { delay = 0, clearFirst = true } = options;
|
|
30
|
+
|
|
31
|
+
if (clearFirst) {
|
|
32
|
+
await this.element.click({ clickCount: 3 });
|
|
33
|
+
await this.page.keyboard.press('Backspace');
|
|
34
|
+
// For multiline, also clear with Ctrl+A
|
|
35
|
+
await this.page.keyboard.down('Control');
|
|
36
|
+
await this.page.keyboard.press('a');
|
|
37
|
+
await this.page.keyboard.up('Control');
|
|
38
|
+
await this.page.keyboard.press('Backspace');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await this.element.type(value, { delay });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getMetadata() {
|
|
45
|
+
const base = await super.getMetadata();
|
|
46
|
+
const extra = await this.element.evaluate(el => ({
|
|
47
|
+
rows: el.rows,
|
|
48
|
+
cols: el.cols,
|
|
49
|
+
maxLength: el.maxLength > 0 ? el.maxLength : null,
|
|
50
|
+
placeholder: el.placeholder || null,
|
|
51
|
+
}));
|
|
52
|
+
return { ...base, ...extra };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getActionDescription(value, identifier) {
|
|
56
|
+
const preview = value.length > 30 ? value.substring(0, 30) + '...' : value;
|
|
57
|
+
return `Typed "${preview}" into ${identifier}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/TimeInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for time input fields.
|
|
5
|
+
* Sets value directly via JavaScript since keyboard input doesn't work reliably.
|
|
6
|
+
*
|
|
7
|
+
* Expected format: "HH:MM" (24-hour) or "HH:MM:SS"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
11
|
+
|
|
12
|
+
export class TimeInputModel extends BaseInputModel {
|
|
13
|
+
static get inputTypes() {
|
|
14
|
+
return ['time'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set time value directly via JavaScript
|
|
19
|
+
* @param {string} value - Time in format "HH:MM" or "HH:MM:SS"
|
|
20
|
+
* @param {object} options - (unused, for interface compatibility)
|
|
21
|
+
*/
|
|
22
|
+
async setValue(value, options = {}) {
|
|
23
|
+
// Validate time format
|
|
24
|
+
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
|
|
25
|
+
if (!timeRegex.test(value)) {
|
|
26
|
+
console.warn(`TimeInputModel: Value "${value}" may not be in correct format (HH:MM or HH:MM:SS)`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await this.element.evaluate((el, val) => {
|
|
30
|
+
el.value = val;
|
|
31
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
32
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
33
|
+
}, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getMetadata() {
|
|
37
|
+
const base = await super.getMetadata();
|
|
38
|
+
const extra = await this.element.evaluate(el => ({
|
|
39
|
+
min: el.min || null,
|
|
40
|
+
max: el.max || null,
|
|
41
|
+
step: el.step || null,
|
|
42
|
+
}));
|
|
43
|
+
return { ...base, ...extra };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getActionDescription(value, identifier) {
|
|
47
|
+
return `Set time "${value}" on ${identifier}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/models/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/index.js
|
|
3
|
+
*
|
|
4
|
+
* Input element models for chrometools-mcp.
|
|
5
|
+
* Provides specialized handlers for different input types.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { getInputModel, InputModelFactory } from './models/index.js';
|
|
9
|
+
*
|
|
10
|
+
* const model = await getInputModel(element, page);
|
|
11
|
+
* await model.setValue('some value');
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
15
|
+
import { TextInputModel } from './TextInputModel.js';
|
|
16
|
+
import { TimeInputModel } from './TimeInputModel.js';
|
|
17
|
+
import { DateInputModel } from './DateInputModel.js';
|
|
18
|
+
import { ColorInputModel } from './ColorInputModel.js';
|
|
19
|
+
import { RangeInputModel } from './RangeInputModel.js';
|
|
20
|
+
import { SelectModel } from './SelectModel.js';
|
|
21
|
+
import { CheckboxModel } from './CheckboxModel.js';
|
|
22
|
+
import { TextareaModel } from './TextareaModel.js';
|
|
23
|
+
import { RadioGroupModel } from './RadioGroupModel.js';
|
|
24
|
+
import { CheckboxGroupModel } from './CheckboxGroupModel.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registry of all input models in priority order.
|
|
28
|
+
* More specific models should come before generic ones.
|
|
29
|
+
*/
|
|
30
|
+
const MODEL_REGISTRY = [
|
|
31
|
+
TimeInputModel,
|
|
32
|
+
DateInputModel,
|
|
33
|
+
ColorInputModel,
|
|
34
|
+
RangeInputModel,
|
|
35
|
+
CheckboxModel,
|
|
36
|
+
SelectModel,
|
|
37
|
+
TextareaModel,
|
|
38
|
+
TextInputModel, // Default fallback for text-like inputs
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Factory class for creating appropriate input models
|
|
43
|
+
*/
|
|
44
|
+
export class InputModelFactory {
|
|
45
|
+
/**
|
|
46
|
+
* Get element info (tagName, inputType)
|
|
47
|
+
*/
|
|
48
|
+
static async getElementInfo(element) {
|
|
49
|
+
return await element.evaluate(el => ({
|
|
50
|
+
tagName: el.tagName.toLowerCase(),
|
|
51
|
+
inputType: el.type?.toLowerCase() || null,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find the appropriate model class for an element
|
|
57
|
+
*/
|
|
58
|
+
static findModelClass(tagName, inputType) {
|
|
59
|
+
// Check for element-specific handlers first (select, textarea)
|
|
60
|
+
for (const ModelClass of MODEL_REGISTRY) {
|
|
61
|
+
if (ModelClass.handlesElement && ModelClass.handlesElement(tagName, inputType)) {
|
|
62
|
+
return ModelClass;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Then check input type handlers
|
|
67
|
+
for (const ModelClass of MODEL_REGISTRY) {
|
|
68
|
+
if (ModelClass.handles(inputType)) {
|
|
69
|
+
return ModelClass;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fallback to TextInputModel for unknown types
|
|
74
|
+
return TextInputModel;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a model instance for the given element
|
|
79
|
+
* @param {ElementHandle} element - Puppeteer element handle
|
|
80
|
+
* @param {Page} page - Puppeteer page
|
|
81
|
+
* @returns {Promise<BaseInputModel>} Model instance
|
|
82
|
+
*/
|
|
83
|
+
static async create(element, page) {
|
|
84
|
+
const { tagName, inputType } = await this.getElementInfo(element);
|
|
85
|
+
const ModelClass = this.findModelClass(tagName, inputType);
|
|
86
|
+
return new ModelClass(element, page);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get model class name for an element (useful for logging)
|
|
91
|
+
*/
|
|
92
|
+
static async getModelName(element) {
|
|
93
|
+
const { tagName, inputType } = await this.getElementInfo(element);
|
|
94
|
+
const ModelClass = this.findModelClass(tagName, inputType);
|
|
95
|
+
return ModelClass.name;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convenience function to get a model for an element
|
|
101
|
+
* @param {ElementHandle} element - Puppeteer element handle
|
|
102
|
+
* @param {Page} page - Puppeteer page
|
|
103
|
+
* @returns {Promise<BaseInputModel>} Model instance
|
|
104
|
+
*/
|
|
105
|
+
export async function getInputModel(element, page) {
|
|
106
|
+
return await InputModelFactory.create(element, page);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Export all models for direct use if needed
|
|
110
|
+
export {
|
|
111
|
+
BaseInputModel,
|
|
112
|
+
TextInputModel,
|
|
113
|
+
TimeInputModel,
|
|
114
|
+
DateInputModel,
|
|
115
|
+
ColorInputModel,
|
|
116
|
+
RangeInputModel,
|
|
117
|
+
SelectModel,
|
|
118
|
+
CheckboxModel,
|
|
119
|
+
TextareaModel,
|
|
120
|
+
RadioGroupModel,
|
|
121
|
+
CheckboxGroupModel,
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, visual testing, Figma comparison
|
|
3
|
+
"version": "3.1.2",
|
|
4
|
+
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"jimp": "^0.22.12",
|
|
51
51
|
"pixelmatch": "^7.1.0",
|
|
52
52
|
"puppeteer": "^24.27.0",
|
|
53
|
+
"ws": "^8.18.0",
|
|
53
54
|
"zod": "^3.25.76"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pom/apom-converter.js
|
|
3
|
+
*
|
|
4
|
+
* Converts analyzePage output to APOM (Agent Page Object Model) format
|
|
5
|
+
* Adds unique IDs and registers elements in selector resolver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert analyzePage result to APOM format
|
|
10
|
+
* This function runs in browser context via page.evaluate()
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} analysis - Result from analyzePage
|
|
13
|
+
* @param {Object} options - Conversion options
|
|
14
|
+
* @returns {Object} APOM formatted result
|
|
15
|
+
*/
|
|
16
|
+
function convertToAPOM(analysis, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
registerElements = true,
|
|
19
|
+
groupBy = 'type'
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
// Generate pageId from URL + timestamp
|
|
23
|
+
const pageId = `page_${btoa(analysis.url).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
|
|
24
|
+
|
|
25
|
+
const result = {
|
|
26
|
+
pageId,
|
|
27
|
+
url: analysis.url,
|
|
28
|
+
title: analysis.title,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
elements: {},
|
|
31
|
+
groups: {},
|
|
32
|
+
metadata: {
|
|
33
|
+
totalElements: 0,
|
|
34
|
+
interactiveCount: 0,
|
|
35
|
+
formCount: analysis.forms?.length || 0
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const elementsToRegister = [];
|
|
40
|
+
let elementCounter = { form: 0, input: 0, textarea: 0, select: 0, button: 0, link: 0, element: 0 };
|
|
41
|
+
|
|
42
|
+
// Process forms
|
|
43
|
+
if (analysis.forms && analysis.forms.length > 0) {
|
|
44
|
+
result.groups.forms = [];
|
|
45
|
+
|
|
46
|
+
analysis.forms.forEach((form, formIdx) => {
|
|
47
|
+
const formId = generateElementId(null, 'form', elementCounter.form++);
|
|
48
|
+
|
|
49
|
+
const formElement = {
|
|
50
|
+
id: formId,
|
|
51
|
+
type: 'form',
|
|
52
|
+
selector: form.selector,
|
|
53
|
+
method: form.method || 'GET',
|
|
54
|
+
action: form.action || '',
|
|
55
|
+
fields: {
|
|
56
|
+
inputs: {},
|
|
57
|
+
textareas: {},
|
|
58
|
+
selects: {},
|
|
59
|
+
checkboxes: {},
|
|
60
|
+
radios: {}
|
|
61
|
+
},
|
|
62
|
+
submitButtons: [],
|
|
63
|
+
resetButtons: [],
|
|
64
|
+
validation: {
|
|
65
|
+
valid: false,
|
|
66
|
+
requiredFields: [],
|
|
67
|
+
invalidFields: []
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Process form fields
|
|
72
|
+
if (form.fields) {
|
|
73
|
+
form.fields.forEach((field, fieldIdx) => {
|
|
74
|
+
let fieldType = 'input';
|
|
75
|
+
if (field.type === 'textarea') fieldType = 'textarea';
|
|
76
|
+
else if (field.type === 'select') fieldType = 'select';
|
|
77
|
+
else if (field.type === 'checkbox') fieldType = 'checkbox';
|
|
78
|
+
else if (field.type === 'radio') fieldType = 'radio';
|
|
79
|
+
|
|
80
|
+
const fieldId = generateElementId(null, fieldType, elementCounter[fieldType] || 0);
|
|
81
|
+
elementCounter[fieldType] = (elementCounter[fieldType] || 0) + 1;
|
|
82
|
+
|
|
83
|
+
const fieldElement = {
|
|
84
|
+
id: fieldId,
|
|
85
|
+
type: fieldType,
|
|
86
|
+
selector: field.selector,
|
|
87
|
+
name: field.name,
|
|
88
|
+
placeholder: field.placeholder,
|
|
89
|
+
required: field.required || false,
|
|
90
|
+
label: field.label,
|
|
91
|
+
value: field.value || '',
|
|
92
|
+
disabled: field.disabled || false
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add select-specific data
|
|
96
|
+
if (fieldType === 'select' && field.options) {
|
|
97
|
+
fieldElement.options = field.options;
|
|
98
|
+
fieldElement.selectedValue = field.selectedValue;
|
|
99
|
+
fieldElement.selectedText = field.selectedText;
|
|
100
|
+
fieldElement.selectedIndex = field.selectedIndex || -1;
|
|
101
|
+
fieldElement.multiple = field.multiple || false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add UI framework info
|
|
105
|
+
if (field.uiFramework) {
|
|
106
|
+
fieldElement.uiFramework = field.uiFramework;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add to form fields
|
|
110
|
+
if (fieldType === 'checkbox') {
|
|
111
|
+
formElement.fields.checkboxes[fieldId] = fieldElement;
|
|
112
|
+
} else if (fieldType === 'radio') {
|
|
113
|
+
formElement.fields.radios[fieldId] = fieldElement;
|
|
114
|
+
} else if (fieldType === 'select') {
|
|
115
|
+
formElement.fields.selects[fieldId] = fieldElement;
|
|
116
|
+
} else if (fieldType === 'textarea') {
|
|
117
|
+
formElement.fields.textareas[fieldId] = fieldElement;
|
|
118
|
+
} else {
|
|
119
|
+
formElement.fields.inputs[fieldId] = fieldElement;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add to global elements
|
|
123
|
+
result.elements[fieldId] = fieldElement;
|
|
124
|
+
elementsToRegister.push({ id: fieldId, selector: field.selector });
|
|
125
|
+
|
|
126
|
+
// Track required fields
|
|
127
|
+
if (field.required) {
|
|
128
|
+
formElement.validation.requiredFields.push(fieldId);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process submit button
|
|
134
|
+
if (form.submitButton) {
|
|
135
|
+
const buttonId = generateElementId(null, 'button', elementCounter.button++);
|
|
136
|
+
const buttonElement = {
|
|
137
|
+
id: buttonId,
|
|
138
|
+
type: 'button',
|
|
139
|
+
buttonType: 'submit',
|
|
140
|
+
selector: form.submitButton.selector,
|
|
141
|
+
text: form.submitButton.text,
|
|
142
|
+
disabled: false
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
formElement.submitButtons.push(buttonElement);
|
|
146
|
+
result.elements[buttonId] = buttonElement;
|
|
147
|
+
elementsToRegister.push({ id: buttonId, selector: form.submitButton.selector });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add form to elements and groups
|
|
151
|
+
result.elements[formId] = formElement;
|
|
152
|
+
result.groups.forms.push(formElement);
|
|
153
|
+
elementsToRegister.push({ id: formId, selector: form.selector });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Process inputs (that are not in forms)
|
|
158
|
+
if (analysis.inputs && analysis.inputs.length > 0) {
|
|
159
|
+
result.groups.inputs = [];
|
|
160
|
+
|
|
161
|
+
analysis.inputs.forEach(input => {
|
|
162
|
+
let inputType = 'input';
|
|
163
|
+
if (input.type === 'select') inputType = 'select';
|
|
164
|
+
else if (input.type === 'textarea') inputType = 'textarea';
|
|
165
|
+
|
|
166
|
+
const inputId = generateElementId(null, inputType, elementCounter[inputType]++);
|
|
167
|
+
|
|
168
|
+
const inputElement = {
|
|
169
|
+
id: inputId,
|
|
170
|
+
type: inputType,
|
|
171
|
+
selector: input.selector,
|
|
172
|
+
name: input.name,
|
|
173
|
+
placeholder: input.placeholder
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Add select-specific data
|
|
177
|
+
if (inputType === 'select' && input.options) {
|
|
178
|
+
inputElement.options = input.options;
|
|
179
|
+
inputElement.selectedValue = input.selectedValue;
|
|
180
|
+
inputElement.selectedText = input.selectedText;
|
|
181
|
+
inputElement.selectedIndex = input.selectedIndex || -1;
|
|
182
|
+
inputElement.multiple = input.multiple || false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add UI framework info
|
|
186
|
+
if (input.uiFramework) {
|
|
187
|
+
inputElement.uiFramework = input.uiFramework;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
result.elements[inputId] = inputElement;
|
|
191
|
+
result.groups.inputs.push(inputElement);
|
|
192
|
+
elementsToRegister.push({ id: inputId, selector: input.selector });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Process buttons
|
|
197
|
+
if (analysis.buttons && analysis.buttons.length > 0) {
|
|
198
|
+
result.groups.buttons = [];
|
|
199
|
+
|
|
200
|
+
analysis.buttons.forEach(button => {
|
|
201
|
+
const buttonId = generateElementId(null, 'button', elementCounter.button++);
|
|
202
|
+
|
|
203
|
+
const buttonElement = {
|
|
204
|
+
id: buttonId,
|
|
205
|
+
type: 'button',
|
|
206
|
+
buttonType: button.type || 'button',
|
|
207
|
+
selector: button.selector,
|
|
208
|
+
text: button.text,
|
|
209
|
+
inForm: button.inForm || false
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
result.elements[buttonId] = buttonElement;
|
|
213
|
+
result.groups.buttons.push(buttonElement);
|
|
214
|
+
elementsToRegister.push({ id: buttonId, selector: button.selector });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Process links
|
|
219
|
+
if (analysis.links && analysis.links.length > 0) {
|
|
220
|
+
result.groups.links = [];
|
|
221
|
+
|
|
222
|
+
analysis.links.forEach(link => {
|
|
223
|
+
const linkId = generateElementId(null, 'link', elementCounter.link++);
|
|
224
|
+
|
|
225
|
+
const linkElement = {
|
|
226
|
+
id: linkId,
|
|
227
|
+
type: 'link',
|
|
228
|
+
selector: link.selector,
|
|
229
|
+
text: link.text,
|
|
230
|
+
href: link.href
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
result.elements[linkId] = linkElement;
|
|
234
|
+
result.groups.links.push(linkElement);
|
|
235
|
+
elementsToRegister.push({ id: linkId, selector: link.selector });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Update metadata
|
|
240
|
+
result.metadata.totalElements = Object.keys(result.elements).length;
|
|
241
|
+
result.metadata.interactiveCount = result.metadata.totalElements;
|
|
242
|
+
|
|
243
|
+
// Register elements if requested (registerElements is boolean flag in options)
|
|
244
|
+
// The actual registration happens in the calling code in index.js after conversion
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Helper function: generateElementId
|
|
250
|
+
// Simple version - uses type + context + index
|
|
251
|
+
function generateElementId(element, type, index) {
|
|
252
|
+
return `${type}_${index}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Export for use in both Node.js and browser context
|
|
256
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
257
|
+
module.exports = {
|
|
258
|
+
convertToAPOM,
|
|
259
|
+
generateElementId
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Make available globally in browser context
|
|
264
|
+
if (typeof window !== 'undefined') {
|
|
265
|
+
window.convertToAPOM = convertToAPOM;
|
|
266
|
+
window.generateElementId = generateElementId;
|
|
267
|
+
}
|