@@ -208,6 +208,7 @@ test.describe('pre-request features tests', () => {
208208 } ) ,
209209 } ;
210210 } ) ;
211+
211212 test ( 'run test cases' , async ( { page } ) => {
212213 for ( const tc of testCases ) {
213214 console . log ( `Running test case: ${ tc . name } ` ) ;
@@ -230,6 +231,7 @@ test.describe('pre-request features tests', () => {
230231 tc . customVerify ( bodyJson ) ;
231232 }
232233 } ) ;
234+
233235 test ( 'send request with content type' , async ( { page } ) => {
234236 await page . getByTestId ( 'settings-button' ) . click ( ) ;
235237 await page . getByTestId ( 'dataFolders' ) . click ( ) ;
@@ -660,14 +662,17 @@ test.describe('unhappy paths', () => {
660662 await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
661663
662664 // verify
663- await expect . soft ( page . getByTestId ( 'response-pane' ) ) . toContainText ( 'my custom error' ) ;
665+ await expect
666+ . soft ( page . getByTestId ( 'response-pane' ) )
667+ . toContainText ( `my custom error` ) ;
664668
665669 await page . getByRole ( 'tab' , { name : 'Scripts' } ) . click ( ) ;
666670 await page . getByTestId ( 'CodeEditor' ) . getByRole ( 'textbox' ) . press ( 'ControlOrMeta+a' ) ;
667671 await page . keyboard . press ( 'Backspace' ) ;
668672 await editor . fill ( `insomnia.INVALID_FIELD.set('', '')` ) ;
669673
670- await page . getByRole ( 'tab' , { name : 'Body' } ) . click ( ) ;
674+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
675+ await page . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 150 ) ) ) ;
671676
672677 // send
673678 await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
@@ -678,3 +683,174 @@ test.describe('unhappy paths', () => {
678683 . toContainText ( `Cannot read properties of undefined (reading 'set')` ) ;
679684 } ) ;
680685} ) ;
686+
687+ test . describe ( 'sandbox features' , ( ) => {
688+ test . slow ( process . platform === 'darwin' || process . platform === 'win32' , 'Slow app start on these platforms' ) ;
689+
690+ test . beforeEach ( async ( { app, page } ) => {
691+ const text = await loadFixture ( 'pre-request-collection.yaml' ) ;
692+ await app . evaluate ( async ( { clipboard } , text ) => clipboard . writeText ( text ) , text ) ;
693+
694+ await page . getByLabel ( 'Import' ) . click ( ) ;
695+ await page . locator ( '[data-test-id="import-from-clipboard"]' ) . click ( ) ;
696+ await page . getByRole ( 'button' , { name : 'Scan' } ) . click ( ) ;
697+ await page . getByRole ( 'dialog' ) . getByRole ( 'button' , { name : 'Import' } ) . click ( ) ;
698+ } ) ;
699+
700+ // Blocked Roots / Scopes group: 'this' is blocked.
701+ test ( 'blocked roots / scopes group' , async ( { page } ) => {
702+ await page . getByLabel ( 'Request Collection' ) . getByTestId ( 'echo pre-request script result' ) . press ( 'Enter' ) ;
703+
704+
705+ await page . getByRole ( 'tab' , { name : 'Scripts' } ) . click ( ) ;
706+ const editor = page . getByTestId ( 'CodeEditor' ) . getByRole ( 'textbox' ) ;
707+
708+ // enter script that accesses a property on 'this'.
709+ await editor . fill ( `insomnia.environment.set('result', String(this?.process));` ) ;
710+
711+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
712+ await page . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 150 ) ) ) ;
713+
714+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
715+
716+ // verify blocked-root error
717+ await expect
718+ . soft ( page . getByTestId ( 'response-pane' ) )
719+ . toContainText ( "The script was blocked because it used 'this'." ) ;
720+
721+ // navigate to Settings → Scripting, disable the "Scopes" blocked roots group
722+ await page . getByTestId ( 'settings-button' ) . click ( ) ;
723+ await page . locator ( 'text=Insomnia Preferences' ) . first ( ) . click ( ) ;
724+ await page . getByRole ( 'tab' , { name : 'Scripting' } ) . click ( ) ;
725+ const scopesSwitch = page . locator ( 'div:has(> h4:has-text("Scopes")) label[data-react-aria-pressable]' ) ;
726+ await scopesSwitch . scrollIntoViewIfNeeded ( ) ;
727+ await scopesSwitch . click ( ) ;
728+
729+ await page . locator ( '.app' ) . press ( 'Escape' ) ;
730+
731+ // re-send — no sandbox error; this === undefined.
732+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
733+ await expect
734+ . soft ( page . getByTestId ( 'response-pane' ) )
735+ . not . toContainText ( "The script was blocked because it used 'this'." ) ;
736+ await expect
737+ . soft ( page . locator ( '[data-testid="response-status-tag"]:visible' ) )
738+ . toContainText ( '200 OK' ) ;
739+ } ) ;
740+
741+ // Blocked Properties / Prototype Mutation group: 'prototype' is blocked.
742+ test ( 'blocked properties / prototype mutation group' , async ( { page } ) => {
743+ await page . getByLabel ( 'Request Collection' ) . getByTestId ( 'echo pre-request script result' ) . press ( 'Enter' ) ;
744+
745+ // enter script that accesses Object.prototype.
746+ await page . getByRole ( 'tab' , { name : 'Scripts' } ) . click ( ) ;
747+ const editor = page . getByTestId ( 'CodeEditor' ) . getByRole ( 'textbox' ) ;
748+ await editor . fill ( `insomnia.environment.set('result', typeof Object.prototype.toString);` ) ;
749+
750+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
751+ await page . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 150 ) ) ) ;
752+
753+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
754+
755+ // verify blocked-property error
756+ await expect
757+ . soft ( page . getByTestId ( 'response-pane' ) )
758+ . toContainText ( "The script was blocked because it used the property 'prototype'." ) ;
759+
760+ // navigate to Settings → Scripting, disable the "Prototype Mutation" blocked properties group
761+ await page . getByTestId ( 'settings-button' ) . click ( ) ;
762+ await page . locator ( 'text=Insomnia Preferences' ) . first ( ) . click ( ) ;
763+ await page . getByRole ( 'tab' , { name : 'Scripting' } ) . click ( ) ;
764+ const protoMutationSwitch = page . locator ( 'div:has(> h4:has-text("Prototype Mutation")) label[data-react-aria-pressable]' ) ;
765+ await protoMutationSwitch . scrollIntoViewIfNeeded ( ) ;
766+ await protoMutationSwitch . click ( ) ;
767+ await expect . soft ( protoMutationSwitch ) . not . toHaveAttribute ( 'data-selected' ) ;
768+ await page . locator ( '.app' ) . press ( 'Escape' ) ;
769+
770+ // re-send — prototype access now allowed; Object.prototype.toString is a function
771+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
772+ await expect
773+ . soft ( page . getByTestId ( 'response-pane' ) )
774+ . not . toContainText ( "The script was blocked because it used the property 'prototype'." ) ;
775+ await expect
776+ . soft ( page . locator ( '[data-testid="response-status-tag"]:visible' ) )
777+ . toContainText ( '200 OK' ) ;
778+ } ) ;
779+
780+ // Mask Rules / Runtime APIs group: 'Function' is masked to undefined at runtime.
781+ test ( 'Mask Rules / Runtime APIs group.' , async ( { page } ) => {
782+ await page . getByLabel ( 'Request Collection' ) . getByTestId ( 'echo pre-request script result' ) . press ( 'Enter' ) ;
783+
784+ // enter script that uses the Function constructor, only masked at runtime.
785+ await page . getByRole ( 'tab' , { name : 'Scripts' } ) . click ( ) ;
786+ const editor = page . getByTestId ( 'CodeEditor' ) . getByRole ( 'textbox' ) ;
787+ await editor . fill ( `const f = new Function('return 42'); insomnia.environment.set('result', f());` ) ;
788+
789+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
790+ await page . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 150 ) ) ) ;
791+
792+ // send — Function masked to undefined → V8 uses the identifier name: "Function is not a constructor"
793+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
794+
795+ await expect
796+ . soft ( page . getByTestId ( 'response-pane' ) )
797+ . toContainText ( 'Function is not a constructor' ) ;
798+
799+ // navigate to Settings → Scripting, disable the "Runtime APIs" mask group
800+ await page . getByTestId ( 'settings-button' ) . click ( ) ;
801+ await page . locator ( 'text=Insomnia Preferences' ) . first ( ) . click ( ) ;
802+ await page . getByRole ( 'tab' , { name : 'Scripting' } ) . click ( ) ;
803+ const runtimeApisSwitch = page . locator ( 'div:has(> h4:has-text("Runtime APIs")) label[data-react-aria-pressable]' ) ;
804+ await runtimeApisSwitch . scrollIntoViewIfNeeded ( ) ;
805+ await runtimeApisSwitch . click ( ) ;
806+ await expect . soft ( runtimeApisSwitch ) . not . toHaveAttribute ( 'data-selected' ) ;
807+ await page . locator ( '.app' ) . press ( 'Escape' ) ;
808+
809+ // re-send — Function is now the real constructor; script returns 42
810+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
811+ await expect
812+ . soft ( page . getByTestId ( 'response-pane' ) )
813+ . not . toContainText ( 'Function is not a constructor' ) ;
814+ await expect
815+ . soft ( page . locator ( '[data-testid="response-status-tag"]:visible' ) )
816+ . toContainText ( '200 OK' ) ;
817+ } ) ;
818+
819+ test ( 'Layered security / unblocked properties resolve undefined' , async ( { page } ) => {
820+ await page . getByLabel ( 'Request Collection' ) . getByTestId ( 'echo pre-request script result' ) . press ( 'Enter' ) ;
821+
822+ // enter script that accesses a property on 'process'.
823+ await page . getByRole ( 'tab' , { name : 'Scripts' } ) . click ( ) ;
824+ const editor = page . getByTestId ( 'CodeEditor' ) . getByRole ( 'textbox' ) ;
825+ await editor . fill ( `insomnia.environment.set('result', String(process?.version));` ) ;
826+
827+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
828+ await page . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 150 ) ) ) ;
829+
830+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
831+
832+ // verify blocked-root error
833+ await expect
834+ . soft ( page . getByTestId ( 'response-pane' ) )
835+ . toContainText ( "The script was blocked because it used 'process'." ) ;
836+
837+ // navigate to Settings → Scripting, disable only the "Node.js Internals" BLOCKED ROOTS group.
838+ await page . getByTestId ( 'settings-button' ) . click ( ) ;
839+ await page . locator ( 'text=Insomnia Preferences' ) . first ( ) . click ( ) ;
840+ await page . getByRole ( 'tab' , { name : 'Scripting' } ) . click ( ) ;
841+ const nodeInternalsSwitch = page . locator ( 'xpath=//h4[normalize-space(text())="Node.js Internals"]/following-sibling::div[1]//label[@data-react-aria-pressable]' ) ;
842+ await nodeInternalsSwitch . scrollIntoViewIfNeeded ( ) ;
843+ await nodeInternalsSwitch . click ( ) ;
844+ await expect . soft ( nodeInternalsSwitch ) . not . toHaveAttribute ( 'data-selected' ) ;
845+ await page . locator ( '.app' ) . press ( 'Escape' ) ;
846+
847+ // process?.version === undefined.
848+ await page . getByTestId ( 'request-pane' ) . getByRole ( 'button' , { name : 'Send' } ) . click ( ) ;
849+ await expect
850+ . soft ( page . getByTestId ( 'response-pane' ) )
851+ . not . toContainText ( "The script was blocked because it used 'process'." ) ;
852+ await expect
853+ . soft ( page . locator ( '[data-testid="response-status-tag"]:visible' ) )
854+ . toContainText ( '200 OK' ) ;
855+ } ) ;
856+ } ) ;
0 commit comments