Skip to content

Commit

Permalink
Impl, unit test, & manual test
Browse files Browse the repository at this point in the history
  • Loading branch information
Micajuine Ho committed Feb 19, 2020
1 parent 31c15e3 commit db28aad
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 20 deletions.
22 changes: 22 additions & 0 deletions extensions/amp-analytics/0.1/amp-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {expandTemplate} from '../../../src/string';
import {getMode} from '../../../src/mode';
import {installLinkerReaderService} from './linker-reader';
import {isArray, isEnumValue} from '../../../src/types';
import {isExperimentOn} from '../../../src/experiments';
import {isIframed} from '../../../src/dom';
import {isInFie} from '../../../src/iframe-helper';
import {toggle} from '../../../src/style';
Expand Down Expand Up @@ -105,6 +106,12 @@ export class AmpAnalytics extends AMP.BaseElement {

/** @private {?boolean} */
this.isInFie_ = null;

/** @private {boolean} */
this.multiSelectorVisibilityOn_ = isExperimentOn(
this.win,
'multi-selector-visibility-trigger'
);
}

/** @override */
Expand Down Expand Up @@ -360,6 +367,21 @@ export class AmpAnalytics extends AMP.BaseElement {
this.addTrigger_(trigger);
} else if (trigger['selector']) {
// Expand the selector using variable expansion.
if (this.multiSelectorVisibilityOn_) {
if (Array.isArray(trigger['selector'])) {
const selectorPromises = trigger['selector'].map(s => {
return this.variableService_.expandTemplate(
s,
expansionOptions,
this.element
);
});
return Promise.all(selectorPromises).then(selectors => {
trigger['selector'] = selectors;
this.addTrigger_(trigger);
});
}
}
return this.variableService_
.expandTemplate(
trigger['selector'],
Expand Down
64 changes: 44 additions & 20 deletions extensions/amp-analytics/0.1/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {dict, hasOwn} from '../../../src/utils/object';
import {getData} from '../../../src/event-helper';
import {getDataParamsFromAttributes} from '../../../src/dom';
import {isEnumValue, isFiniteNumber} from '../../../src/types';
import {isExperimentOn} from '../../../src/experiments';
import {startsWith} from '../../../src/string';

const SCROLL_PRECISION_PERCENT = 5;
Expand Down Expand Up @@ -1424,7 +1425,10 @@ export class VisibilityTracker extends EventTracker {
const waitForSpec = visibilitySpec['waitFor'];
let reportWhenSpec = visibilitySpec['reportWhen'];
let createReportReadyPromiseFunc = null;

const multiSelectorVisibilityOn = isExperimentOn(
this.root.ampdoc.win,
'multi-selector-visibility-trigger'
);
if (reportWhenSpec) {
userAssert(
!visibilitySpec['repeat'],
Expand Down Expand Up @@ -1468,6 +1472,29 @@ export class VisibilityTracker extends EventTracker {
);
}

const getUnlistenPromiseForSelector = (selector, selectionMethod) => {
return this.root
.getAmpElement(
context.parentElement || context,
selector,
selectionMethod
)
.then(element => {
return visibilityManagerPromise.then(
visibilityManager => {
return visibilityManager.listenElement(
element,
visibilitySpec,
this.getReadyPromise(waitForSpec, selector, element),
createReportReadyPromiseFunc,
this.onEvent_.bind(this, eventType, listener, element)
);
},
() => {}
);
});
};

let unlistenPromise;
// Root selectors are delegated to analytics roots.
if (!selector || selector == ':root' || selector == ':host') {
Expand All @@ -1494,31 +1521,28 @@ export class VisibilityTracker extends EventTracker {
// false missed searches.
const selectionMethod =
config['selectionMethod'] || visibilitySpec['selectionMethod'];
unlistenPromise = this.root
.getAmpElement(
context.parentElement || context,

if (multiSelectorVisibilityOn && Array.isArray(selector)) {
unlistenPromise = Promise.all(
selector.map(s => getUnlistenPromiseForSelector(s, selectionMethod))
);
} else {
unlistenPromise = getUnlistenPromiseForSelector(
selector,
selectionMethod
)
.then(element => {
return visibilityManagerPromise.then(
visibilityManager => {
return visibilityManager.listenElement(
element,
visibilitySpec,
this.getReadyPromise(waitForSpec, selector, element),
createReportReadyPromiseFunc,
this.onEvent_.bind(this, eventType, listener, element)
);
},
() => {}
);
});
);
}
}

return function() {
unlistenPromise.then(unlisten => {
unlisten();
if (multiSelectorVisibilityOn && Array.isArray(unlisten)) {
for (let i = 0; i < unlisten.length; i++) {
unlisten[i]();
}
} else {
unlisten();
}
});
};
}
Expand Down
69 changes: 69 additions & 0 deletions extensions/amp-analytics/0.1/test/test-amp-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import {installCryptoService} from '../../../../src/service/crypto-impl';
import {installUserNotificationManagerForTesting} from '../../../amp-user-notification/0.1/amp-user-notification';
import {instrumentationServiceForDocForTesting} from '../instrumentation';
import {toggleExperiment} from '../../../../src/experiments';

describes.realWin(
'amp-analytics',
Expand Down Expand Up @@ -817,6 +818,74 @@ describes.realWin(
});
});

describe('expand multi-selector visibility', () => {
beforeEach(() => {
toggleExperiment(win, 'multi-selector-visibility-trigger', true);
});

afterEach(() => {
toggleExperiment(win, 'multi-selector-visibility-trigger', false);
});

it('expands selector with config variable', () => {
const tracker = ins.root_.getTracker('visible', VisibilityTracker);
const addStub = env.sandbox.stub(tracker, 'add');
const analytics = getAnalyticsTag({
requests: {foo: 'https://example.test/bar'},
triggers: [
{on: 'visible', selector: ['${foo}', '${title}'], request: 'foo'},
],
vars: {foo: 'bar'},
});
return waitForNoSendRequest(analytics).then(() => {
expect(addStub).to.be.calledOnce;
const config = addStub.args[0][2];
expect(config['selector']).to.deep.equal(['bar', 'My Test Title']);
});
});

function selectorExpansionTest(selector) {
it('expand selector value: ' + selector, () => {
const tracker = ins.root_.getTracker('visible', VisibilityTracker);
const addStub = env.sandbox.stub(tracker, 'add');
const analytics = getAnalyticsTag({
requests: {foo: 'https://example.test/bar'},
triggers: [
{on: 'visible', selector: ['${foo}, ${bar}'], request: 'foo'},
],
vars: {foo: selector, bar: '123'},
});
return waitForNoSendRequest(analytics).then(() => {
expect(addStub).to.be.calledOnce;
const config = addStub.args[0][2];
expect(config['selector']).to.deep.equal([selector + ', 123']);
});
});
}

[
'.clazz',
'a, div',
'a .foo',
'a #foo',
'a > div',
'div + p',
'div ~ ul',
'[target=_blank]',
'[title~=flower]',
'[lang|=en]',
'a[href^="https"]',
'a[href$=".pdf"]',
'a[href="w3schools"]',
'a:active',
'p::after',
'p:first-child',
'p:lang(it)',
':not(p)',
'p:nth-child(2)',
].map(selectorExpansionTest);
});

describe('optout by function', () => {
beforeEach(() => {
env.sandbox.stub(AnalyticsConfig.prototype, 'loadConfig').returns(
Expand Down
118 changes: 118 additions & 0 deletions extensions/amp-analytics/0.1/test/test-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {AmpdocAnalyticsRoot} from '../analytics-root';
import {Deferred} from '../../../../src/utils/promise';
import {Signals} from '../../../../src/utils/signals';
import {macroTask} from '../../../../testing/yield';
import {toggleExperiment} from '../../../../src/experiments';

describes.realWin('Events', {amp: 1}, env => {
let win;
Expand All @@ -40,7 +41,9 @@ describes.realWin('Events', {amp: 1}, env => {
let handler;
let analyticsElement;
let target;
let target2;
let child;
let child2;

beforeEach(() => {
win = env.win;
Expand All @@ -55,9 +58,17 @@ describes.realWin('Events', {amp: 1}, env => {
target.classList.add('target');
win.document.body.appendChild(target);

target2 = win.document.createElement('div');
target2.classList.add('target2');
win.document.body.appendChild(target2);

child = win.document.createElement('div');
child.classList.add('child');
target.appendChild(child);

child2 = win.document.createElement('div');
child2.classList.add('child2');
target2.appendChild(child2);
});

describe('AnalyticsEventType', () => {
Expand Down Expand Up @@ -1864,6 +1875,113 @@ describes.realWin('Events', {amp: 1}, env => {
});
});

describe('multi selector visibility trigger', () => {
let targetSignals2;
let saveCallback2;
let eventsSpy;

beforeEach(() => {
toggleExperiment(win, 'multi-selector-visibility-trigger', true);

eventsSpy = env.sandbox.spy(tracker, 'onEvent_');

target2.classList.add('i-amphtml-element');
targetSignals2 = new Signals();
target2.signals = () => targetSignals2;

saveCallback2 = env.sandbox.match(arg => {
if (typeof arg == 'function') {
saveCallback2.callback = arg;
return true;
}
return false;
});
});

afterEach(() => {
toggleExperiment(win, 'multi-selector-visibility-trigger', false);
});

it('should fire event per selector', async () => {
const config = {visibilitySpec: {selector: ['.target', '.target2']}};
const unlisten = env.sandbox.spy();
const unlisten2 = env.sandbox.spy();
iniLoadTrackerMock.expects('getRootSignal').twice();
const readyPromise = Promise.resolve();
// const readyPromise2 = Promise.resolve();
iniLoadTrackerMock
.expects('getElementSignal')
.withExactArgs('ini-load', target)
.returns(readyPromise)
.once();
iniLoadTrackerMock
.expects('getElementSignal')
.withExactArgs('ini-load', target2)
.returns(readyPromise)
.once();
visibilityManagerMock
.expects('listenElement')
.withExactArgs(
target,
config.visibilitySpec,
readyPromise,
/* createReportReadyPromiseFunc */ null,
saveCallback
)
.returns(unlisten)
.once();
visibilityManagerMock
.expects('listenElement')
.withExactArgs(
target2,
config.visibilitySpec,
readyPromise,
/* createReportReadyPromiseFunc */ null,
saveCallback2
)
.returns(unlisten2)
.once();
// Dispose function
const res = tracker.add(
analyticsElement,
'visible',
config,
eventResolver
);
expect(res).to.be.a('function');
const unlistenReady = getAmpElementSpy.returnValues[0];
const unlistenReady2 = getAmpElementSpy.returnValues[1];
// #getAmpElement Promise
await unlistenReady;
await unlistenReady2;
// #assertMeasurable_ Promise
await macroTask();
await macroTask();
saveCallback.callback({totalVisibleTime: 10});
saveCallback2.callback({totalVisibleTime: 15});

// Testing that visibilty manager mock sends state to onEvent_
expect(eventsSpy.getCall(0).args[0]).to.equal('visible');
expect(eventsSpy.getCall(0).args[1]).to.equal(eventResolver);
expect(eventsSpy.getCall(0).args[2]).to.equal(target);
expect(eventsSpy.getCall(0).args[3]).to.deep.equal({
totalVisibleTime: 10,
});
expect(eventsSpy.getCall(1).args[0]).to.equal('visible');
expect(eventsSpy.getCall(1).args[1]).to.equal(eventResolver);
expect(eventsSpy.getCall(1).args[2]).to.equal(target2);
expect(eventsSpy.getCall(1).args[3]).to.deep.equal({
totalVisibleTime: 15,
});

expect(unlisten).to.not.be.called;
expect(unlisten2).to.not.be.called;
await res();
expect(unlisten).to.be.calledOnce;
expect(unlisten2).to.be.calledOnce;
});
});

it('should expand data params', function*() {
target.setAttribute('data-vars-foo', 'bar');

Expand Down
Loading

0 comments on commit db28aad

Please sign in to comment.