/** * Promises that are properties of the SOURCE and cannot be observed at runtime. * * Each is codebase-wide: that EVERY image carries the never-capture class, that * NO module imports the recorder, that the un-delayable loader stays small. No * single moment in a running page can check any of those, so they are checked * against the files — a lint rule that happens to live in the test suite. * * The bundle one matters more here than anywhere else in this product: the * plugin's whole promise is that it is a thin connector that leaves the store * fast, and it ships through wordpress.org to every installed site. * * Anything a running page CAN show is asserted there instead — the recorder's * config lives in replay.test.ts, against the object handed to init(). */ import { describe, it, expect } from 'vitest'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; const SRC = resolve( __dirname ); const WIDGET_ROOT = resolve( __dirname, '..' ); const PLUGIN_ROOT = resolve( __dirname, '../..' ); /** * Source with its comments removed. * * Every assertion below is about CODE. Without this the checks match their own * documentation, which proves nothing. */ function code( source: string ): string { return source.replace( /\/\*[\s\S]*?\*\//g, '' ).replace( /(^|[^:])\/\/.*$/gm, '$1' ); } /** Every .ts/.tsx source file under src, tests excluded. */ function sourceFiles(): string[] { const found: string[] = []; const walk = ( dir: string ) => { for ( const entry of readdirSync( dir ) ) { const full = join( dir, entry ); if ( statSync( full ).isDirectory() ) { walk( full ); continue; } if ( ! /\.tsx?$/.test( entry ) || entry.includes( '.test.' ) ) continue; found.push( full ); } }; walk( SRC ); return found; } describe( 'replay masking', () => { it( 'marks every image the widget renders as never-capture', () => { const offenders: string[] = []; for ( const file of sourceFiles() ) { if ( ! file.endsWith( '.tsx' ) ) continue; const source = code( readFileSync( file, 'utf8' ) ); for ( const img of source.match( //g ) ?? [] ) { if ( ! img.includes( 'ph-no-capture' ) ) { offenders.push( `${ file.replace( WIDGET_ROOT, '' ) }: ${ img.slice( 0, 60 ).replace( /\s+/g, ' ' ) }…` ); } } } // Every image here shows the shopper's own photo or the try-on made from // it. A new one without the class is a body in a replay. expect( offenders, 'add ph-no-capture to these images' ).toEqual( [] ); } ); } ); // The recorder's own configuration — maskAllInputs, blockSelector, api_host — is // asserted in replay.test.ts against the object actually handed to init(), not // against this file's source. Grepping source text for `maskAllInputs: true` // proved only that the source is the source: it would fail on a rename that // changed nothing and stay silent on a config built and never passed. Both were // mutation-checked before those assertions were removed here — flipping // maskAllInputs and hardcoding a posthog.com URL each fail replay.test.ts. describe( 'the storefront bundle budget', () => { it( 'never imports a recorder SDK into the bundle', () => { // This widget builds as a single IIFE — no code splitting — so an import // here would put the whole recorder on every product page of every // installed store, whether or not a shopper opens the modal. const offenders = sourceFiles().filter( ( file ) => { const source = code( readFileSync( file, 'utf8' ) ); return /from\s+['"]posthog-js['"]/.test( source ) || /require\(['"]posthog-js['"]\)/.test( source ); } ); expect( offenders, 'the recorder is loaded at runtime, never bundled' ).toEqual( [] ); } ); it( 'declares no analytics dependency', () => { const pkg = JSON.parse( readFileSync( join( WIDGET_ROOT, 'package.json' ), 'utf8' ) ); const deps = Object.keys( pkg.dependencies ?? {} ); expect( deps ).not.toContain( 'posthog-js' ); // React and react-dom are the only runtime dependencies, which is what // keeps this budget answerable at a glance. expect( deps.sort() ).toEqual( [ 'react', 'react-dom' ] ); } ); it( 'keeps the un-delayable loader small — it runs on every product page', () => { const bootstrap = readFileSync( join( PLUGIN_ROOT, 'assets/bootstrap.js' ), 'utf8' ); // This file is excluded from every optimizer and loads unconditionally. // The ceiling is generous but finite: it exists so "just add it to the // bootstrap" is a decision someone has to make on purpose. expect( bootstrap.length ).toBeLessThan( 40_000 ); } ); } ); describe( 'the single product-analytics path', () => { it( 'never posts a product-analytics event except through the bootstrap', () => { // events.ts keeps a direct-POST fallback for ATTRIBUTION, which the // merchant's own opt-in legitimately covers. Product analytics must not // have one: only the bootstrap can read the shopper's CMP, so a fallback // would be a way to emit without ever consulting it. const analytics = code( readFileSync( join( SRC, 'analytics.ts' ), 'utf8' ) ); expect( analytics ).not.toContain( 'fetch(' ); expect( analytics ).toContain( 'aisthetixTrackProductEvent' ); } ); it( 'asks the bootstrap whether replay may run, rather than deciding itself', () => { const analytics = code( readFileSync( join( SRC, 'analytics.ts' ), 'utf8' ) ); expect( analytics ).toContain( 'aisthetixProductAnalyticsAllowed' ); // No CMP reading in the widget: there is one implementation of that // decision and it is not here. expect( analytics ).not.toContain( 'Cookiebot' ); expect( analytics ).not.toContain( 'cmplz' ); } ); } );