Skip to content

Commit

Permalink
[FTR] Implement browser network condition utils (elastic#163633)
Browse files Browse the repository at this point in the history
## 📓 Summary

The PR implements some utilities into the `browser` service to allow
controlling the network conditions during the execution of a functional
test.

### `getNetworkConditions`

Returns the current network simulation options. If none conditions are
previously set, it returns `undefined`

**N.B.**: _if the testing environment is not a Chromium browser, it
throws an error that can be easily caught to manually skip the test or
handle a fallback scenario._

```ts
it('should display a loading skeleton while loading', async function () {
  // Skip the test in case network condition utils are not available
  try {
    const networkConditions = await browser.getNetworkConditions(); // undefined

    await browser.setNetworkConditions('SLOW_3G');

    const networkConditions = await browser.getNetworkConditions();
    // {
    //   offline: false,
    //   latency: 2000,
    //   download_throughput: 50000,
    //   upload_throughput: 50000,
    // }
  } catch (error) {
    this.skip();
  }
});
```

### `setNetworkConditions`

Set the desired network conditions.
It supports different presets that match the [network profiles provided
by Chrome
debugger](https://github.com/ChromeDevTools/devtools-frontend/blob/da276a3faec9769cb55e442f0db77ebdce5cd178/front_end/core/sdk/NetworkManager.ts#L363-L393):
- `NO_THROTTLING`
- `FAST_3G`
- `SLOW_3G`
- `OFFLINE`
- `CLOUD_USER` (pre-existing)
It also accepts ad-hoc options to configure more specifically the
network conditions.

**N.B.**: _if the testing environment is not a Chromium browser, it
throws an error that can be easily caught to manually skip the test or
handle a fallback scenario._

```ts
it('should display a loading skeleton while loading', async function () {
  // Skip the test in case network condition utils are not available
  try {
    await browser.setNetworkConditions('NO_THROTTLING');
    await browser.setNetworkConditions('FAST_3G');
    await browser.setNetworkConditions('SLOW_3G');
    await browser.setNetworkConditions('OFFLINE');
    await browser.setNetworkConditions('CLOUD_USER');
    await browser.setNetworkConditions({
      offline: false,
      latency: 5, // Additional latency (ms).
      download_throughput: 500 * 1024, // Maximal aggregated download throughput.
      upload_throughput: 500 * 1024, // Maximal aggregated upload throughput.
    });
  } catch (error) {
    this.skip();
  }
});
```

### restoreNetworkConditions

Restore the original network conditions, setting to `NO_THROTTLING`.
The native implementation of `deleteNetworkConditions` exposed by
selenium is unofficial and didn't consistently work, the recommended
approach by the google dev tools team is to restore the connection
setting the no throttling profile.

**N.B.**: _if the testing environment is not a Chromium browser, it
throws an error that can be easily caught to manually skip the test or
handle a fallback scenario._

```ts
it('should display a loading skeleton while loading', async function () {
  // Skip the test in case network condition utils are not available
  try {
    await browser.setNetworkConditions('SLOW_3G'); // Slow down network conditions

    // Do your assertions

    await browser.restoreNetworkConditions(); // Restore network conditions
  } catch (error) {
    this.skip();
  }
});
```

Co-authored-by: Marco Antonio Ghiani <[email protected]>
Co-authored-by: Dzmitry Lemechko <[email protected]>
(cherry picked from commit aa45152)
  • Loading branch information
tonyghiani committed Aug 11, 2023
1 parent c375c78 commit 84762c8
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(event.properties.value4).to.be.a('number');
expect(event.properties.value5).to.be.a('number');

if (browser.isChromium) {
if (browser.isChromium()) {
// Kibana Loaded memory
expect(meta).to.have.property('jsHeapSizeLimit');
expect(meta.jsHeapSizeLimit).to.be.a('number');
Expand Down
75 changes: 70 additions & 5 deletions test/functional/services/common/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
*/

import { setTimeout as setTimeoutAsync } from 'timers/promises';
import { cloneDeepWith } from 'lodash';
import { cloneDeepWith, isString } from 'lodash';
import { Key, Origin, WebDriver } from 'selenium-webdriver';
import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome';
import { modifyUrl } from '@kbn/std';

import sharp from 'sharp';
import { NoSuchSessionError } from 'selenium-webdriver/lib/error';
import { WebElementWrapper } from '../lib/web_element_wrapper';
import { FtrProviderContext, FtrService } from '../../ftr_provider_context';
import { Browsers } from '../remote/browsers';
import { NetworkOptions, NetworkProfile, NETWORK_PROFILES } from '../remote/network_profiles';

export type Browser = BrowserService;

Expand All @@ -25,19 +27,20 @@ class BrowserService extends FtrService {
*/
public readonly keys = Key;
public readonly isFirefox: boolean;
public readonly isChromium: boolean;

private readonly log = this.ctx.getService('log');

constructor(
ctx: FtrProviderContext,
public readonly browserType: string,
private readonly driver: WebDriver
protected readonly driver: WebDriver | ChromiumWebDriver
) {
super(ctx);
this.isFirefox = this.browserType === Browsers.Firefox;
this.isChromium =
this.browserType === Browsers.Chrome || this.browserType === Browsers.ChromiumEdge;
}

public isChromium(): this is { driver: ChromiumWebDriver } {
return this.driver instanceof ChromiumWebDriver;
}

/**
Expand Down Expand Up @@ -661,6 +664,68 @@ class BrowserService extends FtrService {
}
}
}

/**
* Get the network simulation for chromium browsers if available.
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#getNetworkConditions
*
* @return {Promise<NetworkOptions>}
*/
public async getNetworkConditions() {
if (this.isChromium()) {
return this.driver.getNetworkConditions().catch(() => undefined); // Return undefined instead of throwing if no conditions are set.
} else {
const message =
'WebDriver does not support the .getNetworkConditions method.\nProbably the browser in used is not chromium based.';
this.log.error(message);
throw new Error(message);
}
}

/**
* Delete the network simulation for chromium browsers if available.
*
* @return {Promise<void>}
*/
public async restoreNetworkConditions() {
this.log.debug('Restore network conditions simulation.');
return this.setNetworkConditions('NO_THROTTLING');
}

/**
* Set the network conditions for chromium browsers if available.
*
* __Sample Usage:__
*
* browser.setNetworkConditions('FAST_3G')
* browser.setNetworkConditions('SLOW_3G')
* browser.setNetworkConditions('OFFLINE')
* browser.setNetworkConditions({
* offline: false,
* latency: 5, // Additional latency (ms).
* download_throughput: 500 * 1024, // Maximal aggregated download throughput.
* upload_throughput: 500 * 1024, // Maximal aggregated upload throughput.
* });
*
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#setNetworkConditions
*
* @return {Promise<void>}
*/
public async setNetworkConditions(profileOrOptions: NetworkProfile | NetworkOptions) {
const networkOptions = isString(profileOrOptions)
? NETWORK_PROFILES[profileOrOptions]
: profileOrOptions;

if (this.isChromium()) {
this.log.debug(`Set network conditions with profile "${profileOrOptions}".`);
return this.driver.setNetworkConditions(networkOptions);
} else {
const message =
'WebDriver does not support the .setNetworkCondition method.\nProbably the browser in used is not chromium based.';
this.log.error(message);
throw new Error(message);
}
}
}

export async function BrowserProvider(ctx: FtrProviderContext) {
Expand Down
45 changes: 39 additions & 6 deletions test/functional/services/remote/network_profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,50 @@
* Side Public License, v 1.
*/

interface NetworkOptions {
DOWNLOAD: number;
UPLOAD: number;
LATENCY: number;
export type NetworkProfile = 'NO_THROTTLING' | 'FAST_3G' | 'SLOW_3G' | 'OFFLINE' | 'CLOUD_USER';

export interface NetworkOptions {
offline: boolean;
latency: number;
download_throughput: number;
upload_throughput: number;
}

const sec = 10 ** 3;
const MBps = 10 ** 6 / 8; // megabyte per second (MB/s) (can be abbreviated as MBps)

// Selenium uses B/s (bytes) for network throttling
// Download (B/s) Upload (B/s) Latency (ms)
export const NETWORK_PROFILES: { [key: string]: NetworkOptions } = {
CLOUD_USER: { DOWNLOAD: 6 * MBps, UPLOAD: 6 * MBps, LATENCY: 0.1 * sec },

export const NETWORK_PROFILES: Record<NetworkProfile, NetworkOptions> = {
NO_THROTTLING: {
offline: false,
latency: 0,
download_throughput: -1,
upload_throughput: -1,
},
FAST_3G: {
offline: false,
latency: 0.56 * sec,
download_throughput: 1.44 * MBps,
upload_throughput: 0.7 * MBps,
},
SLOW_3G: {
offline: false,
latency: 2 * sec,
download_throughput: 0.4 * MBps,
upload_throughput: 0.4 * MBps,
},
OFFLINE: {
offline: true,
latency: 0,
download_throughput: 0,
upload_throughput: 0,
},
CLOUD_USER: {
offline: false,
latency: 0.1 * sec,
download_throughput: 6 * MBps,
upload_throughput: 6 * MBps,
},
};
24 changes: 7 additions & 17 deletions test/functional/services/remote/webdriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { createStdoutSocket } from './create_stdout_stream';
import { preventParallelCalls } from './prevent_parallel_calls';

import { Browsers } from './browsers';
import { NETWORK_PROFILES } from './network_profiles';
import { NetworkProfile, NETWORK_PROFILES } from './network_profiles';

const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string;
const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string;
Expand Down Expand Up @@ -300,22 +300,17 @@ async function attemptToCreateCommand(
const { session, consoleLog$ } = await buildDriverInstance();

if (throttleOption === '1' && browserType === 'chrome') {
const { KBN_NETWORK_TEST_PROFILE = 'CLOUD_USER' } = process.env;
const KBN_NETWORK_TEST_PROFILE = (process.env.KBN_NETWORK_TEST_PROFILE ??
'CLOUD_USER') as NetworkProfile;

const profile =
KBN_NETWORK_TEST_PROFILE in Object.keys(NETWORK_PROFILES)
? KBN_NETWORK_TEST_PROFILE
: 'CLOUD_USER';
KBN_NETWORK_TEST_PROFILE in NETWORK_PROFILES ? KBN_NETWORK_TEST_PROFILE : 'CLOUD_USER';

const {
DOWNLOAD: downloadThroughput,
UPLOAD: uploadThroughput,
LATENCY: latency,
} = NETWORK_PROFILES[`${profile}`];
const networkProfileOptions = NETWORK_PROFILES[profile];

// Only chrome supports this option.
log.debug(
`NETWORK THROTTLED with profile ${profile}: ${downloadThroughput} B/s down, ${uploadThroughput} B/s up, ${latency} ms latency.`
`NETWORK THROTTLED with profile ${profile}: ${networkProfileOptions.download_throughput} B/s down, ${networkProfileOptions.upload_throughput} B/s up, ${networkProfileOptions.latency} ms latency.`
);

if (noCache) {
Expand All @@ -326,12 +321,7 @@ async function attemptToCreateCommand(
}

// @ts-expect-error
session.setNetworkConditions({
offline: false,
latency,
download_throughput: downloadThroughput,
upload_throughput: uploadThroughput,
});
session.setNetworkConditions(networkProfileOptions);
}

if (attemptId !== attemptCounter) {
Expand Down

0 comments on commit 84762c8

Please sign in to comment.