Setting the file. One moment.
Find Interactions · Wix Harmony Editor · wix/skills · Skills Docs
ContentsBack to the top of the page scripts/copy/ find-interactions.mjs
JavaScript · 76 lines · 7 KB
14 // .swiper-button-next, [class*=next]), and links to in-page ids (#id) whose target is hidden.
15 // Per control the output has: kind, label, target pane id, whether the pane is hidden by default,
16 // and for each state the pane's images, links and text, plus a screenshot of the region.
17 // Map each kind to a Wix component (COPY-SITE item 17): tabs -> Tabs, accordion -> Accordion,
18 // carousel -> Slideshow or the Stores slider gallery, hover/dropdown -> Menu, dialog -> Popup.
19 //
20 // Clicks are dispatched in the page (el.click()), so a consent dialog or a sticky bar covering the
21 // control does not block them. The page is never submitted anywhere: forms are not touched.
22 import { chromium } from 'playwright' ;
23 import fs from 'node:fs' ;
24
25 const args = process.argv. slice ( 2 );
26 const opt = ( n , d ) => { const i = args. indexOf ( `--${ n }` ); return i >= 0 ? args[i + 1 ] : d; };
27 const [ url , out ] = args. filter (( a , i ) => ! a. startsWith ( '--' ) && ! (i > 0 && args[i - 1 ]. startsWith ( '--' )));
28 if ( ! url || ! out) { console. error ( 'usage: find-interactions.mjs <url> <out.json> [--width 1280] [--shots dir]' ); process. exit ( 2 ); }
29 const width = Number ( opt ( 'width' , 1280 )); const shots = opt ( 'shots' , null );
30 if (shots) fs. mkdirSync (shots, { recursive: true });
31
32 const browser = await chromium. launch ();
33 const page = await browser. newPage ({ viewport: { width, height: 900 } });
34 await page. goto (url, { waitUntil: 'networkidle' , timeout: 90000 }). catch (() => {});
35 await page. waitForTimeout ( 2500 );
36
37 const SEL = '[role=tab], [data-toggle=tab], [data-toggle=pill], [data-bs-toggle=tab], [data-toggle=collapse], [data-bs-toggle=collapse], summary, [aria-expanded][aria-controls], .slick-next, .swiper-button-next, button[class*=next], a[href^="#"]:not([href="#"])' ;
38 const controls = await page. evaluate (( SEL ) => {
39 const vis = ( e ) => { const r = e. getBoundingClientRect (); const cs = getComputedStyle (e); return r.width > 4 && r.height > 4 && cs.visibility !== 'hidden' && cs.display !== 'none' ; };
40 const paneOf = ( e ) => { if (e.tagName === 'SUMMARY' && e.parentElement && e.parentElement.tagName === 'DETAILS' ) return e.parentElement; const t = idOf (e); return t ? document. getElementById (t) : null ; };
41 const idOf = ( e ) => { const h = e. getAttribute ( 'href' ) || e. getAttribute ( 'data-target' ) || e. getAttribute ( 'data-bs-target' ) || '' ; const m = h. match ( / ^ #( . + ) $ / ); return e. getAttribute ( 'aria-controls' ) || (m ? m[ 1 ] : null ); };
42 let n = 0 ; const seen = new Set (); const out = [];
43 for ( const e of document. querySelectorAll ( SEL )) {
44 if ( ! vis (e)) continue ; const target = idOf (e); const pane = paneOf (e);
45 if ( / ^ #/ . test (e. getAttribute ( 'href' ) || '' ) && ! pane) continue ; // an anchor to nowhere
46 const key = (target || (pane && pane.tagName === 'DETAILS' ? 'details' : '' )) + '|' + (e.innerText || '' ). trim (). slice ( 0 , 40 ) + '|' + e.tagName; if (seen. has (key)) continue ; seen. add (key);
47 e. setAttribute ( 'data-hz-ctl' , String (n)); const r = e. getBoundingClientRect ();
48 const pane2 = paneOf (e); if (pane2) pane2. setAttribute ( 'data-hz-pane' , String (n));
49 const isDetails = !! pane2 && pane2.tagName === 'DETAILS' ;
50 const kind = e. matches ( '[role=tab],[data-toggle=tab],[data-toggle=pill],[data-bs-toggle=tab]' ) ? 'tab' : e. matches ( '[data-toggle=collapse],[data-bs-toggle=collapse],summary,[aria-expanded]' ) ? 'accordion' : e. matches ( '.slick-next,.swiper-button-next,button[class*=next]' ) ? 'carousel' : 'anchor-target' ;
51 out. push ({ n: n ++ , kind, label: (e.innerText || e. getAttribute ( 'aria-label' ) || '' ). trim (). replace ( / \s + / g , ' ' ). slice ( 0 , 60 ), target, hiddenByDefault: pane ? (pane.tagName === 'DETAILS' ? ! pane.open : (pane. getBoundingClientRect ().height === 0 || getComputedStyle (pane).display === 'none' )) : null , hasPane: !! pane, native: isDetails ? 'details' : null , box: [Math. round (r.left), Math. round (r.top + scrollY), Math. round (r.width), Math. round (r.height)] });
52 }
53 return out;
54 }, SEL );
55
56 const snap = ( n ) => page. evaluate (( n ) => {
57 const el = document. querySelector ( `[data-hz-pane="${ n }"]` ); if ( ! el) return null ;
58 return { open: el.tagName === 'DETAILS' ? el.open : undefined , images: [ ... el. querySelectorAll ( 'img' )]. map (( i ) => (i.currentSrc || i.src || '' ). replace ( / ^ https ? : \/\/ [ ^ /] + / , '' ). slice ( 0 , 200 )), links: [ ... el. querySelectorAll ( 'a[href]' )]. map (( a ) => a. getAttribute ( 'href' )). slice ( 0 , 40 ), text: el.innerText. trim (). replace ( / \s + / g , ' ' ). slice ( 0 , 400 ), shown: el.tagName === 'DETAILS' ? el.open : (el. getBoundingClientRect ().height > 0 && getComputedStyle (el).display !== 'none' ), h: Math. round (el. getBoundingClientRect ().height) };
59 }, n);
60
61 const result = [];
62 for ( const c of controls) {
63 const entry = { ... c, states: [] };
64 if (c.kind === 'carousel' ) { result. push (entry); continue ; } // arrows: the copy uses a slider component; the items are in the page already
65 entry.states. push ({ state: 'before' , ... ( await snap (c.n)) });
66 await page. evaluate (( n ) => document. querySelector ( `[data-hz-ctl="${ n }"]` ). click (), c.n). catch (() => {});
67 await page. waitForTimeout ( 900 );
68 const after = await snap (c.n); entry.states. push ({ state: 'after click' , ... after });
69 if (shots && c.hasPane) { const f = `${ shots }/ctl-${ c . n }-${ c . kind }.png` ; const l = page. locator ( `[data-hz-pane="${ c . n }"]` ); await l. screenshot ({ path: f }). catch (() => {}); entry.shot = f; }
70 result. push (entry);
71 }
72 // a toggled control may be a pair (open and close): leave nothing assumed, record what was clicked
73 fs. writeFileSync (out, JSON . stringify ({ url, width, controls: result }, null , 1 ));
74 const byKind = result. reduce (( m , c ) => ((m[c.kind] = (m[c.kind] || 0 ) + 1 ), m), {});
75 console. log ( JSON . stringify ({ controls: result. length , byKind, hiddenPanes: result. filter (( c ) => c.hiddenByDefault). map (( c ) => `${ c . kind }: ${ c . label } -> ${ c . native === 'details' ? '<details>' : '#' + c . target }` ) }, null , 1 ));
76 await browser. close ();