diff --git a/USAGE.md b/USAGE.md index e12c85e..0f5550e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -111,7 +111,8 @@ The `Watcher` constructor can be passed 3 different options: * `time` - The time threshold * `ratio` - The ratio threshold -* `rootMargin` - The [rootMargin](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin) in object form. +* `rootMargin` - The [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) in object form. +* `root` - The [root](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) element with respect to which we want to watch the target. By default it is window. ## Utility API diff --git a/src/index.ts b/src/index.ts index 38d9275..e772054 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,8 +46,11 @@ import { Frame } from './metal/index'; + import w from './metal/window-proxy'; +import { validateState } from './metal/window-proxy'; + export { on, off, @@ -58,7 +61,8 @@ export { SpanielTrackedElement, setGlobalEngine, getGlobalEngine, - w as __w__ + w as __w__, + validateState }; export function queryElement(el: Element, callback: (clientRect: ClientRect, frame: Frame) => void) { diff --git a/src/interfaces.ts b/src/interfaces.ts index 3fc88b0..277e15c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -23,6 +23,7 @@ export interface SpanielObserverInit { root?: SpanielTrackedElement; rootMargin?: DOMString | DOMMargin; // default: 0px threshold?: SpanielThreshold[]; // default: 0 + ALLOW_CACHED_SCHEDULER?: boolean; } export interface SpanielRecord { @@ -83,4 +84,5 @@ export interface IntersectionObserverInit { root?: SpanielTrackedElement; rootMargin?: DOMString; // default: 0px threshold?: number | number[]; // default: 0 + ALLOW_CACHED_SCHEDULER?: boolean; } \ No newline at end of file diff --git a/src/intersection-observer.ts b/src/intersection-observer.ts index 8238788..a42bbe1 100644 --- a/src/intersection-observer.ts +++ b/src/intersection-observer.ts @@ -132,14 +132,14 @@ export class SpanielIntersectionObserver implements IntersectionObserver { this.id = generateToken(); options.threshold = options.threshold || 0; this.rootMarginObj = rootMarginToDOMMargin(options.rootMargin || '0px'); - + this.root = options.root; if (Array.isArray(options.threshold)) { this.thresholds = >options.threshold; } else { this.thresholds = [options.threshold]; } - this.scheduler = new ElementScheduler(); + this.scheduler = new ElementScheduler(null, this.root, options.ALLOW_CACHED_SCHEDULER); } }; @@ -182,8 +182,8 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit export function generateEntry(frame: Frame, clientRect: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry { let { top, bottom, left, right } = clientRect; let rootBounds: ClientRect = { - left: rootMargin.left, - top: rootMargin.top, + left: frame.left + rootMargin.left, + top: frame.top + rootMargin.top, bottom: rootMargin.bottom, right: rootMargin.right, width: frame.width - (rootMargin.right + rootMargin.left), diff --git a/src/metal/interfaces.ts b/src/metal/interfaces.ts index 842f945..f7a6075 100644 --- a/src/metal/interfaces.ts +++ b/src/metal/interfaces.ts @@ -52,17 +52,30 @@ export interface FrameInterface { scrollLeft: number; width: number; height: number; + x: number; + y: number; + top: number; + left: number; } export interface MetaInterface { + scrollTop: number; + scrollLeft: number; width: number; height: number; - scrollLeft: number; - scrollTop: number; + x: number; + y: number; + top: number; + left: number; } -export interface OnWindowIsDirtyInterface { - fn: any; - scope: any; - id: string; +export interface SpanielClientRectInterface { + width: number; + height: number; + x: number; + y: number; + bottom: number; + top: number; + left: number; + right: number; } diff --git a/src/metal/scheduler.ts b/src/metal/scheduler.ts index 282945a..bfe13c6 100644 --- a/src/metal/scheduler.ts +++ b/src/metal/scheduler.ts @@ -14,7 +14,9 @@ import { SchedulerInterface, ElementSchedulerInterface, FrameInterface, - QueueInterface + QueueInterface, + MetaInterface, + SpanielClientRectInterface } from './interfaces'; import W from './window-proxy'; @@ -29,27 +31,66 @@ const TOKEN_SEED = 'xxxx'.replace(/[xy]/g, function(c) { }); let tokenCounter = 0; -function generateRandomToken() { - return Math.floor(Math.random() * (9999999 - 0o0)).toString(16); -} - export class Frame implements FrameInterface { constructor( public timestamp: number, public scrollTop: number, public scrollLeft: number, public width: number, - public height: number + public height: number, + public x: number, + public y: number, + public top: number, + public left: number ) {} - static generate(): Frame { + static generate(root: Element | Window = window): Frame { + const rootMeta = this.revalidateRootMeta(root); return new Frame( Date.now(), - W.meta.scrollTop, - W.meta.scrollLeft, - W.meta.width, - W.meta.height + rootMeta.scrollTop, + rootMeta.scrollLeft, + rootMeta.width, + rootMeta.height, + rootMeta.x, + rootMeta.y, + rootMeta.top, + rootMeta.left ); } + static revalidateRootMeta(root: any = window): MetaInterface { + let _rootMeta: MetaInterface = { + width: 0, + height: 0, + scrollTop: 0, + scrollLeft: 0, + x: 0, + y: 0, + top: 0, + left: 0 + }; + + // if root is dirty update the cached values + if (W.isDirty) { W.updateMeta(); } + + if (root === window) { + _rootMeta.height = W.meta.height; + _rootMeta.width = W.meta.width; + _rootMeta.scrollLeft = W.meta.scrollLeft; + _rootMeta.scrollTop = W.meta.scrollTop; + }else if (root) { + let _clientRect = getBoundingClientRect(root); + _rootMeta.scrollTop = root.scrollTop; + _rootMeta.scrollLeft = root.scrollLeft; + _rootMeta.width = _clientRect.width; + _rootMeta.height = _clientRect.height; + _rootMeta.x = _clientRect.x; + _rootMeta.y = _clientRect.y; + _rootMeta.top = _clientRect.top; + _rootMeta.left = _clientRect.left; + } + + return _rootMeta; + } } export function generateToken() { @@ -57,17 +98,21 @@ export function generateToken() { } export abstract class BaseScheduler { + protected root: Element | Window; protected engine: EngineInterface; protected queue: QueueInterface; protected isTicking: Boolean = false; protected toRemove: Array = []; + protected id?: string; - constructor(customEngine?: EngineInterface) { + constructor(customEngine?: EngineInterface, root: Element | Window = window) { if (customEngine) { this.engine = customEngine; } else { this.engine = getGlobalEngine(); } + + this.root = root; } protected abstract applyQueue(frame: Frame): void; @@ -81,8 +126,7 @@ export abstract class BaseScheduler { } this.toRemove = []; } - - this.applyQueue(Frame.generate()); + this.applyQueue(Frame.generate(this.root)); this.engine.scheduleRead(this.tick.bind(this)); } } @@ -97,7 +141,7 @@ export abstract class BaseScheduler { let frame: Frame = null; this.engine.scheduleRead(() => { clientRect = getBoundingClientRect(el); - frame = Frame.generate(); + frame = Frame.generate(this.root); }); this.engine.scheduleWork(() => { callback(clientRect, frame); @@ -108,7 +152,6 @@ export abstract class BaseScheduler { } unwatchAll() { this.queue.clear(); - W.__destroy__(); } startTicking() { if (!this.isTicking) { @@ -140,7 +183,7 @@ export class Scheduler extends BaseScheduler implements SchedulerInterface { export class PredicatedScheduler extends Scheduler implements SchedulerInterface { predicate: (frame: Frame) => Boolean; constructor(predicate: (frame: Frame) => Boolean) { - super(null); + super(null, window); this.predicate = predicate; } applyQueue(frame: Frame) { @@ -152,13 +195,17 @@ export class PredicatedScheduler extends Scheduler implements SchedulerInterface export class ElementScheduler extends BaseScheduler implements ElementSchedulerInterface { protected queue: DOMQueue; - protected isDirty: boolean = false; - protected id: string = ''; + protected lastVersion: number = W.version; + protected ALLOW_CACHED_SCHEDULER: boolean; - constructor(customEngine?: EngineInterface) { - super(customEngine); + constructor(customEngine?: EngineInterface, root?: Element | Window, ALLOW_CACHED_SCHEDULER: boolean = false) { + super(customEngine, root); this.queue = new DOMQueue(); - this.id = generateRandomToken(); + this.ALLOW_CACHED_SCHEDULER = ALLOW_CACHED_SCHEDULER; + } + + get isDirty(): boolean { + return W.version !== this.lastVersion; } applyQueue(frame: Frame) { @@ -169,14 +216,19 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI clientRect = this.queue.items[i].clientRect = getBoundingClientRect(el); } + // FLAG WHICH WILL EVENTUALLY BE REMOVED + // THE EXPERIMENTAL FLAG DEFAULTS TO OFF + if (!this.ALLOW_CACHED_SCHEDULER) { + clientRect = this.queue.items[i].clientRect = getBoundingClientRect(el); + } + callback(frame, id, clientRect); } - this.isDirty = false; + this.lastVersion = W.version; } watch(el: Element, callback: (frame: FrameInterface, id: string, clientRect?: ClientRect | null) => void, id?: string): string { - this.initWindowIsDirtyListeners(); this.startTicking(); id = id || generateToken(); let clientRect = null; @@ -189,14 +241,6 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI }); return id; } - - initWindowIsDirtyListeners() { - W.onWindowIsDirtyListeners.push({ fn: this.windowIsDirtyHandler, scope: this, id: this.id }); - } - - windowIsDirtyHandler() { - this.isDirty = true; - } } let globalScheduler: Scheduler = null; diff --git a/src/metal/window-proxy.ts b/src/metal/window-proxy.ts index c97d7b2..faa595e 100644 --- a/src/metal/window-proxy.ts +++ b/src/metal/window-proxy.ts @@ -9,10 +9,8 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ -// detect the presence of DOM import { - MetaInterface, - OnWindowIsDirtyInterface + MetaInterface } from './interfaces'; const nop = () => 0; @@ -26,88 +24,65 @@ interface WindowProxy { getWidth: Function; rAF: Function; meta: MetaInterface; - onWindowIsDirtyListeners: OnWindowIsDirtyInterface[]; - __destroy__: Function; + version: number; + lastVersion: number; + updateMeta: Function; + isDirty: boolean; } const hasDOM = !!((typeof window !== 'undefined') && window && (typeof document !== 'undefined') && document); const hasRAF = hasDOM && !!window.requestAnimationFrame; -const throttleDelay: number = 30; -let resizeTimeout: number = 0; -let scrollTimeout: number = 0; let W: WindowProxy = { - hasRAF, hasDOM, + hasRAF, getScrollTop: nop, getScrollLeft: nop, getHeight: nop, getWidth: nop, - onWindowIsDirtyListeners: [], rAF: hasRAF ? window.requestAnimationFrame.bind(window) : (callback: Function) => { callback(); }, meta: { width: 0, height: 0, scrollTop: 0, - scrollLeft: 0 + scrollLeft: 0, + x: 0, + y: 0, + top: 0, + left: 0 }, - __destroy__() { - this.onWindowIsDirtyListeners = []; + version: 0, + lastVersion: 0, + updateMeta: nop, + get isDirty(): boolean { + return W.version !== W.lastVersion; } }; +export function validateState() { + ++W.version; +} + +// Init after DOM Content has loaded function hasDomSetup() { let se = (document).scrollingElement != null; W.getScrollTop = se ? () => (document).scrollingElement.scrollTop : () => (window).scrollY; W.getScrollLeft = se ? () => (document).scrollingElement.scrollLeft : () => (window).scrollX; } -// Memoize window meta dimensions -function windowSetDimensionsMeta() { - W.meta.height = W.getHeight(); - W.meta.width = W.getWidth(); -} - -function windowSetScrollMeta() { - W.meta.scrollLeft = W.getScrollLeft(); - W.meta.scrollTop = W.getScrollTop(); -} - -// Only invalidate window dimensions on resize -function resizeThrottle() { - window.clearTimeout(resizeTimeout); - - resizeTimeout = window.setTimeout(() => { - windowSetDimensionsMeta(); - }, throttleDelay); - - W.onWindowIsDirtyListeners.forEach((obj) => { - let { fn, scope } = obj; - fn.call(scope); - }); -} - -// Only invalidate window scroll on scroll -function scrollThrottle() { - window.clearTimeout(scrollTimeout); - - scrollTimeout = window.setTimeout(() => { - windowSetScrollMeta(); - }, throttleDelay); - - W.onWindowIsDirtyListeners.forEach((obj) => { - let { fn, scope } = obj; - fn.call(scope); - }); -} - if (hasDOM) { // Set the height and width immediately because they will be available at this point W.getHeight = () => (window).innerHeight; W.getWidth = () => (window).innerWidth; + W.updateMeta = () => { + W.meta.height = W.getHeight(); + W.meta.width = W.getWidth(); + W.meta.scrollLeft = W.getScrollLeft(); + W.meta.scrollTop = W.getScrollTop(); + W.lastVersion = W.version; + }; - windowSetDimensionsMeta(); - windowSetScrollMeta(); + W.updateMeta(); if ((document).readyState !== 'loading') { hasDomSetup(); @@ -115,8 +90,8 @@ if (hasDOM) { (document).addEventListener('DOMContentLoaded', hasDomSetup); } - window.addEventListener('resize', resizeThrottle, false); - window.addEventListener('scroll', scrollThrottle, false); + window.addEventListener('resize', validateState, false); + window.addEventListener('scroll', validateState, false); } export { diff --git a/src/native-watcher.ts b/src/native-watcher.ts index df07b8c..d5684bb 100644 --- a/src/native-watcher.ts +++ b/src/native-watcher.ts @@ -21,10 +21,13 @@ import { IntersectionObserverClass } from './interfaces'; +import W from './metal/window-proxy'; + export interface WatcherConfig { ratio?: number; time?: number; rootMargin?: DOMString | DOMMargin; + root?: SpanielTrackedElement; } export type EventName = 'impressed' | 'exposed' | 'visible' | 'impression-complete'; @@ -62,7 +65,7 @@ function onEntry(entries: SpanielObserverEntry[]) { export class Watcher { observer: SpanielObserver; constructor(ObserverClass: IntersectionObserverClass, config: WatcherConfig = {}) { - let { time, ratio, rootMargin } = config; + let { time, ratio, rootMargin, root } = config; let threshold: Threshold[] = [ { @@ -79,7 +82,7 @@ export class Watcher { ratio: ratio || 0 }); } - + if (ratio) { threshold.push({ label: 'visible', @@ -90,7 +93,8 @@ export class Watcher { this.observer = new SpanielObserver(ObserverClass, onEntry, { rootMargin, - threshold + threshold, + root }); } watch(el: Element, callback: WatcherCallback) { diff --git a/src/spaniel-observer.ts b/src/spaniel-observer.ts index b0e58e3..f471834 100644 --- a/src/spaniel-observer.ts +++ b/src/spaniel-observer.ts @@ -33,7 +33,6 @@ import { import w from './metal/window-proxy'; import { - FrameInterface, generateToken, on, off, @@ -48,7 +47,7 @@ export function DOMMarginToRootMargin(d: DOMMargin): DOMString { export class SpanielObserver implements SpanielObserverInterface { callback: (entries: SpanielObserverEntry[]) => void; - observer: IntersectionObserver; + observer: SpanielIntersectionObserver; thresholds: SpanielThreshold[]; recordStore: { [key: string]: SpanielRecord; }; queuedEntries: SpanielObserverEntry[]; @@ -61,7 +60,7 @@ export class SpanielObserver implements SpanielObserverInterface { this.queuedEntries = []; this.recordStore = {}; this.callback = callback; - let { root, rootMargin, threshold } = options; + let { root, rootMargin, threshold, ALLOW_CACHED_SCHEDULER } = options; rootMargin = rootMargin || '0px'; let convertedRootMargin: DOMString = typeof rootMargin !== 'string' ? DOMMarginToRootMargin(rootMargin) : rootMargin; this.thresholds = threshold.sort((t: SpanielThreshold) => t.ratio ); @@ -69,7 +68,8 @@ export class SpanielObserver implements SpanielObserverInterface { let o: IntersectionObserverInit = { root, rootMargin: convertedRootMargin, - threshold: this.thresholds.map((t: SpanielThreshold) => t.ratio) + threshold: this.thresholds.map((t: SpanielThreshold) => t.ratio), + ALLOW_CACHED_SCHEDULER }; this.observer = new SpanielIntersectionObserver((records: IntersectionObserverEntry[]) => this.internalCallback(records), o); @@ -244,8 +244,6 @@ export class SpanielObserver implements SpanielObserverInterface { off('unload', this.onWindowClosed); off('hide', this.onTabHidden); off('show', this.onTabShown); - - w.__destroy__(); } } unobserve(element: SpanielTrackedElement) { diff --git a/src/utils.ts b/src/utils.ts index 5d81ccd..5cd3b8e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { SpanielClientRectInterface } from './metal/interfaces'; + export function entrySatisfiesRatio(entry: IntersectionObserverEntry, threshold: number) { let { boundingClientRect, intersectionRatio } = entry; @@ -13,14 +15,22 @@ export function entrySatisfiesRatio(entry: IntersectionObserverEntry, threshold: } } -export function getBoundingClientRect(element: Element) { +export function getBoundingClientRect(element: Element): SpanielClientRectInterface { try { - return element.getBoundingClientRect(); + return element.getBoundingClientRect() as SpanielClientRectInterface; } catch (e) { - if (typeof e === 'object' && e !== null && e.description.split(' ')[0] === 'Unspecified' && (e.number & 0xFFFF) === 16389) { - return { top: 0, bottom: 0, left: 0, width: 0, height: 0, right: 0 }; + if (typeof e === 'object' && e !== null && (e.number & 0xFFFF) === 16389) { + return { top: 0, bottom: 0, left: 0, width: 0, height: 0, right: 0, x: 0, y: 0 }; } else { throw e; } } +} + +export function throttle(cb: Function, thottleDelay: number = 5, scope = window) { + let cookie: any; + return () => { + scope.clearTimeout(cookie); + cookie = scope.setTimeout(cb, thottleDelay); + }; } \ No newline at end of file diff --git a/src/watcher.ts b/src/watcher.ts index 271b281..6b7d432 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -20,10 +20,14 @@ import { SpanielTrackedElement } from './interfaces'; +import W from './metal/window-proxy'; + export interface WatcherConfig { ratio?: number; time?: number; rootMargin?: DOMString | DOMMargin; + root?: SpanielTrackedElement; + ALLOW_CACHED_SCHEDULER?: boolean; } export type EventName = 'impressed' | 'exposed' | 'visible' | 'impression-complete'; @@ -61,7 +65,7 @@ function onEntry(entries: SpanielObserverEntry[]) { export class Watcher { observer: SpanielObserver; constructor(config: WatcherConfig = {}) { - let { time, ratio, rootMargin } = config; + let { time, ratio, rootMargin, root, ALLOW_CACHED_SCHEDULER } = config; let threshold: Threshold[] = [ { @@ -89,8 +93,10 @@ export class Watcher { this.observer = new SpanielObserver(onEntry, { rootMargin, - threshold - }); + threshold, + root, + ALLOW_CACHED_SCHEDULER + }); } watch(el: Element, callback: WatcherCallback) { this.observer.observe(el, { diff --git a/test/app/index.html b/test/app/index.html index 1be9b58..2cf2771 100644 --- a/test/app/index.html +++ b/test/app/index.html @@ -4,9 +4,8 @@ -
- -
+
+
diff --git a/test/app/index.js b/test/app/index.js index b1f83e4..d9ac78c 100644 --- a/test/app/index.js +++ b/test/app/index.js @@ -9,7 +9,6 @@ Unless required by applicable law or agreed to in writing, software
distribute // Data structures that SpanielContext can assert against var ASSERTIONS = [[]]; var INDEX = 0; - var GLOBAL_TEST_EVENTS = { push: function(event) { ASSERTIONS[INDEX].push(event); @@ -44,8 +43,6 @@ for (var i = 0; i < elements.length; i++) { })(elements[i]); } -// END SpanielContext Harness - // Example usage of SpanielObserver var target = document.querySelector('.tracked-item[data-id="5"]'); let observer = new spaniel.SpanielObserver(function(changes) { @@ -56,6 +53,63 @@ let observer = new spaniel.SpanielObserver(function(changes) { label: 'impressed', ratio: 0.5, time: 1000 - }] + }], + ALLOW_CACHED_SCHEDULER: true }); observer.observe(target); + + +// !!! CUSTOM ROOT ELEMENT !!! // + + +// SpanielObserver with a custom root element +var root = document.getElementById('root'); + +// Watcher with a custom root element +window.rootWatcher = new spaniel.Watcher({ + time: 100, + ratio: 0.8, + root: root +}); + +var rootTarget = document.querySelector('.tracked-item-root[data-root-target-id="5"]'); +var rootObserver = new spaniel.SpanielObserver(function(changes) { + console.log(changes[0]); +}, { + root: root, + rootMargin: '0px 0px', + threshold: [{ + label: 'impressed', + ratio: 0.5, + time: 1000 + }], + ALLOW_CACHED_SCHEDULER: true +}); + +root.addEventListener('scroll', () => spaniel.validateState(), false); + +// Within the root keep an eye on this specific element +// ie Watcher > SpanielObserver > SpanielIntersectionObserver +rootObserver.observe(rootTarget); + +// List of elements within the root +var elements = document.getElementsByClassName('tracked-item-root'); + +for (var i = 0; i < elements.length; i++) { + (function(el) { + if (i < 6) { + var id = el.getAttribute('data-root-target-id'); + + // spaniel.Watcher.watch every one of these elements within the root + window.rootWatcher.watch(el, function(e, meta) { + var end = meta && meta.duration ? ' for ' + meta.duration + ' milliseconds' : ''; + console.log('custom root: '+id + ' ' + e + end); + GLOBAL_TEST_EVENTS.push({ + id: parseInt(id), + e: e, + meta: meta || {} + }); + }); + } + })(elements[i]); +} diff --git a/test/app/setup.js b/test/app/setup.js index 3091491..2ab964b 100644 --- a/test/app/setup.js +++ b/test/app/setup.js @@ -15,3 +15,14 @@ for (var i = 0; i < 100; i++) { app.appendChild(el); } +/* Root feature test div */ +var root = document.getElementById('root'); +for (var i = 0; i < 30; i++) { + var id = i + 1; + var content = Math.random(0, 100); + var el = document.createElement('div'); + el.setAttribute('class', 'tracked-item-root'); + el.setAttribute('data-root-target-id', id); + el.innerHTML = 'ID: ' + id + ' = ' + content; + root.appendChild(el); +} diff --git a/test/app/style.css b/test/app/style.css index 33fce74..c9e528e 100644 --- a/test/app/style.css +++ b/test/app/style.css @@ -3,14 +3,15 @@ html, body { padding: 0px; } -.tracked-item { +.tracked-item, .tracked-item-root { height: 100px; background-color: #EEE; margin: 0; padding: 0; + width: 150px; } -.tracked-item:nth-child(even) { +.tracked-item:nth-child(even), .tracked-item-root:nth-child(even) { background-color: #DDD; } @@ -39,3 +40,13 @@ nav { width: 100%; height: 100px; } +#app { + float: left; +} +#root { + height: 250px; + width: 165px; + overflow-y: scroll; + float: left; + margin-left: 15px; +} diff --git a/test/constants.js b/test/constants.js index dca6b9f..e47eddf 100644 --- a/test/constants.js +++ b/test/constants.js @@ -13,9 +13,12 @@ const constants = { NIGHTMARE: { TIMEOUT: 10, OPTIONS: { - show: false, - openDevTools: false, - waitTimeout: 0 + // show: true, + // openDevTools: { + // mode: 'detach' + // }, + // dock: true, + // closable: true } } }; diff --git a/test/headless/specs/intersection-observer.js b/test/headless/specs/intersection-observer.js index 15d6cd9..c651f81 100644 --- a/test/headless/specs/intersection-observer.js +++ b/test/headless/specs/intersection-observer.js @@ -230,4 +230,35 @@ testModule('IntersectionObserver', class extends TestClass { assert.equal(result, 6, 'Callback fired 6 times'); }); } + + ['@test observing an occluded element within a root and scrolling it into view should fire callbacks']() { + return this.context.evaluate(function() { + window.STATE.impressions = 0; + let root = document.getElementById('root'); + let rootTarget = document.querySelector('.tracked-item-root[data-root-target-id="5"]'); + let observer = new spaniel.IntersectionObserver(function() { + window.STATE.impressions++; + }, { + root: root, + threshold: 0.5 + }); + observer.observe(rootTarget); + root.addEventListener('scroll', () => spaniel.validateState(), false); + }) + .wait(100) + .evaluate(function() { + root.scrollTop = 350; + }) + .wait(100) + .evaluate(function() { + root.scrollTop = 0; + }) + .wait(100) + .getExecution() + .evaluate(function() { + return window.STATE.impressions; + }).then(function(result) { + assert.equal(result, 2, 'Callback fired twice'); + }); + } }); diff --git a/test/headless/specs/utilities.js b/test/headless/specs/utilities.js index 5bf22cd..d28e0ff 100644 --- a/test/headless/specs/utilities.js +++ b/test/headless/specs/utilities.js @@ -21,56 +21,22 @@ const { } = constants; testModule('Window Proxy', class extends TestClass { - ['@test destroy works']() { - return this.context.evaluate(() => { - window.watcher = new spaniel.Watcher(); - window.target = document.querySelector('.tracked-item[data-id="6"]'); - window.watcher.watch(window.target, () => {}); - }) - .wait(RAF_THRESHOLD * 5) - .evaluate(() => { - window.watcher.destroy(); - }) - .getExecution() - .evaluate(function() { - return spaniel.__w__.onWindowIsDirtyListeners.length; - }).then(function(result) { - assert.equal(result, 0, 'All window isDirty listeners have been removed'); - }); - } - ['@test can listen to window isDirty']() { - return this.context.evaluate(() => { - window.watcher = new spaniel.Watcher(); - window.target = document.querySelector('.tracked-item[data-id="6"]'); - window.watcher.watch(window.target, () => {}); - }) - .wait(RAF_THRESHOLD * 5) - .getExecution() - .evaluate(function() { - let id = window.watcher.observer.observer.scheduler.id; - return spaniel.__w__.onWindowIsDirtyListeners.filter(listener => listener.id === id); - }).then(function(result) { - assert.lengthOf(result, 1, 'This watcher is listening on window isDirty'); - }); - } ['@test window isDirty validation on scroll']() { return this.context.evaluate(() => { - window.testIsDirty = false; - spaniel.__w__.onWindowIsDirtyListeners.push({fn: () => { window.testIsDirty = true }, scope: window, id: 'foo1' }); + window.lastVersion = spaniel.__w__.version; }) .wait(RAF_THRESHOLD * 5) - .scrollTo(10) + .scrollTo(100) .getExecution() .evaluate(function() { - return window.testIsDirty; + return window.lastVersion !== spaniel.__w__.version; }).then(function(result) { assert.isTrue(result, 'The window isDirty'); }); } ['@test window isDirty validation on resize']() { return this.context.evaluate(() => { - window.testIsDirty = false; - spaniel.__w__.onWindowIsDirtyListeners.push({fn: () => { window.testIsDirty = true }, scope: window, id: 'foo2' }); + window.lastVersion = spaniel.__w__.version; }) .wait(RAF_THRESHOLD * 5) .viewport(VIEWPORT.WIDTH + 100, VIEWPORT.HEIGHT + 100) @@ -78,7 +44,7 @@ testModule('Window Proxy', class extends TestClass { .viewport(VIEWPORT.WIDTH, VIEWPORT.HEIGHT) .getExecution() .evaluate(function() { - return window.testIsDirty; + return window.lastVersion !== spaniel.__w__.version; }).then(function(result) { assert.isTrue(result, 'The window isDirty'); }); @@ -168,7 +134,7 @@ testModule('Eventing', class extends TestClass { window.STATE.scrollHandler = function() { window.STATE.scrollEvents++; }; - spaniel.on('scroll', window.STATE.scrollHandler); + spaniel.on('scroll', window.STATE.scrollHandler); }) .scrollTo(10) .wait(RAF_THRESHOLD * 5) diff --git a/test/headless/specs/watcher/impression-complete-event.spec.js b/test/headless/specs/watcher/impression-complete-event.spec.js index 6b7fa63..d36a84e 100644 --- a/test/headless/specs/watcher/impression-complete-event.spec.js +++ b/test/headless/specs/watcher/impression-complete-event.spec.js @@ -47,6 +47,7 @@ testModule('Impression Complete event', class extends WatcherTestClass { return this.context.scrollTo(200) .wait(IMPRESSION_THRESHOLD + RAF_THRESHOLD * 4) .scrollTo(0) + .wait(IMPRESSION_THRESHOLD) .assertOnce(ITEM_TO_OBSERVE, 'impression-complete') .done(); }