diff --git a/detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m b/detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m index a4856379f2..f1bc7f39a3 100644 --- a/detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m +++ b/detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m @@ -62,9 +62,15 @@ + (void)load { } - (void)dtx_setPreferences:(WKPreferences *)preferences { - DTXPreferencesSetWebSecurityEnabled(preferences, NO); + if ([self shouldDisableWebKitSecurity]) { + DTXPreferencesSetWebSecurityEnabled(preferences, NO); + } [self dtx_setPreferences:preferences]; } +- (BOOL)shouldDisableWebKitSecurity { + return [NSUserDefaults.standardUserDefaults boolForKey:@"detoxDisableWebKitSecurity"]; +} + @end diff --git a/detox/ios/Detox/Invocation/WebCodeBuilder+createSelector.swift b/detox/ios/Detox/Invocation/WebCodeBuilder+createSelector.swift index 70d1bf45ee..1ccbb88072 100644 --- a/detox/ios/Detox/Invocation/WebCodeBuilder+createSelector.swift +++ b/detox/ios/Detox/Invocation/WebCodeBuilder+createSelector.swift @@ -36,10 +36,10 @@ extension WebCodeBuilder { var frameElements = getAllElements(frameDoc, selector); elements = elements.concat(frameElements); } catch(e) { - throw e; /* Probably issues accessing iframe documents (CORS restrictions) */ } } + return elements; }; var allElements = getAllElements(document, '\(baseSelector)'); diff --git a/detox/ios/DetoxSync b/detox/ios/DetoxSync index 1c353480c0..f69efdbd95 160000 --- a/detox/ios/DetoxSync +++ b/detox/ios/DetoxSync @@ -1 +1 @@ -Subproject commit 1c353480c0411ca178303769ec07b524b68bd4e2 +Subproject commit f69efdbd9580f3c336ca5c3caed2926afb651793 diff --git a/detox/test/e2e/29.webview.test.js b/detox/test/e2e/29.webview.test.js index f11a28c4ab..18753cc81f 100644 --- a/detox/test/e2e/29.webview.test.js +++ b/detox/test/e2e/29.webview.test.js @@ -1,10 +1,12 @@ const {expectElementSnapshotToMatch} = require("./utils/snapshot"); const {waitForCondition} = require("./utils/waitForCondition"); +const {expectToThrow} = require('./utils/custom-expects'); + const jestExpect = require('expect').default; const MockServer = require('../mock-server/mock-server'); -describe('Web View', () => { +describe('WebView', () => { beforeEach(async () => { await device.reloadReactNative(); await element(by.text('WebView')).tap(); @@ -31,15 +33,15 @@ describe('Web View', () => { }); it('should raise an error when element does not exists but expect to exist', async () => { - await jestExpect(async () => { + await expectToThrow(async () => { await expect(web.element(by.web.id('nonExistentElement'))).toExist(); - }).rejects.toThrowError(); + }); }); it('should raise an error when does element not exists at index', async () => { - await jestExpect(async () => { + await expectToThrow(async () => { await expect(web.element(by.web.tag('p')).atIndex(100)).toExist(); - }).rejects.toThrowError(); + }); }); }); @@ -266,9 +268,9 @@ describe('Web View', () => { it('should raise error when script fails', async () => { const headline = web.element(by.web.id('pageHeadline')); - await jestExpect(async () => { + await expectToThrow(async () => { await headline.runScript('(el) => { el.textContent = "Changed"; throw new Error("Error"); }'); - }).rejects.toThrowError(); + }); }); }); @@ -295,35 +297,6 @@ describe('Web View', () => { }); }); - describe(':ios: inner frame', () => { - /** @type {Detox.WebViewElement} */ - let webview; - const mockServer = new MockServer(); - - beforeAll(async () => { - mockServer.init(); - - if (device.getPlatform() === 'android') { - // Android needs to reverse the port in order to access the mock server - await device.reverseTcpPort(mockServer.port); - } - }); - - afterAll(async () => { - await mockServer.close(); - }); - - beforeEach(async () => { - await element(by.id('toggle3rdWebviewButton')).tap(); - webview = web(by.id('webView')); - }); - - it('should find element in inner frame', async () => { - await expect(webview.element(by.web.tag('h1'))).toExist(); - await expect(webview.element(by.web.tag('h1'))).toHaveText('Hello World!'); - }); - }); - describe('multiple web-views scenario',() => { /** @type {Detox.WebViewElement} */ let webview; @@ -366,18 +339,71 @@ describe('Web View', () => { }); it('should throw on index out of bounds', async () => { - await jestExpect(async () => { + await expectToThrow(async () => { await expect(web(by.id('webView')).atIndex(2).element(by.web.id('message'))).toExist(); - }).rejects.toThrowError(); + }); }); }); // Not implemented yet it(':android: should throw on usage of atIndex', async () => { - await jestExpect(async () => { + await expectToThrow(async () => { await expect(web(by.id('webView')).atIndex(0).element(by.web.id('message'))).toExist(); - }).rejects.toThrowError(); + }); }); }); }); }); + +describe(':ios: WebView CORS (inner frame)', () => { + /** @type {Detox.WebViewElement} */ + let webview; + const mockServer = new MockServer(); + + beforeAll(async () => { + mockServer.init(); + + if (device.getPlatform() === 'android') { + // Android needs to reverse the port in order to access the mock server + await device.reverseTcpPort(mockServer.port); + } + }); + + afterAll(async () => { + await mockServer.close(); + }); + + const launchAndNavigateToInnerFrame = async (shouldDisableWebKitSecurity) => { + await device.launchApp({ + newInstance: true, + launchArgs: { + detoxDisableWebKitSecurity: + shouldDisableWebKitSecurity !== undefined ? (shouldDisableWebKitSecurity ? 'true' : 'false') : undefined, + }, + }); + + await element(by.text('WebView')).tap(); + await element(by.id('toggle3rdWebviewButton')).tap(); + + webview = web(by.id('webView')); + }; + + it('should find element in cross-origin frame when `detoxDisableWebKitSecurity` is `true`', async () => { + await launchAndNavigateToInnerFrame(true); + + await expect(webview.element(by.web.tag('h1'))).toExist(); + await expect(webview.element(by.web.tag('h1'))).toHaveText('Hello World!'); + }); + + it('should not find element in cross-origin frame when `detoxDisableWebKitSecurity` is `false`', async () => { + await launchAndNavigateToInnerFrame(false); + + await expect(webview.element(by.web.tag('h1'))).not.toExist(); + }); + + it('should not find element in cross-origin frame when `detoxDisableWebKitSecurity` is not set', async () => { + await launchAndNavigateToInnerFrame(); + + await expect(webview.element(by.web.tag('h1'))).not.toExist(); + }); +}); diff --git a/docs/api/device.md b/docs/api/device.md index e71b4d0838..7673c39d26 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -257,6 +257,25 @@ await device.launchApp({ }); ``` +#### 12. `detoxDisableWebKitSecurity`—Disable WebKit Security (iOS Only) + +Disables WebKit security on iOS. Default is `false`. + +This is useful for testing web views with iframes that loads CORS-protected content. + +:::caution Important + +Some pages may not load correctly when WebKit security is disabled (for example, PCI DSS-compliant pages). +Disabling WebKit security may cause errors when loading pages that have strict security policies. + +::: + +```js +await device.launchApp({ + launchArgs: { detoxDisableWebKitSecurity: true } +}); +``` + ### `device.terminateApp()` By default, `terminateApp()` with no params will terminate the app file defined in the current [`configuration`](../config/overview.mdx). diff --git a/docs/guide/testing-webviews.md b/docs/guide/testing-webviews.md index ef1b5f7949..941eda100f 100644 --- a/docs/guide/testing-webviews.md +++ b/docs/guide/testing-webviews.md @@ -70,6 +70,18 @@ const elementByCSSSelector = web.element(by.web.cssSelector('#cssSelector')); const elementAtIndex = web.element(by.web.id('identifier').atIndex(1)); ``` +### Bypass CORS Restrictions (iOS Only) + +When testing web views, you may encounter Cross-Origin Resource Sharing (CORS) restrictions that prevent you from interacting with elements inside the web view. + +At the moment, Detox is able to bypass CORS restrictions and other browser security features only on iOS, allowing you to interact with inner elements in cases of CORS restrictions (in most cases). + +To bypass CORS restrictions on iOS, you can pass the [`detoxDisableWebKitSecurity`] launch argument. This argument will disable the WebKit security features, allowing Detox to interact with the WebView in a "Sandbox" environment. + +```javascript +await device.launchApp({ launchArgs: { detoxDisableWebKitSecurity: true } }); +``` + ## Step 3: Perform Actions Actions allow you to interact with elements within a web view. The [Detox WebView APIs][actions-apis] provide various actions that can be invoked on inner elements. @@ -165,3 +177,4 @@ it('should login successfully', async () => { [run-script-api]: ../api/webviews.md#runscriptscript-args [finding-inner-elements]: #step-2-finding-inner-elements [at-index-api]: ../api/webviews.md#webnativematcheratindexindexelementmatcher +[`detoxDisableWebKitSecurity`]: ../api/device.md#12-detoxdisablewebkitsecuritydisable-webkit-security-ios-only