@@ -5,11 +5,14 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
55import { tmpdir } from 'node:os'
66import { dirname , resolve } from 'node:path'
77import { fileURLToPath } from 'node:url'
8+ import { createElement } from 'react'
89import { type BrowserContext , chromium , expect , type Frame , type Page } from '@playwright/test'
910import { pageSnapshotSchema } from '@sim/browser-extension-protocol'
11+ import { SecretReveal } from '@sim/emcn'
1012import { createLogger } from '@sim/logger'
1113import { getErrorMessage } from '@sim/utils/errors'
1214import { generateId } from '@sim/utils/id'
15+ import { renderToStaticMarkup } from 'react-dom/server'
1316
1417const root = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' )
1518const logger = createLogger ( 'BrowserExtensionE2E' )
@@ -18,6 +21,9 @@ if (!reportPath) throw new Error('Set EXTENSION_REPORT_PATH to the output JSON r
1821const output = resolve ( root , reportPath )
1922const checks : { name : string ; status : 'passed' | 'failed' ; durationMs : number ; error ?: string } [ ] =
2023 [ ]
24+ const secretReveal = renderToStaticMarkup (
25+ createElement ( SecretReveal , { value : 'SECRET_REVEAL_SENTINEL' } )
26+ )
2127const fixture = Bun . serve ( {
2228 hostname : '127.0.0.1' ,
2329 port : 0 ,
@@ -55,6 +61,7 @@ const fixture = Bun.serve({
5561 <input type="password" value="PASSWORD_SENTINEL"/><textarea>TEXTAREA_SENTINEL</textarea>
5662 <div contenteditable="true" id="editable">EDITABLE_SENTINEL</div><div hidden>HIDDEN_SENTINEL</div>
5763 <div style="display:none">CSS_HIDDEN_SENTINEL</div><div aria-hidden="true">ARIA_SENTINEL</div>
64+ <section id="secret-reveal"><p>Credential instructions</p>${ secretReveal } <code>visible-code-control</code></section>
5865 <script>const secret='SCRIPT_SENTINEL'</script>
5966 </body></html>` ,
6067 { headers : { 'content-type' : 'text/html' } }
@@ -236,9 +243,11 @@ try {
236243 'A typed sim/ link opens the Sim link resolver on the configured origin' ,
237244 async ( ) => {
238245 const tab = await page . context ( ) . newPage ( )
239- for ( const path of [ 'handbook' , 'jira/123?view=board' ] ) {
240- await tab . goto ( `http://sim/${ path } ` )
241- assert ( tab . url ( ) === `${ origin } /sim/${ path } ` , `sim/${ path } reached ${ tab . url ( ) } ` )
246+ for ( const scheme of [ 'http' , 'https' ] ) {
247+ for ( const path of [ 'handbook' , 'jira/123?view=board' , 'jira/a%2Fb?view=board%20view' ] ) {
248+ await tab . goto ( `${ scheme } ://sim/${ path } ` )
249+ assert ( tab . url ( ) === `${ origin } /sim/${ path } ` , `sim/${ path } reached ${ tab . url ( ) } ` )
250+ }
242251 }
243252 await tab . close ( )
244253 }
@@ -315,6 +324,7 @@ try {
315324 await check ( 'Page extraction excludes secrets and strips URL query and fragment' , async ( ) => {
316325 const [ snapshot ] = await snapshots ( )
317326 assert ( snapshot . text . includes ( 'visible-reference-482' ) , 'Visible page content missing' )
327+ assert ( snapshot . text . includes ( 'visible-code-control' ) , 'Ordinary code was excluded' )
318328 for ( const sentinel of [
319329 'PASSWORD_SENTINEL' ,
320330 'TEXTAREA_SENTINEL' ,
@@ -323,6 +333,7 @@ try {
323333 'CSS_HIDDEN_SENTINEL' ,
324334 'ARIA_SENTINEL' ,
325335 'SCRIPT_SENTINEL' ,
336+ 'SECRET_REVEAL_SENTINEL' ,
326337 ] ) {
327338 assert ( ! snapshot . text . includes ( sentinel ) , `Captured ${ sentinel } ` )
328339 }
@@ -715,6 +726,45 @@ try {
715726 assert ( ( await snapshots ( ) ) . length === before , 'An excluded Sim path was captured' )
716727 }
717728 } )
729+ await check (
730+ 'Revealed credentials stay excluded from exact and surrounding selections' ,
731+ async ( ) => {
732+ await page . goto ( `${ origin } /workspace/example` )
733+ await page . bringToFront ( )
734+ const before = ( await snapshots ( ) ) . length
735+ await capture ( )
736+ await expect . poll ( async ( ) => ( await snapshots ( ) ) . length ) . toBe ( before + 1 )
737+ await expect ( panel . getByRole ( 'status' ) ) . toHaveText ( '' )
738+ await page
739+ . locator ( '#secret-reveal code' )
740+ . first ( )
741+ . evaluate ( ( element ) => {
742+ const range = document . createRange ( )
743+ range . selectNodeContents ( element )
744+ window . getSelection ( ) ?. removeAllRanges ( )
745+ window . getSelection ( ) ?. addRange ( range )
746+ } )
747+ await capture ( 'Selection' )
748+ await expect ( panel . getByRole ( 'status' ) ) . toContainText ( 'could not be read' )
749+ assert ( ( await snapshots ( ) ) . length === before + 1 , 'A revealed credential was attached' )
750+ await page . locator ( '#secret-reveal' ) . evaluate ( ( element ) => {
751+ const range = document . createRange ( )
752+ range . selectNodeContents ( element )
753+ window . getSelection ( ) ?. removeAllRanges ( )
754+ window . getSelection ( ) ?. addRange ( range )
755+ } )
756+ await capture ( 'Selection' )
757+ await expect . poll ( async ( ) => ( await snapshots ( ) ) . length ) . toBe ( before + 2 )
758+ const snapshot = ( await snapshots ( ) ) . at ( - 1 )
759+ assert ( snapshot , 'Missing surrounding selection snapshot' )
760+ assert ( snapshot . text . includes ( 'Credential instructions' ) , 'Public selection text is missing' )
761+ assert ( snapshot . text . includes ( 'visible-code-control' ) , 'Ordinary selected code was excluded' )
762+ assert (
763+ ! snapshot . text . includes ( 'SECRET_REVEAL_SENTINEL' ) ,
764+ 'A surrounding selection leaked the credential'
765+ )
766+ }
767+ )
718768 await check (
719769 'Site access stays modal without violating the packaged security policy' ,
720770 async ( ) => {
0 commit comments