Setting the file. One moment.
Visual Diff · N8n:testmax Assistant UI · n8n-io/n8n · Skills Docs
ContentsBack to the top of the page scripts/visual_diff.mjs
scripts/ visual_diff.mjs
JavaScript · 96 lines · 4 KB
13 import { join, resolve } from 'node:path' ;
14
15 import { loadPng } from '../../testmax-n8n-ai/scripts/n8n_browser.mjs' ;
16
17 const PIXEL_THRESHOLD = 32 ;
18 const RATIO_THRESHOLD = 0.0005 ;
19 const PNG = loadPng ();
20 const runDir = resolve (process.argv[ 2 ] ?? '.' );
21 const plan = JSON . parse ( readFileSync ( join (runDir, 'plan' , 'scenarios.json' ), 'utf8' ));
22
23 const read = ( file ) => PNG .sync. read ( readFileSync (file));
24 const names = ( dir ) =>
25 existsSync (dir)
26 ? readdirSync (dir)
27 . filter (( f ) => f. endsWith ( '.png' ))
28 . map (( f ) => f. slice ( 0 , - 4 ))
29 : [];
30
31 function compare ( branch , master ) {
32 const width = Math. max (branch.width, master.width);
33 const height = Math. max (branch.height, master.height);
34 const diff = new PNG ({ width, height });
35 let changed = 0 ;
36 for ( let y = 0 ; y < height; y ++ ) {
37 for ( let x = 0 ; x < width; x ++ ) {
38 const o = (y * width + x) * 4 ;
39 const inB = x < branch.width && y < branch.height;
40 const inM = x < master.width && y < master.height;
41 const b = inB ? (y * branch.width + x) * 4 : - 1 ;
42 const m = inM ? (y * master.width + x) * 4 : - 1 ;
43 let differs = ! inB || ! inM;
44 if ( ! differs) {
45 for ( let c = 0 ; c < 4 && ! differs; c ++ )
46 differs = Math. abs (branch.data[b + c] - master.data[m + c]) > PIXEL_THRESHOLD ;
47 }
48 if (differs) {
49 changed ++ ;
50 diff.data. set ([ 224 , 17 , 95 , 255 ], o);
51 } else {
52 // The unchanged page shows as a faint gray copy of the branch screen.
53 const gray =
54 0.299 * branch.data[b] + 0.587 * branch.data[b + 1 ] + 0.114 * branch.data[b + 2 ];
55 const v = Math. round ( 255 - ( 255 - gray) * 0.25 );
56 diff.data. set ([v, v, v, 255 ], o);
57 }
58 }
59 }
60 return { diff, changed, ratio: changed / (width * height) };
61 }
62
63 const results = [];
64 for ( const s of plan.scenarios. filter (( x ) => x.kind === 'visual' )) {
65 const branchDir = join (runDir, 'runs' , s.id, 'captures' );
66 const masterDir = join (runDir, 'runs' , `${ s . id }--master` , 'captures' );
67 const all = [ ...new Set ([ ... names (branchDir), ... names (masterDir)])]. sort ();
68 for ( const name of all) {
69 const entry = { scenario: s.id, name };
70 const hasB = existsSync ( join (branchDir, `${ name }.png` ));
71 const hasM = existsSync ( join (masterDir, `${ name }.png` ));
72 if ( ! hasB || ! hasM) {
73 results. push ({ ... entry, status: hasB ? 'branch-only' : 'master-only' });
74 continue ;
75 }
76 const branch = read ( join (branchDir, `${ name }.png` ));
77 const master = read ( join (masterDir, `${ name }.png` ));
78 const { diff , changed , ratio } = compare (branch, master);
79 const out = join (runDir, 'visual' , s.id);
80 mkdirSync (out, { recursive: true });
81 writeFileSync ( join (out, `${ name }.diff.png` ), PNG .sync. write (diff));
82 const resized = branch.width !== master.width || branch.height !== master.height;
83 results. push ({
84 ... entry,
85 status: resized || ratio > RATIO_THRESHOLD ? 'changed' : 'unchanged' ,
86 changedPixels: changed,
87 ratio,
88 branchSize: [branch.width, branch.height],
89 masterSize: [master.width, master.height],
90 diff: `visual/${ s . id }/${ name }.diff.png` ,
91 });
92 }
93 }
94 writeFileSync ( join (runDir, 'visual.json' ), JSON . stringify (results, null , 2 ));
95 const changed = results. filter (( r ) => r.status !== 'unchanged' ). length ;
96 console. log ( `${ results . length } captures compared, ${ changed } with a difference` );