From 86b708b7ee3bdf344b206adad386a0426186ab3d Mon Sep 17 00:00:00 2001 From: Nikulin Date: Wed, 8 Apr 2026 13:02:47 +0200 Subject: [PATCH 1/4] Asterio Bid Adapter: add initial bid adapter --- modules/asterioBidAdapter.js | 97 ++++++++++ modules/asterioBidAdapter.md | 29 +++ test/spec/modules/asterioBidAdapter_spec.js | 200 ++++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 modules/asterioBidAdapter.js create mode 100644 modules/asterioBidAdapter.md create mode 100644 test/spec/modules/asterioBidAdapter_spec.js diff --git a/modules/asterioBidAdapter.js b/modules/asterioBidAdapter.js new file mode 100644 index 00000000000..27dceb7513e --- /dev/null +++ b/modules/asterioBidAdapter.js @@ -0,0 +1,97 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepClone } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'asterio'; +export const ENDPOINT = 'https://bid.asterio.ai/prebid/bid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.adUnitToken); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const bids = validBidRequests.map(bidRequest => ({ + bidId: bidRequest.bidId, + adUnitToken: bidRequest.params.adUnitToken, + pos: bidRequest.params.pos, + sizes: prepareSizes(bidRequest.sizes) + })); + + const payload = { + requestId: bidderRequest.bidderRequestId, + bids, + referer: bidderRequest.refererInfo?.page, + schain: validBidRequests[0]?.ortb2?.source?.ext?.schain + }; + + if (bidderRequest?.gdprConsent) { + payload.gdprConsent = { + consentRequired: typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' ? bidderRequest.gdprConsent.gdprApplies : false, + consentString: bidderRequest.gdprConsent.consentString + }; + } + + return { + method: 'POST', + url: validBidRequests[0]?.params?.endpoint || ENDPOINT, + data: payload, + options: { + contentType: 'application/json', + customHeaders: { + 'Rtb-Direct': true + } + } + }; + }, + + interpretResponse: function (serverResponse, _request) { + const serverBody = serverResponse.body; + if (!serverBody || typeof serverBody !== 'object' || !Array.isArray(serverBody.bids)) { + return []; + } + + return serverBody.bids.map(bidResponse => { + const bid = deepClone(bidResponse); + + bid.cpm = parseFloat(bidResponse.cpm); + bid.requestId = bidResponse.requestId; + bid.ad = bidResponse.ad; + bid.width = bidResponse.width; + bid.height = bidResponse.height; + bid.currency = bidResponse.currency || 'USD'; + bid.netRevenue = typeof bidResponse.netRevenue === 'boolean' ? bidResponse.netRevenue : true; + bid.ttl = bidResponse.ttl; + bid.creativeId = bidResponse.creativeId; + bid.mediaType = bidResponse.mediaType || bidResponse.format || 'banner'; + + if (VIDEO === bid.mediaType && bidResponse.ad) { + bid.vastXml = bidResponse.ad; + } + + bid.meta = {}; + bid.meta.advertiserDomains = bid.adomain || []; + + return bid; + }); + }, + + onBidWon: function (bid) { + if (bid.winUrl) { + const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); + ajax(winUrl, null); + return true; + } + return false; + } +}; + +function prepareSizes(sizes) { + return sizes ? sizes.map(size => ({ width: size[0], height: size[1] })) : []; +} + +registerBidder(spec); diff --git a/modules/asterioBidAdapter.md b/modules/asterioBidAdapter.md new file mode 100644 index 00000000000..0c1be7f5e41 --- /dev/null +++ b/modules/asterioBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +``` +Module Name: Asterio Bidder Adapter +Module Type: Bidder Adapter +Maintainer: mnikulin@asteriosoft.com +``` + +# Description + +Connects to Asterio Bidder for bids. +Asterio bid adapter supports Banner and Video ads. + +# Test Parameters +``` +const adUnits = [ + { + bids: [ + { + bidder: 'asterio', + params: { + adUnitToken: '????????-????-????-????-????????????', // adUnitToken provided by Asterio + endpoint: 'https://bid.asterio.ai/prebid/bid' + } + } + ] + } +]; +``` diff --git a/test/spec/modules/asterioBidAdapter_spec.js b/test/spec/modules/asterioBidAdapter_spec.js new file mode 100644 index 00000000000..910c6fd61e8 --- /dev/null +++ b/test/spec/modules/asterioBidAdapter_spec.js @@ -0,0 +1,200 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as ajaxModule from 'src/ajax.js'; +import { ENDPOINT, spec } from 'modules/asterioBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +const REQUEST = { + bidId: '456', + bidder: 'asterio', + sizes: [[300, 250], [300, 600]], + params: { + adUnitToken: 'bbd6b4a6-66b8-479d-9527-e17899544693', + pos: 1 + }, + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'asteriosoft.com', + sid: 'publisher-1', + hp: 1 + }] + } + } + } + } +}; + +const BIDDER_BANNER_RESPONSE = { + bids: [{ + ad: '
test
', + requestId: 'request-1', + cpm: 1.23, + currency: 'USD', + width: 300, + height: 250, + ttl: 300, + creativeId: 'creative-1', + netRevenue: true, + winUrl: 'http://tracker.test/win?price=${AUCTION_PRICE}', + format: 'banner', + mediaType: 'banner', + adomain: ['example.com'] + }] +}; + +const BIDDER_VIDEO_RESPONSE = { + bids: [{ + ad: '', + requestId: 'request-2', + cpm: 2.34, + currency: 'USD', + width: 640, + height: 360, + ttl: 300, + creativeId: 'creative-2', + netRevenue: true, + winUrl: 'http://tracker.test/win?price=${AUCTION_PRICE}', + format: 'video', + mediaType: 'video', + adomain: ['video.example.com'] + }] +}; + +describe('asterioBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when adUnitToken is present', function () { + expect(spec.isBidRequestValid(REQUEST)).to.equal(true); + }); + + it('should return false when params are missing', function () { + const bid = { ...REQUEST, params: { pos: 1 } }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid({ ...REQUEST, params: null })).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should create a POST request to the direct prebid endpoint', function () { + const bidderRequest = spec.buildRequests([REQUEST], { + bidderRequestId: '123', + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' + }, + refererInfo: { + page: 'http://test.com/path.html' + } + }); + + expect(bidderRequest.method).to.equal('POST'); + expect(bidderRequest.url).to.equal(ENDPOINT); + expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }); + expect(bidderRequest.options.contentType).to.equal('application/json'); + expect(bidderRequest.data.requestId).to.equal('123'); + expect(bidderRequest.data.referer).to.equal('http://test.com/path.html'); + expect(bidderRequest.data.schain).to.deep.equal(REQUEST.ortb2.source.ext.schain); + expect(bidderRequest.data.bids).to.deep.equal([{ + bidId: '456', + adUnitToken: 'bbd6b4a6-66b8-479d-9527-e17899544693', + pos: 1, + sizes: [{ width: 300, height: 250 }, { width: 300, height: 600 }] + }]); + expect(bidderRequest.data.gdprConsent).to.deep.equal({ + consentRequired: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' + }); + }); + + it('should allow endpoint override from bidder params', function () { + const bidderRequest = spec.buildRequests([{ + ...REQUEST, + params: { + ...REQUEST.params, + endpoint: 'https://bidder.adsp-dev.asteriosoft.com/prebid/request' + } + }], { + bidderRequestId: '123', + refererInfo: { + page: 'http://test.com/path.html' + } + }); + + expect(bidderRequest.url).to.equal('https://bidder.adsp-dev.asteriosoft.com/prebid/request'); + }); + }); + + describe('interpretResponse', function () { + it('should map banner bids from direct response', function () { + const result = spec.interpretResponse({ body: BIDDER_BANNER_RESPONSE }, {}); + + expect(result).to.have.lengthOf(1); + expect(result[0]).to.include({ + ad: '
test
', + requestId: 'request-1', + cpm: 1.23, + currency: 'USD', + width: 300, + height: 250, + ttl: 300, + creativeId: 'creative-1', + netRevenue: true, + winUrl: 'http://tracker.test/win?price=${AUCTION_PRICE}', + format: 'banner', + mediaType: 'banner' + }); + expect(result[0].meta.advertiserDomains).to.deep.equal(['example.com']); + }); + + it('should map video bids and expose vastXml', function () { + const result = spec.interpretResponse({ body: BIDDER_VIDEO_RESPONSE }, {}); + + expect(result).to.have.lengthOf(1); + expect(result[0].mediaType).to.equal('video'); + expect(result[0].vastXml).to.equal(''); + expect(result[0].meta.advertiserDomains).to.deep.equal(['video.example.com']); + }); + + it('should return empty array for invalid response body', function () { + expect(spec.interpretResponse({ body: undefined }, {})).to.deep.equal([]); + expect(spec.interpretResponse({ body: '' }, {})).to.deep.equal([]); + expect(spec.interpretResponse({ body: {} }, {})).to.deep.equal([]); + }); + }); + + describe('onBidWon', function () { + let ajaxStub; + + beforeEach(function () { + ajaxStub = sinon.stub(ajaxModule, 'ajax'); + }); + + afterEach(function () { + ajaxStub.restore(); + }); + + it('should fire win tracking request', function () { + const bidWonResult = spec.onBidWon(BIDDER_BANNER_RESPONSE.bids[0]); + + expect(bidWonResult).to.equal(true); + expect(ajaxStub.calledOnceWithExactly('http://tracker.test/win?price=1.23', null)).to.equal(true); + }); + + it('should return false when there is no winUrl', function () { + expect(spec.onBidWon({ cpm: 1.23 })).to.equal(false); + expect(ajaxStub.called).to.equal(false); + }); + }); +}); From f1c2864ffb5c7a344508eb190b18def104e7589c Mon Sep 17 00:00:00 2001 From: Denis Anoykin Date: Wed, 8 Apr 2026 21:49:25 +0200 Subject: [PATCH 2/4] Asterio Bid Adapter: corrected documentation --- modules/asterioBidAdapter.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/asterioBidAdapter.md b/modules/asterioBidAdapter.md index 0c1be7f5e41..93d4cf44d3d 100644 --- a/modules/asterioBidAdapter.md +++ b/modules/asterioBidAdapter.md @@ -20,7 +20,6 @@ const adUnits = [ bidder: 'asterio', params: { adUnitToken: '????????-????-????-????-????????????', // adUnitToken provided by Asterio - endpoint: 'https://bid.asterio.ai/prebid/bid' } } ] From 7a78ddd6a267354b050b19f78979b3f8dd59268d Mon Sep 17 00:00:00 2001 From: Nikulin Date: Thu, 30 Apr 2026 19:41:12 +0200 Subject: [PATCH 3/4] Asterio Bid Adapter: review fixes --- modules/asterioBidAdapter.js | 18 ++++-- modules/asterioBidAdapter.md | 7 +++ test/spec/modules/asterioBidAdapter_spec.js | 68 ++++++++++++++++++--- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/modules/asterioBidAdapter.js b/modules/asterioBidAdapter.js index 27dceb7513e..fb2fbcff313 100644 --- a/modules/asterioBidAdapter.js +++ b/modules/asterioBidAdapter.js @@ -1,5 +1,5 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { deepClone } from '../src/utils.js'; +import { deepAccess, deepClone } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { VIDEO } from '../src/mediaTypes.js'; @@ -18,7 +18,7 @@ export const spec = { const bids = validBidRequests.map(bidRequest => ({ bidId: bidRequest.bidId, adUnitToken: bidRequest.params.adUnitToken, - pos: bidRequest.params.pos, + pos: getPosition(bidRequest), sizes: prepareSizes(bidRequest.sizes) })); @@ -38,10 +38,10 @@ export const spec = { return { method: 'POST', - url: validBidRequests[0]?.params?.endpoint || ENDPOINT, + url: ENDPOINT, data: payload, options: { - contentType: 'application/json', + contentType: 'text/plain', customHeaders: { 'Rtb-Direct': true } @@ -91,7 +91,15 @@ export const spec = { }; function prepareSizes(sizes) { - return sizes ? sizes.map(size => ({ width: size[0], height: size[1] })) : []; + if (!Array.isArray(sizes) || sizes.length === 0) { + return []; + } + const normalizedSizes = typeof sizes[0] === 'number' ? [sizes] : sizes; + return normalizedSizes.map(size => ({ width: size[0], height: size[1] })); +} + +function getPosition(bidRequest) { + return bidRequest.params.pos ?? deepAccess(bidRequest, 'mediaTypes.banner.pos') ?? deepAccess(bidRequest, 'mediaTypes.video.pos'); } registerBidder(spec); diff --git a/modules/asterioBidAdapter.md b/modules/asterioBidAdapter.md index 93d4cf44d3d..f18b7156bd7 100644 --- a/modules/asterioBidAdapter.md +++ b/modules/asterioBidAdapter.md @@ -11,6 +11,13 @@ Maintainer: mnikulin@asteriosoft.com Connects to Asterio Bidder for bids. Asterio bid adapter supports Banner and Video ads. +# Bid Params + +| Name | Scope | Type | Description | +| ---- | ----- | ---- | ----------- | +| `adUnitToken` | required | String | Asterio ad unit token provided by Asterio. | +| `pos` | optional | Number | Ad position override. When omitted, the adapter uses `mediaTypes.banner.pos` or `mediaTypes.video.pos` from the ad unit. | + # Test Parameters ``` const adUnits = [ diff --git a/test/spec/modules/asterioBidAdapter_spec.js b/test/spec/modules/asterioBidAdapter_spec.js index 910c6fd61e8..d7702d3b3df 100644 --- a/test/spec/modules/asterioBidAdapter_spec.js +++ b/test/spec/modules/asterioBidAdapter_spec.js @@ -102,7 +102,7 @@ describe('asterioBidAdapter', function () { expect(bidderRequest.method).to.equal('POST'); expect(bidderRequest.url).to.equal(ENDPOINT); expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }); - expect(bidderRequest.options.contentType).to.equal('application/json'); + expect(bidderRequest.options.contentType).to.equal('text/plain'); expect(bidderRequest.data.requestId).to.equal('123'); expect(bidderRequest.data.referer).to.equal('http://test.com/path.html'); expect(bidderRequest.data.schain).to.deep.equal(REQUEST.ortb2.source.ext.schain); @@ -118,21 +118,73 @@ describe('asterioBidAdapter', function () { }); }); - it('should allow endpoint override from bidder params', function () { + it('should use banner mediaTypes pos when params pos is absent', function () { const bidderRequest = spec.buildRequests([{ ...REQUEST, params: { - ...REQUEST.params, - endpoint: 'https://bidder.adsp-dev.asteriosoft.com/prebid/request' + adUnitToken: REQUEST.params.adUnitToken + }, + mediaTypes: { + banner: { + pos: 0 + } } }], { - bidderRequestId: '123', - refererInfo: { - page: 'http://test.com/path.html' + bidderRequestId: '123' + }); + + expect(bidderRequest.data.bids[0].pos).to.equal(0); + }); + + it('should use video mediaTypes pos when params pos and banner pos are absent', function () { + const bidderRequest = spec.buildRequests([{ + ...REQUEST, + params: { + adUnitToken: REQUEST.params.adUnitToken + }, + mediaTypes: { + video: { + pos: 3 + } + } + }], { + bidderRequestId: '123' + }); + + expect(bidderRequest.data.bids[0].pos).to.equal(3); + }); + + it('should use params pos as an override', function () { + const bidderRequest = spec.buildRequests([{ + ...REQUEST, + params: { + ...REQUEST.params, + pos: 2 + }, + mediaTypes: { + banner: { + pos: 1 + }, + video: { + pos: 3 + } } + }], { + bidderRequestId: '123' + }); + + expect(bidderRequest.data.bids[0].pos).to.equal(2); + }); + + it('should support flat size tuples', function () { + const bidderRequest = spec.buildRequests([{ + ...REQUEST, + sizes: [640, 360] + }], { + bidderRequestId: '123' }); - expect(bidderRequest.url).to.equal('https://bidder.adsp-dev.asteriosoft.com/prebid/request'); + expect(bidderRequest.data.bids[0].sizes).to.deep.equal([{ width: 640, height: 360 }]); }); }); From 2e7b6d860765235a9f1d419b501171042aceaa46 Mon Sep 17 00:00:00 2001 From: Nikulin Date: Thu, 30 Apr 2026 22:53:27 +0200 Subject: [PATCH 4/4] Asterio Bid Adapter: migrating to typescript --- ...erioBidAdapter.js => asterioBidAdapter.ts} | 74 +++++++++++++++---- test/spec/modules/asterioBidAdapter_spec.js | 7 +- 2 files changed, 64 insertions(+), 17 deletions(-) rename modules/{asterioBidAdapter.js => asterioBidAdapter.ts} (57%) diff --git a/modules/asterioBidAdapter.js b/modules/asterioBidAdapter.ts similarity index 57% rename from modules/asterioBidAdapter.js rename to modules/asterioBidAdapter.ts index fb2fbcff313..313eb4420b5 100644 --- a/modules/asterioBidAdapter.js +++ b/modules/asterioBidAdapter.ts @@ -1,28 +1,72 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { type AdapterRequest, type BidderSpec, type ServerResponse, registerBidder } from '../src/adapters/bidderFactory.js'; import { deepAccess, deepClone } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; -import { VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import type { BidRequest } from '../src/adapterManager.js'; +import type { Size } from '../src/types/common.d.ts'; const BIDDER_CODE = 'asterio'; export const ENDPOINT = 'https://bid.asterio.ai/prebid/bid'; -export const spec = { +export type AsterioBidParams = { + adUnitToken: string; + pos?: number; +}; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: AsterioBidParams; + } +} + +type AsterioBidPayload = { + bidId: string; + adUnitToken: string; + pos?: number; + sizes: Array<{ width: number; height: number }>; +}; + +type AsterioServerBid = { + ad?: string; + requestId: string; + cpm: string | number; + currency?: string; + width: number; + height: number; + ttl: number; + creativeId: string; + netRevenue?: boolean; + mediaType?: string; + format?: string; + adomain?: string[]; +}; + +export const spec: BidderSpec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner', 'video'], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { return !!(bid.params && bid.params.adUnitToken); }, buildRequests: function (validBidRequests, bidderRequest) { - const bids = validBidRequests.map(bidRequest => ({ + const bids: AsterioBidPayload[] = validBidRequests.map(bidRequest => ({ bidId: bidRequest.bidId, adUnitToken: bidRequest.params.adUnitToken, pos: getPosition(bidRequest), sizes: prepareSizes(bidRequest.sizes) })); - const payload = { + const payload: { + requestId: string; + bids: AsterioBidPayload[]; + referer: string; + schain: unknown; + gdprConsent?: { + consentRequired: boolean; + consentString?: string; + }; + } = { requestId: bidderRequest.bidderRequestId, bids, referer: bidderRequest.refererInfo?.page, @@ -43,22 +87,22 @@ export const spec = { options: { contentType: 'text/plain', customHeaders: { - 'Rtb-Direct': true + 'Rtb-Direct': 'true' } } }; }, - interpretResponse: function (serverResponse, _request) { + interpretResponse: function (serverResponse: ServerResponse, _request: AdapterRequest) { const serverBody = serverResponse.body; if (!serverBody || typeof serverBody !== 'object' || !Array.isArray(serverBody.bids)) { return []; } - return serverBody.bids.map(bidResponse => { + return serverBody.bids.map((bidResponse: AsterioServerBid) => { const bid = deepClone(bidResponse); - bid.cpm = parseFloat(bidResponse.cpm); + bid.cpm = parseFloat(String(bidResponse.cpm)); bid.requestId = bidResponse.requestId; bid.ad = bidResponse.ad; bid.width = bidResponse.width; @@ -80,17 +124,17 @@ export const spec = { }); }, - onBidWon: function (bid) { + onBidWon: function (bid: { winUrl?: string; cpm: number }) { if (bid.winUrl) { - const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); - ajax(winUrl, null); + const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE}/, String(bid.cpm)); + ajax(winUrl, null, undefined, { keepalive: true }); return true; } return false; } }; -function prepareSizes(sizes) { +function prepareSizes(sizes: Size | Size[]) { if (!Array.isArray(sizes) || sizes.length === 0) { return []; } @@ -98,7 +142,7 @@ function prepareSizes(sizes) { return normalizedSizes.map(size => ({ width: size[0], height: size[1] })); } -function getPosition(bidRequest) { +function getPosition(bidRequest: BidRequest): number | undefined { return bidRequest.params.pos ?? deepAccess(bidRequest, 'mediaTypes.banner.pos') ?? deepAccess(bidRequest, 'mediaTypes.video.pos'); } diff --git a/test/spec/modules/asterioBidAdapter_spec.js b/test/spec/modules/asterioBidAdapter_spec.js index d7702d3b3df..c45ef42dd5b 100644 --- a/test/spec/modules/asterioBidAdapter_spec.js +++ b/test/spec/modules/asterioBidAdapter_spec.js @@ -101,7 +101,7 @@ describe('asterioBidAdapter', function () { expect(bidderRequest.method).to.equal('POST'); expect(bidderRequest.url).to.equal(ENDPOINT); - expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }); + expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': 'true' }); expect(bidderRequest.options.contentType).to.equal('text/plain'); expect(bidderRequest.data.requestId).to.equal('123'); expect(bidderRequest.data.referer).to.equal('http://test.com/path.html'); @@ -241,7 +241,10 @@ describe('asterioBidAdapter', function () { const bidWonResult = spec.onBidWon(BIDDER_BANNER_RESPONSE.bids[0]); expect(bidWonResult).to.equal(true); - expect(ajaxStub.calledOnceWithExactly('http://tracker.test/win?price=1.23', null)).to.equal(true); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal('http://tracker.test/win?price=1.23'); + expect(ajaxStub.firstCall.args[1]).to.equal(null); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ keepalive: true }); }); it('should return false when there is no winUrl', function () {