mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
feat(web-test): highlight groups fix, recording auto-stop, fillField alias
- highlight(): exact match by name ignores size filter (supports Representation=None groups),
error message lists available elements by category
- startRecording(): { force: true } option to restart if already recording
- executeScript(): auto-stop recording on script error (prevents "Already recording")
- fillField(name, value): silent alias for fillFields({ name: value })
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -333,12 +333,12 @@ Clear filters. Without arguments clears all, with `{ field }` clears specific ba
|
||||
#### `screenshot()` → PNG Buffer
|
||||
#### `wait(seconds)` → form state
|
||||
#### `getPage()` → Playwright Page (raw, for advanced scripting)
|
||||
#### `startRecording(path, opts?)` / `stopRecording()` → MP4 video recording
|
||||
#### `startRecording(path, opts?)` / `stopRecording()` → MP4 video recording (`{ force: true }` to restart if already recording)
|
||||
#### `showCaption(text, opts?)` / `hideCaption()` → text overlay on page
|
||||
#### `showTitleSlide(text, opts?)` / `hideTitleSlide()` → full-screen title card (intro/outro)
|
||||
#### `isRecording()` → boolean
|
||||
#### `setHighlight(on)` / `isHighlightMode()` → auto-highlight mode for video
|
||||
#### `highlight(text)` / `unhighlight()` → manual element highlighting
|
||||
#### `highlight(text)` / `unhighlight()` → manual element highlighting (error lists available elements)
|
||||
#### `addNarration(videoPath, opts?)` → narrated MP4 with TTS voiceover
|
||||
#### `getCaptions()` → caption timestamps from last recording
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test browser v1.0 — Playwright browser management for 1C web client
|
||||
// web-test browser v1.2 — Playwright browser management for 1C web client
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Playwright browser management for 1C web client.
|
||||
@@ -1440,6 +1440,11 @@ export async function fillFields(fields) {
|
||||
return { filled: results, form: formData };
|
||||
}
|
||||
|
||||
/** Convenience alias: fill a single field. Same as fillFields({ name: value }). */
|
||||
export async function fillField(name, value) {
|
||||
return fillFields({ [name]: value });
|
||||
}
|
||||
|
||||
/** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists). */
|
||||
export async function clickElement(text, { dblclick, table, toggle } = {}) {
|
||||
ensureConnected();
|
||||
@@ -3526,7 +3531,13 @@ export function isRecording() {
|
||||
*/
|
||||
export async function startRecording(outputPath, opts = {}) {
|
||||
ensureConnected();
|
||||
if (recorder) throw new Error('Already recording. Call stopRecording() first.');
|
||||
if (recorder) {
|
||||
if (opts.force) {
|
||||
try { await stopRecording(); } catch {}
|
||||
} else {
|
||||
throw new Error('Already recording. Call stopRecording() first, or use { force: true }.');
|
||||
}
|
||||
}
|
||||
lastCaptions = [];
|
||||
lastRecordingDuration = null;
|
||||
|
||||
@@ -4063,8 +4074,8 @@ export async function highlight(text, opts = {}) {
|
||||
|
||||
// 2. Form groups/panels — checked BEFORE buttons/fields because group names
|
||||
// often collide with command bar buttons (e.g. "БизнесПроцессы" is both a
|
||||
// panel and a command bar element). Groups are large visual containers;
|
||||
// min-area filter (100x50) prevents matching small elements.
|
||||
// panel and a command bar element). Min-area filter (100x50) only for fuzzy
|
||||
// match — exact match by name works regardless of size (Representation=None).
|
||||
if (!elId) {
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum !== null) {
|
||||
@@ -4072,9 +4083,9 @@ export async function highlight(text, opts = {}) {
|
||||
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
||||
const target = ${JSON.stringify(normYo(text.toLowerCase()))};
|
||||
const p = 'form' + ${formNum} + '_';
|
||||
// Collect visible group containers — _container or _div elements (min 100x50 to skip command bars)
|
||||
// Collect ALL visible group containers — _container or _div elements
|
||||
const groups = [...document.querySelectorAll('[id^="' + p + '"][id$="_container"], [id^="' + p + '"][id$="_div"]')]
|
||||
.filter(el => el.offsetWidth >= 100 && el.offsetHeight >= 50);
|
||||
.filter(el => el.offsetWidth > 0);
|
||||
const items = groups.map(el => {
|
||||
const idName = el.id.replace(p, '').replace(/_(container|div)$/, '');
|
||||
// Try to find a visible title/label for this group
|
||||
@@ -4082,15 +4093,15 @@ export async function highlight(text, opts = {}) {
|
||||
|| document.getElementById(p + idName + '_title_text');
|
||||
const label = norm(titleEl?.innerText || '').toLowerCase();
|
||||
const name = norm(idName).toLowerCase();
|
||||
return { id: el.id, name, label };
|
||||
const big = el.offsetWidth >= 100 && el.offsetHeight >= 50;
|
||||
return { id: el.id, name, label, big };
|
||||
});
|
||||
// Fuzzy match: exact label → exact name → startsWith → includes
|
||||
// Skip includes() for short strings (< 4 chars) to avoid false positives
|
||||
// e.g. "Да" matching "Удаляемые"
|
||||
// Exact match: no size filter (supports Representation=None groups)
|
||||
let found = items.find(i => i.label === target);
|
||||
if (!found) found = items.find(i => i.name === target);
|
||||
if (!found) found = items.find(i => i.label.startsWith(target) || i.name.startsWith(target));
|
||||
if (!found && target.length >= 4) found = items.find(i => i.label.includes(target) || i.name.includes(target));
|
||||
// Fuzzy match: only large groups (min 100x50) to avoid matching command bars
|
||||
if (!found) found = items.filter(i => i.big).find(i => i.label.startsWith(target) || i.name.startsWith(target));
|
||||
if (!found && target.length >= 4) found = items.filter(i => i.big).find(i => i.label.includes(target) || i.name.includes(target));
|
||||
return found ? found.id : null;
|
||||
})()`);
|
||||
}
|
||||
@@ -4157,7 +4168,56 @@ export async function highlight(text, opts = {}) {
|
||||
})()`);
|
||||
}
|
||||
|
||||
if (!elId) throw new Error(`highlight: "${text}" not found`);
|
||||
if (!elId) {
|
||||
// Collect available elements to help the caller fix the name
|
||||
const available = await page.evaluate(`(() => {
|
||||
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
||||
const result = {};
|
||||
// Commands
|
||||
const cmds = [...document.querySelectorAll('[id^="cmd_"][id$="_txt"]')].filter(e => e.offsetWidth > 0).map(e => norm(e.innerText));
|
||||
if (cmds.length) result.commands = cmds;
|
||||
// Sections
|
||||
const secs = [...document.querySelectorAll('[id^="themesCell_theme_"]')].map(e => norm(e.innerText)).filter(Boolean);
|
||||
if (secs.length) result.sections = secs;
|
||||
// Form elements
|
||||
${(() => {
|
||||
// Detect form inline to avoid extra evaluate round-trip
|
||||
return `
|
||||
const forms = {};
|
||||
document.querySelectorAll('[id^="form"]').forEach(el => {
|
||||
const m = el.id.match(/^form(\\d+)_/);
|
||||
if (m) forms[m[1]] = (forms[m[1]] || 0) + 1;
|
||||
});
|
||||
let formNum = null, maxCount = 0;
|
||||
for (const [n, c] of Object.entries(forms)) {
|
||||
if (parseInt(n) > 0 && c > maxCount) { maxCount = c; formNum = n; }
|
||||
}
|
||||
if (formNum !== null) {
|
||||
const p = 'form' + formNum + '_';
|
||||
// Groups
|
||||
const groups = [...document.querySelectorAll('[id^="' + p + '"][id$="_container"], [id^="' + p + '"][id$="_div"]')]
|
||||
.filter(el => el.offsetWidth > 0)
|
||||
.map(el => {
|
||||
const idName = el.id.replace(p, '').replace(/_(container|div)$/, '');
|
||||
const titleEl = document.getElementById(p + idName + '#title_text') || document.getElementById(p + idName + '_title_text');
|
||||
return norm(titleEl?.innerText || '') || idName;
|
||||
}).filter(Boolean);
|
||||
if (groups.length) result.groups = groups;
|
||||
// Buttons/links
|
||||
const btns = [...document.querySelectorAll('[id^="' + p + '"].btnText, [id^="' + p + '"] .btnText, [id^="' + p + '"].hplnk')]
|
||||
.filter(el => el.offsetWidth > 0).map(el => norm(el.innerText)).filter(Boolean);
|
||||
if (btns.length) result.buttons = [...new Set(btns)];
|
||||
}`;
|
||||
})()}
|
||||
return result;
|
||||
})()`);
|
||||
const parts = [];
|
||||
for (const [cat, items] of Object.entries(available)) {
|
||||
parts.push(` ${cat}: ${items.join(', ')}`);
|
||||
}
|
||||
const hint = parts.length ? `\nAvailable:\n${parts.join('\n')}` : '';
|
||||
throw new Error(`highlight: "${text}" not found${hint}`);
|
||||
}
|
||||
|
||||
// Overlay div + rAF tracking loop (not clipped by overflow:hidden, follows layout shifts)
|
||||
await page.evaluate(({ elId, color, padding }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// web-test run v1.1 — CLI runner for 1C web client automation
|
||||
// web-test run v1.2 — CLI runner for 1C web client automation
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* CLI runner for 1C web client automation.
|
||||
@@ -131,7 +131,7 @@ async function executeScript(code, { noRecord } = {}) {
|
||||
// Wrap action functions to auto-detect 1C errors (modal, balloon)
|
||||
// and stop execution immediately with diagnostic info
|
||||
const ACTION_FNS = [
|
||||
'clickElement', 'fillFields', 'selectValue', 'fillTableRow',
|
||||
'clickElement', 'fillFields', 'fillField', 'selectValue', 'fillTableRow',
|
||||
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
|
||||
'closeForm', 'filterList', 'unfilterList'
|
||||
];
|
||||
@@ -162,6 +162,11 @@ async function executeScript(code, { noRecord } = {}) {
|
||||
console.log = origLog;
|
||||
console.error = origErr;
|
||||
|
||||
// Auto-stop recording if active (prevents "Already recording" on next exec)
|
||||
if (browser.isRecording()) {
|
||||
try { await browser.stopRecording(); } catch {}
|
||||
}
|
||||
|
||||
// Error screenshot
|
||||
let shotFile;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user