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,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/BaseInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Base class for all input element models.
|
|
5
|
+
* Provides common interface for interacting with form elements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class BaseInputModel {
|
|
9
|
+
constructor(element, page) {
|
|
10
|
+
this.element = element;
|
|
11
|
+
this.page = page;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the input type (to be overridden by subclasses)
|
|
16
|
+
*/
|
|
17
|
+
static get inputTypes() {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if this model handles the given input type
|
|
23
|
+
*/
|
|
24
|
+
static handles(inputType) {
|
|
25
|
+
return this.inputTypes.includes(inputType?.toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set value on the element
|
|
30
|
+
* @param {string} value - Value to set
|
|
31
|
+
* @param {object} options - Additional options (delay, clearFirst, etc.)
|
|
32
|
+
*/
|
|
33
|
+
async setValue(value, options = {}) {
|
|
34
|
+
throw new Error('setValue must be implemented by subclass');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get current value from the element
|
|
39
|
+
*/
|
|
40
|
+
async getValue() {
|
|
41
|
+
return await this.element.evaluate(el => el.value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear the element value
|
|
46
|
+
*/
|
|
47
|
+
async clear() {
|
|
48
|
+
await this.element.evaluate(el => {
|
|
49
|
+
el.value = '';
|
|
50
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
51
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Focus on the element
|
|
57
|
+
*/
|
|
58
|
+
async focus() {
|
|
59
|
+
await this.element.focus();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dispatch input and change events (for framework compatibility)
|
|
64
|
+
*/
|
|
65
|
+
async dispatchEvents() {
|
|
66
|
+
await this.element.evaluate(el => {
|
|
67
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
68
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get element metadata
|
|
74
|
+
*/
|
|
75
|
+
async getMetadata() {
|
|
76
|
+
return await this.element.evaluate(el => ({
|
|
77
|
+
tagName: el.tagName.toLowerCase(),
|
|
78
|
+
type: el.type,
|
|
79
|
+
name: el.name,
|
|
80
|
+
id: el.id,
|
|
81
|
+
disabled: el.disabled,
|
|
82
|
+
required: el.required,
|
|
83
|
+
value: el.value,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get description of the action performed (for logging)
|
|
89
|
+
*/
|
|
90
|
+
getActionDescription(value, identifier) {
|
|
91
|
+
return `Set value "${value}" on ${identifier}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/CheckboxGroupModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for checkbox groups.
|
|
5
|
+
* Allows selecting multiple options from a group by name, values, or label texts.
|
|
6
|
+
* Supports set (replace all), add, remove, and toggle modes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
10
|
+
|
|
11
|
+
export class CheckboxGroupModel extends BaseInputModel {
|
|
12
|
+
/**
|
|
13
|
+
* Create a CheckboxGroupModel
|
|
14
|
+
* @param {string} groupName - The name attribute of the checkbox group
|
|
15
|
+
* @param {Page} page - Puppeteer page
|
|
16
|
+
*/
|
|
17
|
+
constructor(groupName, page) {
|
|
18
|
+
super(null, page);
|
|
19
|
+
this.groupName = groupName;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all options in this checkbox group
|
|
24
|
+
*/
|
|
25
|
+
async getOptions() {
|
|
26
|
+
return await this.page.evaluate((name) => {
|
|
27
|
+
const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
|
|
28
|
+
return Array.from(inputs).map(input => {
|
|
29
|
+
// Find label text
|
|
30
|
+
let label = null;
|
|
31
|
+
const parentLabel = input.closest('label');
|
|
32
|
+
if (parentLabel) {
|
|
33
|
+
label = parentLabel.textContent?.trim();
|
|
34
|
+
} else if (input.id) {
|
|
35
|
+
const labelFor = document.querySelector(`label[for="${input.id}"]`);
|
|
36
|
+
if (labelFor) {
|
|
37
|
+
label = labelFor.textContent?.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
label = label || input.getAttribute('aria-label') || input.value;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
value: input.value,
|
|
44
|
+
label,
|
|
45
|
+
checked: input.checked,
|
|
46
|
+
disabled: input.disabled
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}, this.groupName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get currently selected values
|
|
54
|
+
*/
|
|
55
|
+
async getValue() {
|
|
56
|
+
return await this.page.evaluate((name) => {
|
|
57
|
+
const checked = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
|
|
58
|
+
return Array.from(checked).map(input => input.value);
|
|
59
|
+
}, this.groupName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set checkbox values
|
|
64
|
+
* @param {string|string[]} values - Value(s) or label text(s) to select
|
|
65
|
+
* @param {object} options - { mode: 'set'|'add'|'remove'|'toggle', by: 'value'|'text'|'auto' }
|
|
66
|
+
*/
|
|
67
|
+
async setValue(values, options = {}) {
|
|
68
|
+
const { mode = 'set', by = 'auto' } = options;
|
|
69
|
+
const valueList = Array.isArray(values) ? values : [values];
|
|
70
|
+
|
|
71
|
+
const result = await this.page.evaluate((name, vals, selectMode, selectBy) => {
|
|
72
|
+
const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
|
|
73
|
+
const changes = [];
|
|
74
|
+
|
|
75
|
+
// Helper to check if value matches
|
|
76
|
+
const matches = (input, val) => {
|
|
77
|
+
// Get label text
|
|
78
|
+
let label = null;
|
|
79
|
+
const parentLabel = input.closest('label');
|
|
80
|
+
if (parentLabel) {
|
|
81
|
+
label = parentLabel.textContent?.trim();
|
|
82
|
+
} else if (input.id) {
|
|
83
|
+
const labelFor = document.querySelector(`label[for="${input.id}"]`);
|
|
84
|
+
if (labelFor) {
|
|
85
|
+
label = labelFor.textContent?.trim();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
label = label || input.getAttribute('aria-label');
|
|
89
|
+
|
|
90
|
+
switch (selectBy) {
|
|
91
|
+
case 'value':
|
|
92
|
+
return input.value === val;
|
|
93
|
+
case 'text':
|
|
94
|
+
return label && (label === val || label.toLowerCase().includes(val.toLowerCase()));
|
|
95
|
+
case 'auto':
|
|
96
|
+
default:
|
|
97
|
+
return input.value === val ||
|
|
98
|
+
(label && (label === val || label.toLowerCase().includes(val.toLowerCase())));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// If mode is 'set', first uncheck all (that are not in values)
|
|
103
|
+
if (selectMode === 'set') {
|
|
104
|
+
for (const input of inputs) {
|
|
105
|
+
if (input.disabled) continue;
|
|
106
|
+
const shouldBeChecked = vals.some(val => matches(input, val));
|
|
107
|
+
if (input.checked !== shouldBeChecked) {
|
|
108
|
+
input.checked = shouldBeChecked;
|
|
109
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
110
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
111
|
+
changes.push({ value: input.value, checked: shouldBeChecked });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// For add/remove/toggle modes
|
|
116
|
+
for (const val of vals) {
|
|
117
|
+
for (const input of inputs) {
|
|
118
|
+
if (input.disabled) continue;
|
|
119
|
+
if (!matches(input, val)) continue;
|
|
120
|
+
|
|
121
|
+
let newState = input.checked;
|
|
122
|
+
switch (selectMode) {
|
|
123
|
+
case 'add':
|
|
124
|
+
newState = true;
|
|
125
|
+
break;
|
|
126
|
+
case 'remove':
|
|
127
|
+
newState = false;
|
|
128
|
+
break;
|
|
129
|
+
case 'toggle':
|
|
130
|
+
newState = !input.checked;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (input.checked !== newState) {
|
|
135
|
+
input.checked = newState;
|
|
136
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
137
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
138
|
+
changes.push({ value: input.value, checked: newState });
|
|
139
|
+
}
|
|
140
|
+
break; // Found matching input for this value
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get final state
|
|
146
|
+
const checked = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
changes,
|
|
150
|
+
selectedValues: Array.from(checked).map(input => input.value)
|
|
151
|
+
};
|
|
152
|
+
}, this.groupName, valueList, mode, by);
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check specific values (add mode)
|
|
159
|
+
*/
|
|
160
|
+
async check(values) {
|
|
161
|
+
return await this.setValue(values, { mode: 'add' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Uncheck specific values (remove mode)
|
|
166
|
+
*/
|
|
167
|
+
async uncheck(values) {
|
|
168
|
+
return await this.setValue(values, { mode: 'remove' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Toggle specific values
|
|
173
|
+
*/
|
|
174
|
+
async toggle(values) {
|
|
175
|
+
return await this.setValue(values, { mode: 'toggle' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clear all checkboxes
|
|
180
|
+
*/
|
|
181
|
+
async clear() {
|
|
182
|
+
return await this.page.evaluate((name) => {
|
|
183
|
+
const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
|
|
184
|
+
inputs.forEach(input => {
|
|
185
|
+
if (!input.disabled) {
|
|
186
|
+
input.checked = false;
|
|
187
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
188
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return { success: true, cleared: inputs.length };
|
|
192
|
+
}, this.groupName);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getActionDescription(values, identifier) {
|
|
196
|
+
const valueList = Array.isArray(values) ? values : [values];
|
|
197
|
+
return `Selected [${valueList.join(', ')}] in checkbox group "${this.groupName}"`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/CheckboxModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for checkbox and radio button inputs.
|
|
5
|
+
* Supports check, uncheck, toggle, and click operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
9
|
+
|
|
10
|
+
export class CheckboxModel extends BaseInputModel {
|
|
11
|
+
static get inputTypes() {
|
|
12
|
+
return ['checkbox', 'radio'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set checkbox/radio state
|
|
17
|
+
* @param {string|boolean} value - 'check', 'uncheck', 'toggle', true, false
|
|
18
|
+
* @param {object} options - (unused, for interface compatibility)
|
|
19
|
+
*/
|
|
20
|
+
async setValue(value, options = {}) {
|
|
21
|
+
const normalizedValue = this._normalizeValue(value);
|
|
22
|
+
|
|
23
|
+
await this.element.evaluate((el, shouldBeChecked) => {
|
|
24
|
+
if (shouldBeChecked === 'toggle') {
|
|
25
|
+
el.checked = !el.checked;
|
|
26
|
+
} else {
|
|
27
|
+
el.checked = shouldBeChecked;
|
|
28
|
+
}
|
|
29
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
30
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
31
|
+
}, normalizedValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_normalizeValue(value) {
|
|
35
|
+
if (typeof value === 'boolean') {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const strValue = String(value).toLowerCase();
|
|
40
|
+
|
|
41
|
+
if (['check', 'checked', 'true', 'yes', '1', 'on'].includes(strValue)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (['uncheck', 'unchecked', 'false', 'no', '0', 'off'].includes(strValue)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (strValue === 'toggle') {
|
|
48
|
+
return 'toggle';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default: treat as check
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check the checkbox/radio
|
|
57
|
+
*/
|
|
58
|
+
async check() {
|
|
59
|
+
await this.setValue(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Uncheck the checkbox (not applicable to radio)
|
|
64
|
+
*/
|
|
65
|
+
async uncheck() {
|
|
66
|
+
await this.setValue(false);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Toggle the checkbox state
|
|
71
|
+
*/
|
|
72
|
+
async toggle() {
|
|
73
|
+
await this.setValue('toggle');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if currently checked
|
|
78
|
+
*/
|
|
79
|
+
async isChecked() {
|
|
80
|
+
return await this.element.evaluate(el => el.checked);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getValue() {
|
|
84
|
+
return await this.isChecked();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getMetadata() {
|
|
88
|
+
const base = await super.getMetadata();
|
|
89
|
+
const extra = await this.element.evaluate(el => ({
|
|
90
|
+
checked: el.checked,
|
|
91
|
+
inputValue: el.value, // The value attribute (sent when checked)
|
|
92
|
+
}));
|
|
93
|
+
return { ...base, ...extra };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getActionDescription(value, identifier) {
|
|
97
|
+
const normalizedValue = this._normalizeValue(value);
|
|
98
|
+
if (normalizedValue === 'toggle') {
|
|
99
|
+
return `Toggled ${identifier}`;
|
|
100
|
+
}
|
|
101
|
+
return normalizedValue ? `Checked ${identifier}` : `Unchecked ${identifier}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/ColorInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for color picker inputs.
|
|
5
|
+
* Sets value directly via JavaScript.
|
|
6
|
+
*
|
|
7
|
+
* Expected format: "#RRGGBB" (hex color)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
11
|
+
|
|
12
|
+
export class ColorInputModel extends BaseInputModel {
|
|
13
|
+
static get inputTypes() {
|
|
14
|
+
return ['color'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set color value directly via JavaScript
|
|
19
|
+
* @param {string} value - Color in hex format "#RRGGBB"
|
|
20
|
+
* @param {object} options - (unused, for interface compatibility)
|
|
21
|
+
*/
|
|
22
|
+
async setValue(value, options = {}) {
|
|
23
|
+
// Normalize color format
|
|
24
|
+
let normalizedValue = value;
|
|
25
|
+
|
|
26
|
+
// Add # if missing
|
|
27
|
+
if (!normalizedValue.startsWith('#')) {
|
|
28
|
+
normalizedValue = '#' + normalizedValue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Convert 3-char hex to 6-char
|
|
32
|
+
if (/^#[0-9A-Fa-f]{3}$/.test(normalizedValue)) {
|
|
33
|
+
normalizedValue = '#' + normalizedValue[1] + normalizedValue[1] +
|
|
34
|
+
normalizedValue[2] + normalizedValue[2] +
|
|
35
|
+
normalizedValue[3] + normalizedValue[3];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate hex color format
|
|
39
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(normalizedValue)) {
|
|
40
|
+
console.warn(`ColorInputModel: Value "${value}" may not be a valid hex color`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await this.element.evaluate((el, val) => {
|
|
44
|
+
el.value = val;
|
|
45
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
46
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
47
|
+
}, normalizedValue.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getActionDescription(value, identifier) {
|
|
51
|
+
return `Set color "${value}" on ${identifier}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/DateInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for date-related input fields.
|
|
5
|
+
* Handles: date, datetime-local, month, week
|
|
6
|
+
* Sets value directly via JavaScript since keyboard input doesn't work reliably.
|
|
7
|
+
*
|
|
8
|
+
* Expected formats:
|
|
9
|
+
* - date: "YYYY-MM-DD"
|
|
10
|
+
* - datetime-local: "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS"
|
|
11
|
+
* - month: "YYYY-MM"
|
|
12
|
+
* - week: "YYYY-Www" (e.g., "2024-W01")
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
16
|
+
|
|
17
|
+
export class DateInputModel extends BaseInputModel {
|
|
18
|
+
static get inputTypes() {
|
|
19
|
+
return ['date', 'datetime-local', 'month', 'week'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set date value directly via JavaScript
|
|
24
|
+
* @param {string} value - Date in appropriate format for input type
|
|
25
|
+
* @param {object} options - (unused, for interface compatibility)
|
|
26
|
+
*/
|
|
27
|
+
async setValue(value, options = {}) {
|
|
28
|
+
const inputType = await this.element.evaluate(el => el.type);
|
|
29
|
+
|
|
30
|
+
// Validate format based on input type
|
|
31
|
+
this._validateFormat(value, inputType);
|
|
32
|
+
|
|
33
|
+
await this.element.evaluate((el, val) => {
|
|
34
|
+
el.value = val;
|
|
35
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
36
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
37
|
+
}, value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_validateFormat(value, inputType) {
|
|
41
|
+
const formats = {
|
|
42
|
+
'date': /^\d{4}-\d{2}-\d{2}$/,
|
|
43
|
+
'datetime-local': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/,
|
|
44
|
+
'month': /^\d{4}-\d{2}$/,
|
|
45
|
+
'week': /^\d{4}-W\d{2}$/,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const regex = formats[inputType];
|
|
49
|
+
if (regex && !regex.test(value)) {
|
|
50
|
+
console.warn(`DateInputModel: Value "${value}" may not be in correct format for ${inputType}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getMetadata() {
|
|
55
|
+
const base = await super.getMetadata();
|
|
56
|
+
const extra = await this.element.evaluate(el => ({
|
|
57
|
+
min: el.min || null,
|
|
58
|
+
max: el.max || null,
|
|
59
|
+
step: el.step || null,
|
|
60
|
+
}));
|
|
61
|
+
return { ...base, ...extra };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getActionDescription(value, identifier) {
|
|
65
|
+
return `Set date "${value}" on ${identifier}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/RadioGroupModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for radio button groups.
|
|
5
|
+
* Allows selecting one option from a group by name, value, or label text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
9
|
+
|
|
10
|
+
export class RadioGroupModel extends BaseInputModel {
|
|
11
|
+
/**
|
|
12
|
+
* Create a RadioGroupModel
|
|
13
|
+
* @param {string} groupName - The name attribute of the radio group
|
|
14
|
+
* @param {Page} page - Puppeteer page
|
|
15
|
+
*/
|
|
16
|
+
constructor(groupName, page) {
|
|
17
|
+
super(null, page);
|
|
18
|
+
this.groupName = groupName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get all options in this radio group
|
|
23
|
+
*/
|
|
24
|
+
async getOptions() {
|
|
25
|
+
return await this.page.evaluate((name) => {
|
|
26
|
+
const inputs = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
|
|
27
|
+
return Array.from(inputs).map(input => {
|
|
28
|
+
// Find label text
|
|
29
|
+
let label = null;
|
|
30
|
+
const parentLabel = input.closest('label');
|
|
31
|
+
if (parentLabel) {
|
|
32
|
+
label = parentLabel.textContent?.trim();
|
|
33
|
+
} else if (input.id) {
|
|
34
|
+
const labelFor = document.querySelector(`label[for="${input.id}"]`);
|
|
35
|
+
if (labelFor) {
|
|
36
|
+
label = labelFor.textContent?.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
label = label || input.getAttribute('aria-label') || input.value;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
value: input.value,
|
|
43
|
+
label,
|
|
44
|
+
checked: input.checked,
|
|
45
|
+
disabled: input.disabled
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}, this.groupName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get currently selected value
|
|
53
|
+
*/
|
|
54
|
+
async getValue() {
|
|
55
|
+
return await this.page.evaluate((name) => {
|
|
56
|
+
const checked = document.querySelector(`input[type="radio"][name="${name}"]:checked`);
|
|
57
|
+
return checked ? checked.value : null;
|
|
58
|
+
}, this.groupName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Select a radio option by value or label text
|
|
63
|
+
* @param {string} value - Value or label text to select
|
|
64
|
+
* @param {object} options - { by: 'value'|'text'|'auto' }
|
|
65
|
+
*/
|
|
66
|
+
async setValue(value, options = {}) {
|
|
67
|
+
const { by = 'auto' } = options;
|
|
68
|
+
|
|
69
|
+
const result = await this.page.evaluate((name, val, selectBy) => {
|
|
70
|
+
const inputs = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
|
|
71
|
+
|
|
72
|
+
for (const input of inputs) {
|
|
73
|
+
if (input.disabled) continue;
|
|
74
|
+
|
|
75
|
+
let match = false;
|
|
76
|
+
|
|
77
|
+
// Get label text
|
|
78
|
+
let label = null;
|
|
79
|
+
const parentLabel = input.closest('label');
|
|
80
|
+
if (parentLabel) {
|
|
81
|
+
label = parentLabel.textContent?.trim();
|
|
82
|
+
} else if (input.id) {
|
|
83
|
+
const labelFor = document.querySelector(`label[for="${input.id}"]`);
|
|
84
|
+
if (labelFor) {
|
|
85
|
+
label = labelFor.textContent?.trim();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
label = label || input.getAttribute('aria-label');
|
|
89
|
+
|
|
90
|
+
switch (selectBy) {
|
|
91
|
+
case 'value':
|
|
92
|
+
match = input.value === val;
|
|
93
|
+
break;
|
|
94
|
+
case 'text':
|
|
95
|
+
match = label && (label === val || label.toLowerCase().includes(val.toLowerCase()));
|
|
96
|
+
break;
|
|
97
|
+
case 'auto':
|
|
98
|
+
default:
|
|
99
|
+
// Try value first, then label
|
|
100
|
+
match = input.value === val ||
|
|
101
|
+
(label && (label === val || label.toLowerCase().includes(val.toLowerCase())));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (match) {
|
|
106
|
+
input.checked = true;
|
|
107
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
108
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
109
|
+
return { success: true, selectedValue: input.value, selectedLabel: label };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { success: false, error: `No matching option found for "${val}" in radio group "${name}"` };
|
|
114
|
+
}, this.groupName, value, by);
|
|
115
|
+
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
throw new Error(result.error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getActionDescription(value, identifier) {
|
|
124
|
+
return `Selected "${value}" in radio group "${this.groupName}"`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models/RangeInputModel.js
|
|
3
|
+
*
|
|
4
|
+
* Model for range/slider inputs.
|
|
5
|
+
* Sets value directly via JavaScript.
|
|
6
|
+
*
|
|
7
|
+
* Expected format: numeric value within min/max range
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseInputModel } from './BaseInputModel.js';
|
|
11
|
+
|
|
12
|
+
export class RangeInputModel extends BaseInputModel {
|
|
13
|
+
static get inputTypes() {
|
|
14
|
+
return ['range'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set range value directly via JavaScript
|
|
19
|
+
* @param {string|number} value - Numeric value
|
|
20
|
+
* @param {object} options - (unused, for interface compatibility)
|
|
21
|
+
*/
|
|
22
|
+
async setValue(value, options = {}) {
|
|
23
|
+
const numericValue = parseFloat(value);
|
|
24
|
+
|
|
25
|
+
if (isNaN(numericValue)) {
|
|
26
|
+
console.warn(`RangeInputModel: Value "${value}" is not a valid number`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get min/max and clamp value
|
|
30
|
+
const { min, max } = await this.element.evaluate(el => ({
|
|
31
|
+
min: parseFloat(el.min) || 0,
|
|
32
|
+
max: parseFloat(el.max) || 100,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const clampedValue = Math.max(min, Math.min(max, numericValue));
|
|
36
|
+
if (clampedValue !== numericValue) {
|
|
37
|
+
console.warn(`RangeInputModel: Value ${numericValue} clamped to ${clampedValue} (range: ${min}-${max})`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await this.element.evaluate((el, val) => {
|
|
41
|
+
el.value = val;
|
|
42
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
43
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
44
|
+
}, clampedValue.toString());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getMetadata() {
|
|
48
|
+
const base = await super.getMetadata();
|
|
49
|
+
const extra = await this.element.evaluate(el => ({
|
|
50
|
+
min: el.min || '0',
|
|
51
|
+
max: el.max || '100',
|
|
52
|
+
step: el.step || '1',
|
|
53
|
+
}));
|
|
54
|
+
return { ...base, ...extra };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getActionDescription(value, identifier) {
|
|
58
|
+
return `Set range value "${value}" on ${identifier}`;
|
|
59
|
+
}
|
|
60
|
+
}
|