diff --git a/lib/api/client-commands/captureNetworkRequests.js b/lib/api/client-commands/network/captureRequests.js similarity index 84% rename from lib/api/client-commands/captureNetworkRequests.js rename to lib/api/client-commands/network/captureRequests.js index d71794cdb2..658aab01f2 100644 --- a/lib/api/client-commands/captureNetworkRequests.js +++ b/lib/api/client-commands/network/captureRequests.js @@ -1,5 +1,5 @@ -const ClientCommand = require('./_base-command.js'); -const {Logger} = require('../../utils'); +const ClientCommand = require('../_base-command.js'); +const {Logger} = require('../../../utils'); /** * Capture outgoing network calls from the browser. @@ -9,7 +9,7 @@ const {Logger} = require('../../utils'); * it('captures and logs network requests as they occur', function(this: ExtendDescribeThis<{requestCount: number}>) { * this.requestCount = 1; * browser - * .captureNetworkRequests((requestParams) => { + * .network.captureRequests((requestParams) => { * console.log('Request Number:', this.requestCount!++); * console.log('Request URL:', requestParams.request.url); * console.log('Request method:', requestParams.request.method); @@ -19,8 +19,9 @@ const {Logger} = require('../../utils'); * }); * }); * - * @method captureNetworkRequests + * @method network.captureRequests * @syntax .captureNetworkRequests(onRequestCallback) + * @syntax .network.captureRequests(onRequestCallback) * @param {function} onRequestCallback Callback function called whenever a new outgoing network request is made. * @api protocol.cdp * @since 2.2.0 @@ -28,6 +29,10 @@ const {Logger} = require('../../utils'); */ class CaptureNetworkCalls extends ClientCommand { + static get namespacedAliases() { + return 'captureNetworkRequests'; + } + performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { diff --git a/lib/api/client-commands/mockNetworkResponse.js b/lib/api/client-commands/network/mockResponse.js similarity index 84% rename from lib/api/client-commands/mockNetworkResponse.js rename to lib/api/client-commands/network/mockResponse.js index e564b46a7e..e38bcbd54b 100644 --- a/lib/api/client-commands/mockNetworkResponse.js +++ b/lib/api/client-commands/network/mockResponse.js @@ -1,5 +1,5 @@ -const ClientCommand = require('./_base-command.js'); -const {Logger} = require('../../utils'); +const ClientCommand = require('../_base-command.js'); +const {Logger} = require('../../../utils'); /** * Intercept the request made on a particular URL and mock the response. @@ -8,7 +8,7 @@ const {Logger} = require('../../utils'); * describe('mock network response', function() { * it('intercepts the request made to Google search and mocks its response', function() { * browser - * .mockNetworkResponse('https://www.google.com/', { + * .network.mockResponse('https://www.google.com/', { * status: 200, * headers: { * 'Content-Type': 'UTF-8' @@ -20,8 +20,9 @@ const {Logger} = require('../../utils'); * }); * }); * - * @method mockNetworkResponse + * @method network.mockResponse * @syntax .mockNetworkResponse(urlToIntercept, {status, headers, body}, [callback]) + * @syntax .network.mockResponse(urlToIntercept, {status, headers, body}, [callback]) * @param {string} urlToIntercept URL to intercept and mock the response from. * @param {object} response Response to return. Defaults: `{status: 200, headers: {}, body: ''}`. * @param {function} [callback] Callback function to be called when the command finishes. @@ -31,6 +32,10 @@ const {Logger} = require('../../utils'); */ class MockNetworkResponse extends ClientCommand { + static get namespacedAliases() { + return 'mockNetworkResponse'; + } + performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { diff --git a/lib/api/client-commands/setNetworkConditions.js b/lib/api/client-commands/network/setConditions.js similarity index 61% rename from lib/api/client-commands/setNetworkConditions.js rename to lib/api/client-commands/network/setConditions.js index 6a932c70c7..7d642f4b6e 100644 --- a/lib/api/client-commands/setNetworkConditions.js +++ b/lib/api/client-commands/network/setConditions.js @@ -1,28 +1,36 @@ -const ClientCommand = require('./_base-command.js'); -const {Logger} = require('../../utils'); +const ClientCommand = require('../_base-command.js'); +const {Logger} = require('../../../utils'); /** * * Command to set Chrome network emulation settings. * * @example - * this.demoTest = function (browser) { - * browser.setNetworkConditions({ + * describe('set network conditions', function() { + * it('sets the network conditions',function() { + * browser + * .network.setConditions({ * offline: false, - * latency: 50000, - * download_throughput: 450 * 1024, - * upload_throughput: 150 * 1024 + * latency: 3000, + * download_throughput: 500 * 1024, + * upload_throughput: 500 * 1024 * }); - * }; + * }); + * }); * - * - * @method setNetworkConditions + * @method network.setConditions * @syntax .setNetworkConditions(spec, [callback]) + * @syntax .network.setConditions(spec, [callback]) * @param {object} spec * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ class SetNetworkConditions extends ClientCommand { + + static get namespacedAliases() { + return 'setNetworkConditions'; + } + performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('SetNetworkConditions is not supported while using this driver'); diff --git a/test/src/api/commands/client/testCaptureNetworkRequests.js b/test/src/api/commands/client/testCaptureNetworkRequests.js index 90920f1f63..fea08563b2 100644 --- a/test/src/api/commands/client/testCaptureNetworkRequests.js +++ b/test/src/api/commands/client/testCaptureNetworkRequests.js @@ -85,6 +85,73 @@ describe('.captureNetworkRequests()', function () { }); }); + it('browser.network.captureRequests()', function (done) { + + MockServer.addMock({ + url: '/session', + response: { + value: { + sessionId: '13521-10219-202', + capabilities: { + browserName: 'chrome', + browserVersion: '92.0' + } + } + }, + method: 'POST', + statusCode: 201 + }, true); + + Nightwatch.initW3CClient({ + desiredCapabilities: { + browserName: 'chrome', + 'goog:chromeOptions': {} + }, + output: process.env.VERBOSE === '1', + silent: false + }).then(client => { + const expected = {}; + + const cdpNetworkEvent = JSON.stringify({ + method: 'Network.requestWillBeSent', + params: { + request: { + url: 'https://www.google.com', + method: 'GET', + headers: [] + } + } + }); + + cdp.resetConnection(); + client.transport.driver.createCDPConnection = function() { + return Promise.resolve({ + _wsConnection: { + on: (event, callback) => { + expected['wsEvent'] = event; + callback(cdpNetworkEvent); + } + }, + execute: function(command, params) { + expected['cdpCommand'] = command; + expected['cdpParams'] = params; + } + }); + }; + + const userCallback = (requestParams) => { + expected['requestParams'] = requestParams; + }; + client.api.network.captureRequests(userCallback, function () { + assert.deepEqual(expected.cdpCommand, 'Network.enable'); + assert.deepEqual(expected.cdpParams, {}); + assert.strictEqual(expected.wsEvent, 'message'); + assert.deepEqual(expected.requestParams, JSON.parse(cdpNetworkEvent).params); + }); + client.start(done); + }); + }); + it('throws error without callback', function (done) { MockServer.addMock({ diff --git a/test/src/api/commands/client/testMockNetworkResponse.js b/test/src/api/commands/client/testMockNetworkResponse.js index 3d9f812edb..c5cf2b8c97 100644 --- a/test/src/api/commands/client/testMockNetworkResponse.js +++ b/test/src/api/commands/client/testMockNetworkResponse.js @@ -98,6 +98,86 @@ describe('.mockNetworkResponse()', function () { }); }); + it('browser.network.mockResponse(urlToIntercept, {status, headers, body}) with url match', function (done) { + MockServer.addMock({ + url: '/session', + response: { + value: { + sessionId: '13521-10219-202', + capabilities: { + browserName: 'chrome', + browserVersion: '92.0' + } + } + }, + method: 'POST', + statusCode: 201 + }, true); + + Nightwatch.initW3CClient({ + desiredCapabilities: { + browserName: 'chrome', + 'goog:chromeOptions': {} + }, + output: process.env.VERBOSE === '1', + silent: false + }).then(client => { + const expected = { + cdpCommands: [] + }; + + // Parameters of actual request made by browser + const cdpFetchRequestPauseEvent = JSON.stringify({ + method: 'Fetch.requestPaused', + params: { + requestId: '123', + request: { + url: 'https://www.google.com/' + } + } + }); + + cdp.resetConnection(); + client.transport.driver.createCDPConnection = function() { + return Promise.resolve({ + _wsConnection: { + on: (event, callback) => { + expected['wsEvent'] = event; + callback(cdpFetchRequestPauseEvent); + } + }, + execute: function(command, params) { + expected.cdpCommands.push(command); + if (command === 'Fetch.fulfillRequest') { + expected['requestId'] = params.requestId; + expected['responseCode'] = params.responseCode; + expected['responseHeaders'] = params.responseHeaders; + expected['responseBody'] = params.body; + } + } + }); + }; + + const response = { + status: 200, + headers: {'Content-Type': 'UTF-8'}, + body: 'Hey there!' + }; + client.api.network.mockResponse('https://www.google.com/', response, function () { + // Assert final response with response passed + assert.strictEqual(expected.responseCode, response.status); + assert.deepEqual(expected.responseHeaders, [{name: 'Content-Type', value: 'UTF-8'}]); + assert.strictEqual(expected.responseBody, Buffer.from(response.body, 'utf-8').toString('base64')); + + assert.strictEqual(expected.requestId, JSON.parse(cdpFetchRequestPauseEvent).params.requestId); + assert.strictEqual(expected.wsEvent, 'message'); + assert.deepEqual(expected.cdpCommands, ['Fetch.fulfillRequest', 'Fetch.enable', 'Network.setCacheDisabled']); + }); + + client.start(done); + }); + }); + it('browser.mockNetworkResponse(urlToIntercept, {status, headers, body}) with multiple mocks', function (done) { MockServer.addMock({ url: '/session', diff --git a/test/src/api/commands/client/testSetNetworkConditions.js b/test/src/api/commands/client/testSetNetworkConditions.js index 9087db3531..e05b6f1708 100644 --- a/test/src/api/commands/client/testSetNetworkConditions.js +++ b/test/src/api/commands/client/testSetNetworkConditions.js @@ -5,7 +5,11 @@ const Nightwatch = require('../../../../lib/nightwatch.js'); describe('.setNetworkConditions()', function () { beforeEach(function (done) { - CommandGlobals.beforeEach.call(this, done); + this.server = MockServer.init(); + + this.server.on('listening', () => { + done(); + }); }); afterEach(function (done) { @@ -59,6 +63,70 @@ describe('.setNetworkConditions()', function () { }); }); + + it('browser.network.setConditions()', function (done) { + MockServer.addMock( + { + url: '/session', + response: { + value: { + sessionId: '13521-10219-202', + capabilities: { + browserName: 'chrome', + browserVersion: '92.0' + } + } + }, + method: 'POST', + statusCode: 201 + }, + true + ); + + Nightwatch.initW3CClient({ + desiredCapabilities: { + browserName: 'chrome', + 'goog:chromeOptions': {} + }, + output: process.env.VERBOSE === '1', + silent: false + }).then((client) => { + const expected = {}; + client.transport.driver.setNetworkConditions = function (spec) { + expected['download_throughput'] = spec.download_throughput; + expected['latency'] = spec.latency; + expected['offline'] = spec.offline; + expected['upload_throughput'] = spec.upload_throughput; + + return Promise.resolve(); + }; + + client.api.network.setConditions({ + offline: false, + latency: 50000, + download_throughput: 450 * 1024, + upload_throughput: 150 * 1024 + }, + function (result) { + expected['callback_result'] = result.value; + }); + + client.start(function (err) { + try { + assert.strictEqual(err, undefined); + assert.strictEqual(expected.callback_result, null); + assert.strictEqual(expected.download_throughput, 460800); + assert.strictEqual(expected.latency, 50000); + assert.strictEqual(expected.offline, false); + assert.strictEqual(expected.upload_throughput, 153600); + done(); + } catch (e){ + done(e); + } + }); + }); + }); + it('browser.setNetworkConditions - driver not supported', function (done) { Nightwatch.initW3CClient({ desiredCapabilities: { diff --git a/types/index.d.ts b/types/index.d.ts index 3c8286b92b..42b7d9a7a6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -516,6 +516,7 @@ export interface NamespacedApi { document: DocumentNsCommands; window: WindowNsCommands; firefox: FirefoxNsCommands; + network: NetworkNsCommands; assert: Assert; verify: Assert; @@ -1383,70 +1384,6 @@ export interface ChromiumClientCommands { ) => void ): Awaitable; - /** - * Capture outgoing network calls from the browser. - * - * @example - * describe('capture network requests', function() { - * it('captures and logs network requests as they occur', function(this: ExtendDescribeThis<{requestCount: number}>) { - * this.requestCount = 1; - * browser - * .captureNetworkRequests((requestParams) => { - * console.log('Request Number:', this.requestCount!++); - * console.log('Request URL:', requestParams.request.url); - * console.log('Request method:', requestParams.request.method); - * console.log('Request headers:', requestParams.request.headers); - * }) - * .navigateTo('https://www.google.com'); - * }); - * }); - * - * @see https://nightwatchjs.org/guide/network-requests/capture-network-calls.html - */ - captureNetworkRequests( - onRequestCallback: ( - requestParams: Protocol.Network.RequestWillBeSentEvent - ) => void, - callback?: ( - this: NightwatchAPI, - result: NightwatchCallbackResult - ) => void - ): Awaitable; - - /** - * Intercept the request made on a particular URL and mock the response. - * - * @example - * describe('mock network response', function() { - * it('intercepts the request made to Google search and mocks its response', function() { - * browser - * .mockNetworkResponse('https://www.google.com/', { - * status: 200, - * headers: { - * 'Content-Type': 'UTF-8' - * }, - * body: 'Hello there!' - * }) - * .navigateTo('https://www.google.com/') - * .pause(2000); - * }); - * }); - * - * @see https://nightwatchjs.org/guide/network-requests/mock-network-response.html - */ - mockNetworkResponse( - urlToIntercept: string, - response?: { - status?: Protocol.Fetch.FulfillRequestRequest['responseCode']; - headers?: { [name: string]: string }; - body?: Protocol.Fetch.FulfillRequestRequest['body']; - }, - callback?: ( - this: NightwatchAPI, - result: NightwatchCallbackResult - ) => void - ): Awaitable; - /** * Override device mode/dimensions. * @@ -1576,6 +1513,12 @@ export interface ChromiumClientCommands { ) => void ): Awaitable; + captureNetworkRequests: NetworkNsCommands['captureRequests']; + + mockNetworkResponse: NetworkNsCommands['mockResponse']; + + setNetworkConditions: NetworkNsCommands['setConditions']; + /** * Listen to the `console` events (ex. `console.log` event) and * register callback to process the same. @@ -5214,6 +5157,103 @@ export interface FirefoxNsCommands { uninstallAddon(addonId: string | PromiseLike): Awaitable, null>; } +export interface NetworkNsCommands { + /** + * Capture outgoing network calls from the browser. + * + * @example + * describe('capture network requests', function() { + * it('captures and logs network requests as they occur', function(this: ExtendDescribeThis<{requestCount: number}>) { + * this.requestCount = 1; + * browser + * .network.captureRequests((requestParams) => { + * console.log('Request Number:', this.requestCount!++); + * console.log('Request URL:', requestParams.request.url); + * console.log('Request method:', requestParams.request.method); + * console.log('Request headers:', requestParams.request.headers); + * }) + * .navigateTo('https://www.google.com'); + * }); + * }); + * + * @see https://nightwatchjs.org/guide/network-requests/capture-network-calls.html + */ + captureRequests( + onRequestCallback: ( + requestParams: Protocol.Network.RequestWillBeSentEvent + ) => void, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable, null>; + + /** + * Intercept the request made on a particular URL and mock the response. + * + * @example + * describe('mock network response', function() { + * it('intercepts the request made to Google search and mocks its response', function() { + * browser + * .network.mockResponse('https://www.google.com/', { + * status: 200, + * headers: { + * 'Content-Type': 'UTF-8' + * }, + * body: 'Hello there!' + * }) + * .navigateTo('https://www.google.com/') + * .pause(2000); + * }); + * }); + * + * @see https://nightwatchjs.org/guide/network-requests/mock-network-response.html + */ + mockResponse( + urlToIntercept: string, + response?: { + status?: Protocol.Fetch.FulfillRequestRequest['responseCode']; + headers?: { [name: string]: string }; + body?: Protocol.Fetch.FulfillRequestRequest['body']; + }, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable, null>; + + /** + * Command to set Chrome network emulation settings. + * + * @example + * describe('set network conditions', function() { + * it('sets the network conditions',function() { + * browser + * .network.setConditions({ + * offline: false, + * latency: 3000, + * download_throughput: 500 * 1024, + * upload_throughput: 500 * 1024 + * }); + * }); + * }); + * + * @see https://nightwatchjs.org/api/setNetworkConditions.html + */ + setConditions( + spec: { + offline: boolean; + latency: number; + download_throughput: number; + upload_throughput: number; + }, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable, null>; +} + export interface AlertsNsCommands { /** * Accepts the currently displayed alert dialog. Usually, this is equivalent to clicking on the 'OK' button in the dialog. @@ -6040,34 +6080,6 @@ export interface WebDriverProtocolSessions { result: NightwatchCallbackResult ) => void ): Awaitable; - - /** - * Command to set Chrome network emulation settings. - * - * @example - * this.demoTest = function() { - * browser.setNetworkConditions({ - * offline: false, - * latency: 50000, - * download_throughput: 450 * 1024, - * upload_throughput: 150 * 1024 - * }); - * }; - * - * @see https://nightwatchjs.org/api/setNetworkConditions.html - */ - setNetworkConditions( - spec: { - offline: boolean; - latency: number; - download_throughput: number; - upload_throughput: number; - }, - callback?: ( - this: NightwatchAPI, - result: NightwatchCallbackResult - ) => void - ): Awaitable; } export interface WebDriverProtocolNavigation { diff --git a/types/tests/chromiumClientCommands.test-d.ts b/types/tests/chromiumClientCommands.test-d.ts index 6feb7641bc..a0ec87531f 100644 --- a/types/tests/chromiumClientCommands.test-d.ts +++ b/types/tests/chromiumClientCommands.test-d.ts @@ -97,6 +97,41 @@ describe('capture network requests', function () { expectType(result); }); + + it('captures and logs network requests as they occur', function (this: ExtendDescribeThis<{ requestCount: number }>) { + this.requestCount = 1; + browser + .network.captureRequests((requestParams) => { + console.log('Request Number:', this.requestCount!++); + console.log('Request URL:', requestParams.request.url); + console.log('Request method:', requestParams.request.method); + console.log('Request headers:', requestParams.request.headers); + }) + .navigateTo('https://www.google.com'); + }); + + it('tests different ways of using captureRequests', () => { + // with all parameters + browser.network.captureRequests( + (requestParams) => { + console.log('Request URL:', requestParams.request.url); + console.log('Request method:', requestParams.request.method); + console.log('Request headers:', requestParams.request.headers); + }, + function (result) { + expectType(this); + // without any parameter + expectError(this.network.captureRequests()) + console.log(result.value); + } + ); + }); + + it('tests captureRequests with async', async () => { + const result = await browser.network.captureRequests(() => {}); + + expectType(result); + }); }); // @@ -152,6 +187,197 @@ describe('mock network response', function () { expectType(result); }); + + it('intercepts the request made to Google search and mocks its response', function () { + browser + .network.mockResponse('https://www.google.com/', { + status: 200, + headers: { + 'Content-Type': 'UTF-8', + }, + body: 'Hello there!', + }) + .navigateTo('https://www.google.com/') + .pause(2000); + }); + + it('tests different ways of using mockNetworkResponse', () => { + // with all parameters + browser.network.mockResponse( + 'https://www.google.com/', + { + status: 200, + headers: { + 'Content-Type': 'UTF-8', + }, + body: 'Hello there!', + }, + function (result) { + expectType(this); + // without any parameter (invalid) + expectError(this.network.mockResponse()) + console.log(result.value); + } + ); + + // with no response + browser.network.mockResponse('https://www.google.com/'); + + // with empty response + browser.network.mockResponse('https://www.google.com/', {}); + + // with just one parameter + browser.network.mockResponse('https://www.google.com/', { + body: 'Hello there!', + }); + }); + + it('tests mockResponse with async', async () => { + const result = await browser.network.mockResponse('https://www.google.com/'); + + expectType(result); + }); +}); + +// +//.setNetworkConditions +// +describe('set network conditions', function () { + it('sets the network conditions', function () { + browser + .setNetworkConditions({ + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }) + .navigateTo('https://www.google.com') + .pause(2000) + }); + + it('tests different ways of using setNetworkConditions', () => { + // with all parameters + browser.setNetworkConditions( + { + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }, + function (result) { + expectType(this); + // without any parameter (resets the network conditions) + // without any parameter (invalid) + expectError(this.setNetworkConditions()) + // missing 'offline' parameter + expectError(this.setNetworkConditions({ + latency: 3000, + download_throughput: 500 * 1024, + upload_throughput: 500 * 1024, + })); + // missing 'latency' parameter + expectError(this.setNetworkConditions({ + offline: false, + download_throughput: 500 * 1024, + upload_throughput: 500 * 1024, + })); + // missing 'download_throughput' parameter + expectError(this.setNetworkConditions({ + offline: false, + latency: 3000, + upload_throughput: 500 * 1024, + })); + // missing 'upload_throughput' parameter + expectError(this.setNetworkConditions({ + offline: false, + latency: 3000, + download_throughput: 500 * 1024, + })); + + console.log(result.value); + } + ); + + }); + + it('tests setNetworkConditions with async', async () => { + const result = await browser.setNetworkConditions({ + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }); + + expectType(result); + }); + + it('sets the network conditions', function () { + browser + .network.setConditions({ + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }) + .navigateTo('https://www.google.com') + .pause(2000) + }); + + it('tests different ways of using setNetworkConditions', () => { + // with all parameters + browser.network.setConditions( + { + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }, + function (result) { + expectType(this); + // without any parameter (resets the network conditions) + // without any parameter (invalid) + expectError(this.network.setConditions()) + // missing 'offline' parameter + expectError(this.network.setConditions({ + latency: 3000, + download_throughput: 500 * 1024, + upload_throughput: 500 * 1024, + })); + // missing 'latency' parameter + expectError(this.network.setConditions({ + offline: false, + download_throughput: 500 * 1024, + upload_throughput: 500 * 1024, + })); + // missing 'download_throughput' parameter + expectError(this.network.setConditions({ + offline: false, + latency: 3000, + upload_throughput: 500 * 1024, + })); + // missing 'upload_throughput' parameter + expectError(this.network.setConditions({ + offline: false, + latency: 3000, + download_throughput: 500 * 1024, + })); + + console.log(result.value); + } + ); + + }); + + it('tests setConditions with async', async () => { + const result = await browser.network.setConditions({ + offline: false, + latency: 3000, // Additional latency (ms). + download_throughput: 500 * 1024, // Maximal aggregated download throughput. + upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + }); + + expectType(result); + }); }); //