diff --git a/extensions/amp-analytics/0.1/amp-analytics.js b/extensions/amp-analytics/0.1/amp-analytics.js
index 38b26eb4a744..dd83212a7ddc 100644
--- a/extensions/amp-analytics/0.1/amp-analytics.js
+++ b/extensions/amp-analytics/0.1/amp-analytics.js
@@ -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';
@@ -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 */
@@ -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'],
diff --git a/extensions/amp-analytics/0.1/events.js b/extensions/amp-analytics/0.1/events.js
index 4a3702730c1d..0b3c97844437 100644
--- a/extensions/amp-analytics/0.1/events.js
+++ b/extensions/amp-analytics/0.1/events.js
@@ -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;
@@ -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'],
@@ -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') {
@@ -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();
+ }
});
};
}
diff --git a/extensions/amp-analytics/0.1/test/test-amp-analytics.js b/extensions/amp-analytics/0.1/test/test-amp-analytics.js
index 03589c519ed5..92513dd5e1a7 100644
--- a/extensions/amp-analytics/0.1/test/test-amp-analytics.js
+++ b/extensions/amp-analytics/0.1/test/test-amp-analytics.js
@@ -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',
@@ -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(
diff --git a/extensions/amp-analytics/0.1/test/test-events.js b/extensions/amp-analytics/0.1/test/test-events.js
index 108ad00e7850..779a605050bf 100644
--- a/extensions/amp-analytics/0.1/test/test-events.js
+++ b/extensions/amp-analytics/0.1/test/test-events.js
@@ -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;
@@ -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;
@@ -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', () => {
@@ -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');
diff --git a/test/manual/amp-analytics-visible-repeat.amp.html b/test/manual/amp-analytics-visible-repeat.amp.html
index 39c936213aca..de18753dea22 100644
--- a/test/manual/amp-analytics-visible-repeat.amp.html
+++ b/test/manual/amp-analytics-visible-repeat.amp.html
@@ -56,6 +56,23 @@
}
+
+
+