Skip to content

Commit

Permalink
Impl, unit tests, manual tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Micajuine Ho committed Feb 26, 2020
1 parent db28aad commit 9b8dc50
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 61 deletions.
57 changes: 52 additions & 5 deletions extensions/amp-analytics/0.1/analytics-root.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,45 @@ export class AnalyticsRoot {
});
}

/**
* @param {string} selector DOM query selector.
* @return {!Promise<!Array<!Element>>} Element corresponding to the selector.
*/
getElements_(selector) {
// Wait for document-ready to avoid false missed searches
return this.ampdoc.whenReady().then(() => {
const results = [];
const foundElements = this.getRoot().querySelectorAll(selector);

// DOM search can "look" outside the boundaries of the root, thus make
// sure the result is contained. Length is not supported in all browsers
if (foundElements && foundElements.length) {
for (let i = 0; i < foundElements.length; i++) {
if (this.contains(foundElements[i])) {
results.push(foundElements[i]);
}
}
}
userAssert(results.length, `Element "${selector}" not found`);
return results;
});
}

/**
* Searches for the AMP elements that matches from the root.
*
* @param {string} selector DOM query selector.
* @return {!Promise<!Array<!AmpElement>>} Array of AMP elements corresponding to the selector if found.
*/
getAmpElements(selector) {
return this.getElements_(selector).then(elements => {
for (let i = 0; i < elements.length; i++) {
this.verifyAmpElement_(elements[i], selector);
}
return elements;
});
}

/**
* Searches the AMP element that matches the selector within the scope of the
* analytics root in relationship to the specified context node.
Expand All @@ -287,15 +326,23 @@ export class AnalyticsRoot {
*/
getAmpElement(context, selector, selectionMethod) {
return this.getElement(context, selector, selectionMethod).then(element => {
userAssert(
element.classList.contains('i-amphtml-element'),
'Element "%s" is required to be an AMP element',
selector
);
this.verifyAmpElement_(element, selector);
return element;
});
}

/**
* @param {!Element} element
* @param {string} selector
*/
verifyAmpElement_(element, selector) {
userAssert(
element.classList.contains('i-amphtml-element'),
'Element "%s" is required to be an AMP element',
selector
);
}

/**
* Creates listener-filter for DOM events to check against the specified
* selector. If the node (or its ancestors) match the selector the listener
Expand Down
69 changes: 42 additions & 27 deletions extensions/amp-analytics/0.1/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -1472,28 +1472,19 @@ 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)
);
},
() => {}
const getUnlistenPromiseForElement = element =>
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.
Expand Down Expand Up @@ -1523,14 +1514,38 @@ export class VisibilityTracker extends EventTracker {
config['selectionMethod'] || visibilitySpec['selectionMethod'];

if (multiSelectorVisibilityOn && Array.isArray(selector)) {
unlistenPromise = Promise.all(
selector.map(s => getUnlistenPromiseForSelector(s, selectionMethod))
userAssert(
!selectionMethod,
'Cannot have selectionMethod defined with an array selector: ',
selector
);
unlistenPromise = Promise.all(
selector.map(individualSelector => {
return this.root.getAmpElements(individualSelector);
})
).then(selectorArrayOfElements => {
const uniqueElements = [];
for (let i = 0; i < selectorArrayOfElements.length; i++) {
for (let j = 0; j < selectorArrayOfElements[i].length; j++) {
if (
uniqueElements.indexOf(selectorArrayOfElements[i][j]) === -1
) {
uniqueElements.push(selectorArrayOfElements[i][j]);
}
}
}
return Promise.all(
uniqueElements.map(element => getUnlistenPromiseForElement(element))
);
});
} else {
unlistenPromise = getUnlistenPromiseForSelector(
selector,
selectionMethod
);
unlistenPromise = this.root
.getAmpElement(
context.parentElement || context,
selector,
selectionMethod
)
.then(element => getUnlistenPromiseForElement(element));
}
}

Expand Down
75 changes: 67 additions & 8 deletions extensions/amp-analytics/0.1/test/test-analytics-root.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,25 +333,84 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, env => {
addTestInstance(root3.getElement(body, '#target'), null);
});

it('should find an AMP element for AMP search', () => {
it('should find an AMP element for AMP search', async () => {
child.classList.add('i-amphtml-element');
return root.getAmpElement(body, '#child').then(element => {
expect(element).to.equal(child);
});
const element = await root.getAmpElement(body, '#child');
expect(element).to.equal(child);
});

it('should allow not-found element for AMP search', () => {
return root.getAmpElement(body, '#unknown').catch(error => {
it('should allow not-found element for AMP search', async () => {
await root.getAmpElement(body, '#unknown').catch(error => {
expect(error).to.match(/Element "#unknown" not found/);
});
});

it('should fail if the found element is not AMP for AMP search', () => {
it('should fail if the found element is not AMP for AMP search', async () => {
child.classList.remove('i-amphtml-element');
return root.getAmpElement(body, '#child').catch(error => {
await root.getAmpElement(body, '#child').catch(error => {
expect(error).to.match(/required to be an AMP element/);
});
});

describe('get amp elements', () => {
let child2;
let elements;
let error;

beforeEach(() => {
error = false;
child2 = win.document.createElement('child');
body.appendChild(child2);
child.classList.add('i-amphtml-element');
child2.classList.add('i-amphtml-element');
});

afterEach(() => {
if (!error) {
expect(elements).to.contain(child);
expect(elements).to.contain(child2);
expect(elements.length).to.equal(2);
}
});

it('should find elements by ID', async () => {
child.id = 'myId';
child2.id = 'myId';
elements = await root.getAmpElements('#myId');
});

it('should find element by class', async () => {
child.classList.add('myClass');
child2.classList.add('myClass');
elements = await root.getAmpElements('.myClass');
});

it('should find element by tag name', async () => {
elements = await root.getAmpElements('child');
});

it('should find element by selector', async () => {
child.id = 'myId';
child2.id = 'myId';
child.classList.add('myClass');
child2.classList.add('myClass');
elements = await root.getAmpElements('#myId.myClass');
});

it('should allow not-found element for AMP search', async () => {
error = true;
await root.getAmpElement(body, '#unknown').catch(error => {
expect(error).to.match(/Element "#unknown" not found/);
});
});

it('should fail if the found element is not AMP for AMP search', async () => {
error = true;
await root.getAmpElements('#child').catch(error => {
expect(error).to.match(/required to be an AMP element/);
});
});
});
});

describe('createSelectiveListener', () => {
Expand Down

0 comments on commit 9b8dc50

Please sign in to comment.