diff --git a/src/assets/javascripts/device/viewport.ts b/src/assets/javascripts/device/viewport.ts index bc7a78e221bf73be55cab01d55f9115faa515c81..dee0f361fecfc3d3aeb12fe09dc9ac3e01ab02ba 100644 --- a/src/assets/javascripts/device/viewport.ts +++ b/src/assets/javascripts/device/viewport.ts @@ -24,9 +24,12 @@ import { BehaviorSubject } from "rxjs/BehaviorSubject" import { Observable } from "rxjs/Observable" import "rxjs/add/observable/fromEvent" +import "rxjs/add/operator/distinctUntilChanged" import "rxjs/add/operator/map" import "rxjs/add/operator/merge" +import { isEqual, isUndefined } from "lodash" + /* ---------------------------------------------------------------------------- * Types * ------------------------------------------------------------------------- */ @@ -52,14 +55,18 @@ export interface ViewportSize { * ------------------------------------------------------------------------- */ /** - * Observable for window scroll events + * Create an observable for window scroll events + * + * @return Observable */ -export const scroll$ = Observable.fromEvent<UIEvent>(window, "scroll") +const scroll = () => Observable.fromEvent<UIEvent>(window, "scroll") /** - * Observable for window resize events + * Create an observable for window resize events + * + * @return Observable */ -export const resize$ = Observable.fromEvent<UIEvent>(window, "resize") +const resize = () => Observable.fromEvent<UIEvent>(window, "resize") /* ---------------------------------------------------------------------------- * Functions @@ -79,10 +86,7 @@ export const resize$ = Observable.fromEvent<UIEvent>(window, "resize") * leading to a duplicate emission. At the time of writing, this can be * observed in Chrome and Firefox, while Safari seems to handle it correctly. * - * Browser environments can be pretty unpredictable, so it's best to always - * create a fresh observable when necessary, e.g. when a breakpoint is hit, - * and to filter for duplicate values (or just accept that it could happen). - * For decision, context is necessary, so it must be done by the caller. + * For this reason we only emit consecutive distinct values. * * @return Subject */ @@ -91,14 +95,15 @@ export function offset() { x: window.pageXOffset, y: window.pageYOffset }) - scroll$ - .merge(resize$) + scroll() + .merge(resize()) .map<UIEvent, ViewportOffset>(() => ({ x: window.pageXOffset, y: window.pageYOffset })) .subscribe(subject$) return subject$ + .distinctUntilChanged((x, y) => !isUndefined(y) && isEqual(x, y)) } /** @@ -113,11 +118,12 @@ export function size() { width: window.innerWidth, height: window.innerHeight }) - resize$ + resize() .map<UIEvent, ViewportSize>(() => ({ width: window.innerWidth, height: window.innerHeight })) .subscribe(subject$) return subject$ + .distinctUntilChanged((x, y) => !isUndefined(y) && isEqual(x, y)) } diff --git a/tests/karma.conf.ts b/tests/karma.conf.ts index e807988505f1dd10222ca3a4ad2dcf9953704755..bf4fea8f9e273020b9598e71d05b1979f0bdb7f7 100644 --- a/tests/karma.conf.ts +++ b/tests/karma.conf.ts @@ -127,7 +127,8 @@ export default (config: KarmaConfig & KarmaConfigOptions) => { /* Configuration for spec reporter */ specReporter: { - suppressSkipped: true + suppressErrorSummary: true, + suppressSkipped: !config.singleRun }, /* Configuration for coverage reporter */ diff --git a/tests/unit/device/breakpoint.spec.ts b/tests/unit/device/breakpoint.spec.ts index 23dac9dd042fa0d29e5fdae4c90c053cdc8f81a0..ed0a7bfe29abae3460761185f7c0ca51114e20bc 100644 --- a/tests/unit/device/breakpoint.spec.ts +++ b/tests/unit/device/breakpoint.spec.ts @@ -20,7 +20,7 @@ * IN THE SOFTWARE. */ -import { BehaviorSubject } from "rxjs/BehaviorSubject" +import { Subject } from "rxjs/Subject" import "rxjs/add/operator/take" import "rxjs/add/operator/toArray" @@ -37,9 +37,12 @@ describe("device", () => { /* Breakpoint observable factories */ describe("breakpoint", () => { - /* Reset viewport after each test */ - beforeEach(() => { - viewport.set(400) + /* Set viewport in next animation frame to force rendering */ + beforeEach(done => { + requestAnimationFrame(() => { + viewport.set(480, 320) + done() + }) }) /* Reset viewport after each test */ @@ -51,12 +54,12 @@ describe("device", () => { describe(".query", () => { /* Media query for testing purposes */ - const media = window.matchMedia("(min-width: 400px)") + const media = window.matchMedia("(min-width: 480px)") /* Test: it should return a behavior subject */ it("should return a behavior subject", () => { const media$ = query(media) - expect(media$).toEqual(jasmine.any(BehaviorSubject)) + expect(media$).toEqual(jasmine.any(Subject)) }) /* Test: it should emit on subscription immediately */ @@ -64,8 +67,8 @@ describe("device", () => { const media$ = query(media) media$ .take(1) - .subscribe(active => { - expect(active).toBeTruthy() + .subscribe(initial => { + expect(initial).toEqual(true) done() }, done.fail) }) @@ -73,15 +76,17 @@ describe("device", () => { /* Test: it should emit on media query status change */ it("should emit on media query status change", done => { const media$ = query(media) - viewport.set(399) media$ .take(2) .toArray() .subscribe(([initial, changed]) => { - expect(initial).toBeTruthy() - expect(changed).toBeFalsy() + expect(initial).toEqual(true) + expect(changed).toEqual(false) done() }, done.fail) + requestAnimationFrame(() => { + viewport.set(479) + }) }) }) @@ -90,67 +95,72 @@ describe("device", () => { /* Test: it should return a behavior subject */ it("should return a behavior subject", () => { - const media$ = from(400) - expect(media$).toEqual(jasmine.any(BehaviorSubject)) + const media$ = from(480) + expect(media$).toEqual(jasmine.any(Subject)) }) /* Test: it should emit on subscription immediately */ it("should emit on subscription immediately", done => { - const media$ = from(400) + const media$ = from(480) media$ .take(1) - .subscribe(active => { - expect(active).toBeTruthy() + .subscribe(initial => { + expect(initial).toEqual(true) done() }, done.fail) }) /* Test: it should emit on media query status change */ it("should emit on media query status change", done => { - const media$ = from(400) - viewport.set(399) + const media$ = from(480) media$ .take(2) .toArray() .subscribe(([initial, changed]) => { - expect(initial).toBeTruthy() - expect(changed).toBeFalsy() + expect(initial).toEqual(true) + expect(changed).toEqual(false) done() }, done.fail) + requestAnimationFrame(() => { + viewport.set(479) + }) }) }) - /* Create a subject for a minimum media query */ + /* Create a subject for a maximum media query */ describe(".to", () => { /* Test: it should return a behavior subject */ it("should return a behavior subject", () => { - const media$ = to(399) - expect(media$).toEqual(jasmine.any(BehaviorSubject)) + const media$ = to(479) + expect(media$).toEqual(jasmine.any(Subject)) }) /* Test: it should emit on subscription immediately */ it("should emit on subscription immediately", done => { - const media$ = to(399) + const media$ = to(479) media$ - .take(1).subscribe(active => { - expect(active).toBeFalsy() + .take(1) + .subscribe(initial => { + expect(initial).toEqual(false) done() }, done.fail) }) /* Test: it should emit on media query status change */ it("should emit on media query status change", done => { - const media$ = to(399) - viewport.set(399) + const media$ = to(479) media$ .take(2) .toArray() .subscribe(([initial, changed]) => { - expect(initial).toBeFalsy() - expect(changed).toBeTruthy() + expect(initial).toEqual(false) + expect(changed).toEqual(true) done() }, done.fail) + requestAnimationFrame(() => { + viewport.set(479) + }) }) }) }) diff --git a/tests/unit/device/viewport.spec.ts b/tests/unit/device/viewport.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..49fdcc1cad3801540ffae9cfc6f2a4ad286aec98 --- /dev/null +++ b/tests/unit/device/viewport.spec.ts @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2017 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { Subject } from "rxjs/Subject" + +import "rxjs/add/operator/take" +import "rxjs/add/operator/toArray" + +import { offset, size } from "~/device/viewport" + +/* ---------------------------------------------------------------------------- + * Tests + * ------------------------------------------------------------------------- */ + +/* Device utilities */ +describe("device", () => { + + /* Viewport observable factories */ + describe("viewport", () => { + + /* Set viewport in next animation frame to force rendering */ + beforeEach(done => { + requestAnimationFrame(() => { + document.body.style.minHeight = `${window.innerHeight * 2}px` + viewport.set(480, 320) + done() + }) + }) + + /* Reset viewport after each test */ + afterEach(() => { + document.body.style.minHeight = undefined + viewport.reset() + }) + + /* Create a subject emitting changes in viewport offset */ + describe(".offset", () => { + + /* Test: it should return a behavior subject */ + it("should return a behavior subject", () => { + const offset$ = offset() + expect(offset$).toEqual(jasmine.any(Subject)) + }) + + /* Test: it should emit on subscription immediately */ + it("should emit on subscription immediately", done => { + const offset$ = offset() + offset$ + .take(1) + .subscribe(initial => { + expect(initial).toEqual({ + x: window.pageXOffset, + y: window.pageYOffset + }) + done() + }, done.fail) + }) + + /* Test: it should emit on viewport offset change */ + it("should emit on viewport offset change", done => { + const offset$ = offset() + offset$ + .take(2) + .toArray() + .subscribe(([initial, changed]) => { + expect(initial).toEqual({ + x: 0, + y: 0 + }) + expect(changed).toEqual({ + x: 0, + y: 1 + }) + done() + }, done.fail) + window.scrollTo(0, 1) + }) + }) + + /* Create a subject emitting changes in viewport size */ + describe(".size", () => { + + /* Test: it should return a behavior subject */ + it("should return a behavior subject", () => { + const size$ = size() + expect(size$).toEqual(jasmine.any(Subject)) + }) + + /* Test: it should emit on subscription immediately */ + it("should emit on subscription immediately", done => { + const size$ = size() + size$ + .take(1) + .subscribe(initial => { + expect(initial).toEqual({ + width: window.innerWidth, + height: window.innerHeight + }) + done() + }, done.fail) + }) + + /* Test: it should emit on viewport size change */ + it("should emit on viewport size change", done => { + const size$ = size() + size$ + .take(2) + .toArray() + .subscribe(([initial, changed]) => { + expect(initial).toEqual({ + width: 480, + height: 320 + }) + expect(changed).toEqual({ + width: 479, + height: 319 + }) + done() + }, done.fail) + viewport.set(479, 319) + }) + }) + }) +}) diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 45c4fcf0f3f57a418d481fff688d78ade407d7d6..f81b695ab85c214692e34d032bc0e842772377ae 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -22,7 +22,7 @@ declare module "*.json" { const value: any - export default value + export = value } declare const viewport: any /* karma-viewport */ diff --git a/typings/karma.d.ts b/typings/karma.d.ts index 36ccdc26ad47b105d75a0034643d206cf8470a6b..5ef7db3eb2b82ca301c1fd890041ef0be0c712ff 100644 --- a/typings/karma.d.ts +++ b/typings/karma.d.ts @@ -30,6 +30,7 @@ declare module "karma" { interface ConfigOptions { webpack?: WebpackConfig /* karma-webpack */ specReporter?: { /* karma-spec-reporter */ + suppressErrorSummary: boolean suppressSkipped: boolean } coverageIstanbulReporter?: { /* karma-coverage */