diff --git a/ads/google/a4a/test/test-google-ads-a4a-config.js b/ads/google/a4a/test/test-google-ads-a4a-config.js index 042179d30007..188f04a15bad 100644 --- a/ads/google/a4a/test/test-google-ads-a4a-config.js +++ b/ads/google/a4a/test/test-google-ads-a4a-config.js @@ -20,12 +20,29 @@ import { isInManualExperiment, } from '../traffic-experiments'; import {resetExperimentToggles_} from '../../../../src/experiments'; +import {installViewerService} from '../../../../src/service/viewer-impl'; +import {resetServiceForTesting} from '../../../../src/service'; +import {documentStateFor} from '../../../../src/document-state'; +import {platformFor} from '../../../../src/platform'; import * as sinon from 'sinon'; +const EXP_ID = 'EXP_ID'; +/** @type {!Branches} */ +const EXTERNAL_BRANCHES = { + control: 'EXT_CONTROL', + experiment: 'EXT_EXPERIMENT', +}; +/** @type {!Branches} */ +const INTERNAL_BRANCHES = { + control: 'INT_CONTROL', + experiment: 'INT_EXPERIMENT', +}; + describe('a4a_config', () => { let sandbox; let win; let rand; + let events; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -38,32 +55,34 @@ describe('a4a_config', () => { href: 'https://cdn.ampproject.org/fnord', pathname: '/fnord', origin: 'https://cdn.ampproject.org', + hash: '', }, document: { + hidden: false, cookie: null, + visibilityState: 'visible', + addEventListener: function(type, listener) { + events[type] = listener; + }, }, crypto: { subtle: true, webkitSubtle: true, }, + navigator: window.navigator, }; + events = {}; + platformFor(win); + documentStateFor(win); + installViewerService(win); }); afterEach(() => { resetExperimentToggles_(); // Clear saved, page-level experiment state. + resetServiceForTesting(win, 'viewer'); sandbox.restore(); }); - const EXP_ID = 'EXP_ID'; - const EXTERNAL_BRANCHES = { - control: 'EXT_CONTROL', - experiment: 'EXT_EXPERIMENT', - }; - const INTERNAL_BRANCHES = { - control: 'INT_CONTROL', - experiment: 'INT_EXPERIMENT', - }; - it('should attach expt ID and return true when expt is on', () => { rand.onFirstCall().returns(-1); // Force experiment on. rand.onSecondCall().returns(0.75); // Select second branch. @@ -107,7 +126,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called ever').to.be.false; + expect(rand).to.not.be.called; expect(element.getAttribute('data-experiment-id')).to.not.be.ok; }); @@ -119,7 +138,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called ever').to.be.false; + expect(rand).to.not.be.called; expect(element.getAttribute('data-experiment-id')).to.not.be.ok; }); @@ -155,7 +174,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.true; + expect(rand, 'rand called at least once').to.be.called; expect(element.getAttribute('data-experiment-id')).to.not.be.ok; }); @@ -167,7 +186,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.true; + expect(rand, 'rand called at least once').to.be.called; expect(element.getAttribute('data-experiment-id')).to.not.be.ok; }); @@ -181,7 +200,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.true; + expect(rand, 'rand called at least once').to.be.called; expect(element.getAttribute('data-experiment-id')).to.equal( INTERNAL_BRANCHES.experiment); }); @@ -196,7 +215,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.true; + expect(rand, 'rand called at least once').to.be.called; expect(element.getAttribute('data-experiment-id')).to.equal( INTERNAL_BRANCHES.experiment); }); @@ -211,7 +230,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.false; + expect(rand).to.not.be.called; expect(element.getAttribute('data-experiment-id')).to.equal( EXTERNAL_BRANCHES.experiment); }); @@ -227,7 +246,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.false; + expect(rand).to.not.be.called; expect(element.getAttribute('data-experiment-id')).to.equal( EXTERNAL_BRANCHES.control); }); @@ -243,7 +262,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.false; + expect(rand).to.not.be.called; expect(element.getAttribute('data-experiment-id')).to.not.be.ok; }); @@ -258,7 +277,7 @@ describe('a4a_config', () => { expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; expect(win.document.cookie).to.be.null; - expect(rand.called, 'rand called at least once').to.be.false; + expect(rand).to.not.be.called; expect(isInManualExperiment(element), 'element in manual experiment') .to.be.true; // And it shouldn't be in any *other* experiments. @@ -272,5 +291,100 @@ describe('a4a_config', () => { } }); }); +}); + +// These tests are separated because they need to invoke installViewerService +// within the test, rather than in the beforeEach(). +describe('a4a_config hash param parsing', () => { + let sandbox; + let win; + let rand; + let events; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + rand = sandbox.stub(Math, 'random'); + win = { + AMP_MODE: { + localDev: true, + }, + location: { + href: 'https://cdn.ampproject.org/fnord', + pathname: '/fnord', + origin: 'https://cdn.ampproject.org', + hash: '', + search: 'somewhere=over&the=rainbow', + }, + document: { + hidden: false, + cookie: null, + visibilityState: 'visible', + addEventListener: function(type, listener) { + events[type] = listener; + }, + }, + crypto: { + subtle: true, + webkitSubtle: true, + }, + navigator: window.navigator, + }; + events = {}; + platformFor(win); + documentStateFor(win); + }); + afterEach(() => { + resetExperimentToggles_(); // Clear saved, page-level experiment state. + resetServiceForTesting(win, 'viewer'); + sandbox.restore(); + }); + const hashBaseConditions = ['#exp=PARAM', + '#p=blarg&exp=PARAM', + '#p=blarg&exp=PARAM&s=987', + '#p=blarg&exp=zort:123,PARAM,spaz:987&s=987']; + + hashBaseConditions.forEach(hashBase => { + it(`should find viewer param when pattern is ${hashBase}`, () => { + win.location.hash = hashBase.replace('PARAM', 'a4a:-1'); + installViewerService(win); + // Ensure that internal branches aren't attached, even if the PRNG + // would normally trigger them. + rand.onFirstCall().returns(-1); + const element = document.createElement('div'); + // Should not register as 'A4A enabled', but should still attach the + // control experiment ID. + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(rand).to.not.be.called; + expect(isInManualExperiment(element), 'element in manual experiment') + .to.be.true; + // And it shouldn't be in any *other* experiments. + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, EXTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, INTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + }); + + it(`hash should trump search; pattern=${hashBase}`, () => { + win.location.search = hashBase.replace('PARAM', 'a4a:-1'); + win.location.hash = hashBase.replace('PARAM', 'a4a:2'); + installViewerService(win); + // Ensure that internal branches aren't attached, even if the PRNG + // would normally trigger them. + rand.onFirstCall().returns(-1); + const element = document.createElement('div'); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(rand).to.not.be.called; + expect(element.getAttribute('data-experiment-id')).to.equal( + EXTERNAL_BRANCHES.experiment); + }); + }); }); diff --git a/ads/google/a4a/traffic-experiments.js b/ads/google/a4a/traffic-experiments.js index 402484d7c02c..76d4b026cf4e 100644 --- a/ads/google/a4a/traffic-experiments.js +++ b/ads/google/a4a/traffic-experiments.js @@ -26,6 +26,7 @@ import {isGoogleAdsA4AValidEnvironment} from './utils'; import {isExperimentOn, toggleExperiment} from '../../../src/experiments'; import {dev} from '../../../src/log'; import {getMode} from '../../../src/mode'; +import {viewerFor} from '../../../src/viewer'; import {parseQueryString} from '../../../src/url'; /** @typedef {{string: {branches: !Branches}}} */ @@ -121,7 +122,8 @@ export function googleAdsIsA4AEnabled(win, element, experimentName, */ function maybeSetExperimentFromUrl(win, experimentName, controlBranchId, treatmentBranchId, manualId) { - const expParam = parseQueryString(win.location.search)['exp']; + const expParam = viewerFor(win).getParam('exp') || + parseQueryString(win.location.search)['exp']; if (!expParam) { return; }