Loading...
Loading...
Explore any Hexagone Web space via Playwright headless browser, capture screenshots, and produce a PO-oriented Markdown document.
npx skill4agent add dedalus-erp-pas/foundation-skills hexagone-web-feature-extractornpm install playwrighthttps://ws004202.dedalus.lan:8065/hexagone-01/vue/login| Parameter | Default | Description |
|---|---|---|
| Viewport | | Browser viewport size |
| Sidebar click X coordinate | | Horizontal pixel position for sidebar icon clicks (collapsed mode) |
| Sidebar max left boundary | | Max |
| Header height offset | | Min |
| Login wait timeout | | Max time to poll for successful login |
| Page load wait timeout | | Max time to poll for page load after navigation |
| Screenshots directory | | Where screenshots are saved (relative to working directory) |
1. SETUP → Install Playwright, launch headless Chromium
2. CONNECTION → Log in to Hexagone Web
3. NAVIGATION → Navigate to the target space
4. DISCOVERY → Expand sidebar, list all menu pages
5. EXPLORATION → Visit each page, capture screenshots + metadata
6. GENERATION → Produce the Markdown document with embedded screenshotspage.screenshot()npm install playwright
npx playwright install chromiumconst { chromium } = require('playwright');
const browser = await chromium.launch({
headless: true,
args: ['--ignore-certificate-errors', '--no-sandbox']
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true // Handles self-signed certs automatically
});
const page = await context.newPage();ignoreHTTPSErrors: trueawait page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(3000); // Wait for Vue.js to mountapvhnpage.evaluate()await page.evaluate(({ username, password }) => {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
const userInput = document.querySelector('input[type="text"]');
if (userInput) {
nativeSetter.call(userInput, username);
userInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const pwdInput = document.querySelector('input[type="password"]');
if (pwdInput) {
nativeSetter.call(pwdInput, password);
pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const loginBtn = Array.from(document.querySelectorAll('button'))
.find(b => /connect/i.test(b.textContent));
if (loginBtn) loginBtn.click();
}, { username: USERNAME, password: PASSWORD });/loginfor (let i = 0; i < 15; i++) {
await sleep(2000);
if (!page.url().includes('/login')) break;
}page.screenshot()page.mouse.click()el.click()page.evaluate()el.click()clickdivbg:orange-dark<i class="hexa-icons">changer_espaces</i><span>// Find the space selector coordinates
const selectorRect = await page.evaluate(() => {
for (const el of document.querySelectorAll('div, span')) {
const cls = typeof el.className === 'string' ? el.className : '';
if (cls.includes('bg:orange-dark') && !cls.includes('uppercase') && !cls.includes('hover:')) {
const rect = el.getBoundingClientRect();
if (rect.top > 30 && rect.top < 70 && rect.height > 15) {
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
}
return null;
});
// Click with REAL mouse events (mandatory for Vue.js)
await page.mouse.click(selectorRect.x, selectorRect.y);
await sleep(3000);<div>px:1 py:3/4 hover:bg:orange-dark cursor:pointer// Find the target space element
const target = await page.evaluate((spaceName) => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode;
if (el.textContent.trim() === spaceName) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top > 30) {
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
}
return null;
}, TARGET_SPACE);
// Click with mouse (not el.click())
await page.mouse.click(target.x, target.y);patient-portalfor (let i = 0; i < 12; i++) {
await sleep(2000);
if (!page.url().includes('patient-portal')) break;
}
await sleep(3000); // Extra wait for Vue.js renderingawait page.mouse.click(34, 50); // Hamburger icon position
await sleep(2000);cursor:pointer<i class="hexa-icons">const menuItems = await page.evaluate((excludeLabels) => {
const items = [];
const seen = new Set();
const allEls = document.querySelectorAll('[class*="cursor:pointer"], a');
for (const el of allEls) {
const rect = el.getBoundingClientRect();
if (rect.left < 280 && rect.top > 55 && rect.height > 15 && rect.height < 60) {
let text = el.textContent.trim();
// Strip icon prefix text
const icon = el.querySelector('i');
if (icon) text = text.replace(icon.textContent.trim(), '').trim();
if (!text || text.length <= 1 || text.length >= 60 || seen.has(text)) continue;
if (excludeLabels.includes(text)) continue;
// Skip section headers (all-caps short text like "ACHATS")
if (/^[A-Z ]+$/.test(text) && text.length < 15) continue;
seen.add(text);
items.push({
label: text,
y: Math.round(rect.top + rect.height / 2),
x: Math.round(rect.left + rect.width / 2)
});
}
}
return items;
}, ['Trier par Importance', 'Trier par Emetteur']);<div class="sidebar--section min-w:sidebar bg:teal-darker">
<div class="py:1 transition cursor:pointer w:inherit hover:bg:teal-dark">
<div class="flex items:center whitespace:no-wrap">
<i class="hexa-icons text:3/2" aria-label="Fournisseurs">fournisseurs</i>
<span class="sidebar--label">Fournisseurs</span>
</div>
</div>
</div>a.hexa<div>cursor:pointer<a>Trier par ImportanceTrier par EmetteurACHATSawait page.mouse.click(34, 50); // Toggle hamburger
await sleep(1000);For each menu item:
1. Expand sidebar: page.mouse.click(34, 50), wait 1.5s
2. Re-discover the item position via page.evaluate() (label matching)
3. Click the item: page.mouse.click(freshPos.x, freshPos.y)
4. Wait for page load (networkidle + 1.5s extra)
5. Take screenshot: page.screenshot({ path: ... })
6. Extract metadata via page.evaluate()
7. If tabs found, explore them (see 5.2)
8. Build feature description with PO-oriented textpage.mouse.click()const tabCoords = await page.evaluate((label) => {
for (const tab of document.querySelectorAll('[role="tab"], .v-tab')) {
if (tab.textContent.trim() === label) {
const rect = tab.getBoundingClientRect();
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
return null;
}, tabLabel);
if (tabCoords) {
await page.mouse.click(tabCoords.x, tabCoords.y);
await sleep(2500);
await page.screenshot({ path: tabScreenshotPath });
}[class*="tab"][role="tab"].v-tabawait page.screenshot({
path: path.join(SCREENSHOT_DIR, `${index}-${sanitize(pageName)}.png`),
fullPage: false // Capture viewport only (1920x1080)
});features.jsonfeatures.json{
"space": "Name of the explored space",
"features": [
{
"title": "Feature name (string, required)",
"description": "PO-oriented functional description (string, required)",
"capabilities": ["Capability 1", "Capability 2"],
"businessValue": "Business value description (string)",
"screenshots": [
{ "file": "filename.png", "caption": "Screenshot description" }
]
}
]
}spacefeaturestitledescriptioncapabilitiesscreenshotsfilecaptionscripts/generate-md.jsnode scripts/generate-md.js --input features.json --output /path/to/output.md --screenshots /path/to/screenshots--inputnpm install# Title (space name)
> Subtitle with date
## Table of contents (linked)
For each feature:
## N. Feature title
- Functional description
- Screenshot(s) as 
### Key capabilities (numbered list)
### Business value
---grep -c '^## [0-9]' output.md # Should match the number of featureshttps://ws004202.dedalus.lan:8065/hexagone-01/vue/loginapvhnpage.mouse.click()el.click()page.evaluate()page.mouse.click()[class*="cursor:pointer"]a.hexa<div><a><i class="hexa-icons">tdbTableau de bordTableau de bord[role="tab"].v-tab[class*="tab"]| Problem | Cause | Solution |
|---|---|---|
| Space dropdown does not open | Used | Always use |
| All page screenshots are identical | Sidebar collapsed, clicks miss sidebar items | Re-expand sidebar + re-discover coordinates before each click |
| Sidebar items not found | Used | Use |
| Too many "tabs" detected | Broad selector | Use |
| "Trier par Importance" in page list | Sort button mistaken for page | Add to exclude list: |
| Fields not detected by Vue.js | Direct value injection without events | Use |
| Login button click selects wrong button | Multiple buttons on page | Use text-content matching: |
| Page content not loaded after click | Slow server or heavy page | Use |
| SSL certificate error | Self-signed cert on Hexagone Web server | |
| Malformed features.json | Check required fields: |