Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iOS): add launch-argument to disable WebKit security. #4429

Merged
merged 6 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Expand Down
2 changes: 1 addition & 1 deletion detox/ios/DetoxSync
106 changes: 66 additions & 40 deletions detox/test/e2e/29.webview.test.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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();
});
});
});

Expand Down Expand Up @@ -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();
});
});
});

Expand All @@ -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;
Expand Down Expand Up @@ -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();
});
});
19 changes: 19 additions & 0 deletions docs/api/device.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
13 changes: 13 additions & 0 deletions docs/guide/testing-webviews.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Loading