diff --git a/.travis.yml b/.travis.yml index 0d4e9c2..5ecd176 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,3 +18,9 @@ script: - npm run test -- --no-progress --code-coverage --single-run --browser=ChromeNoSandbox - npm run e2e -- --no-progress - npm run coverage +addons: + apt: + sources: + - google-chrome + packages: + - google-chrome-stable diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index 9de7cc5..bf242c4 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -4,6 +4,8 @@ import { browser } from 'protractor'; describe('ImageLoader Lib E2E Tests', function () { let page: AppPage; + const browserWaitTimeout = 10000; + beforeEach(() => page = new AppPage()); beforeEach(() => page.navigateTo()); @@ -16,6 +18,18 @@ describe('ImageLoader Lib E2E Tests', function () { }); }); + describe('placeholder image', () => { + beforeEach(() => page.setWindowSize(300, 580)); + + it('should load placeholder image', () => { + expect(page.getImageElement().getAttribute('src')).toEqual('http://via.placeholder.com/35x15?text=placeholder'); + }); + + it('should update placeholder loaded boolean on init', () => { + expect(page.getplaceholderBooleanElement().getText()).toEqual('true'); + }); + }); + describe('lazy load image', () => { beforeEach(() => page.setWindowSize(300, 580)); @@ -25,34 +39,94 @@ describe('ImageLoader Lib E2E Tests', function () { expect(imageLoaderCompClass).toContain('sn-image-not-loaded'); expect(imgSrc).toEqual('http://via.placeholder.com/35x15?text=placeholder'); - page.scrollTo(0, 580 * 1.5); + page.scrollTo(0, 580 * 1.5) + .then(() => { + browser.wait(() => page.getLoadedImageElement()); + }); + imageLoaderCompClass = page.getImageLoaderComp().getAttribute('class'); imgSrc = page.getImageElement().getAttribute('srcset'); expect(imageLoaderCompClass).toContain('sn-image-loaded'); expect(imgSrc).toEqual('http://via.placeholder.com/150x350?text=xs+1x 1x, http://via.placeholder.com/300x700?text=xs+2x 2x'); + + }); + + it('should update full res image event count on when in viewport', () => { + expect(page.getFullResCountElement().getText()).toEqual('0'); + + page.scrollTo(0, 580 * 1.5) + .then(() => { + browser.wait(() => page.getLoadedImageElement()); + }); + + expect(page.getFullResCountElement().getText()).toEqual('1'); }); + }); describe('responsive image', () => { beforeEach(() => page.setWindowSize(300, 580)); it('should load correct image for device size', () => { - page.scrollTo(0, 580 * 1.5); + page.scrollTo(0, 580 * 1.5) + .then(() => { + browser.wait(() => page.getLoadedImageElement()); + }); const imageLoaderCompClass = page.getImageLoaderComp().getAttribute('class'); let imgSrc = page.getImageElement().getAttribute('srcset'); - expect(imageLoaderCompClass).toContain('sn-image-loaded'); expect(imgSrc).toEqual('http://via.placeholder.com/150x350?text=xs+1x 1x, http://via.placeholder.com/300x700?text=xs+2x 2x'); - page.setWindowSize(768, 580); + page.setWindowSize(768, 580) + .then(() => { + browser.wait(() => page.getLoadedImageElementBySrcSet( + 'http://via.placeholder.com/350x250?text=md+1x 1x, http://via.placeholder.com/700x500?text=md+2x 2x' + ), browserWaitTimeout); + }); + imgSrc = page.getImageElement().getAttribute('srcset'); expect(imgSrc).toEqual('http://via.placeholder.com/350x250?text=md+1x 1x, http://via.placeholder.com/700x500?text=md+2x 2x'); - page.setWindowSize(1024, 580); + page.setWindowSize(1024, 580) + .then(() => { + browser.wait(() => page.getLoadedImageElementBySrcSet( + 'http://via.placeholder.com/700x400?text=lg+1x 1x, http://via.placeholder.com/1400x800?text=lg+2x 2x' + ), browserWaitTimeout); + }); + imgSrc = page.getImageElement().getAttribute('srcset'); expect(imgSrc).toEqual('http://via.placeholder.com/700x400?text=lg+1x 1x, http://via.placeholder.com/1400x800?text=lg+2x 2x'); }); + + it('should update image loaded event count on window resize when image in viewport', () => { + expect(page.getFullResCountElement().getText()).toEqual('0'); + + page.scrollTo(0, 580 * 1.5) + .then(() => { + browser.wait(() => page.getLoadedImageElement()); + }); + + expect(page.getFullResCountElement().getText()).toEqual('1'); + + page.setWindowSize(768, 580) + .then(() => { + browser.wait(() => page.getLoadedImageElementBySrcSet( + 'http://via.placeholder.com/350x250?text=md+1x 1x, http://via.placeholder.com/700x500?text=md+2x 2x' + ), browserWaitTimeout); + }); + expect(page.getFullResCountElement().getText()).toEqual('2'); + + page.setWindowSize(1024, 580) + .then(() => { + browser.wait(() => page.getLoadedImageElementBySrcSet( + 'http://via.placeholder.com/700x400?text=lg+1x 1x, http://via.placeholder.com/1400x800?text=lg+2x 2x' + ), browserWaitTimeout); + }); + expect(page.getFullResCountElement().getText()).toEqual('3'); + }); + }); + }); diff --git a/e2e/app.po.ts b/e2e/app.po.ts index d4a290f..90f6728 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -6,13 +6,11 @@ export class AppPage { } scrollTo(x: number = 0, y: number = 0) { - browser.executeScript(`window.scrollTo(${x}, ${y})`); - browser.sleep(200); + return browser.executeScript(`window.scrollTo(${x}, ${y})`); } setWindowSize(x: number, y: number) { - browser.driver.manage().window().setSize(x, y); - browser.sleep(200); + return browser.driver.manage().window().setSize(x, y); } getImageLoaderComp() { @@ -22,4 +20,20 @@ export class AppPage { getImageElement() { return element(by.css('sn-image-loader .foo')); } + + getLoadedImageElement() { + return element(by.css('.sn-image-loaded')).isPresent(); + } + + getLoadedImageElementBySrcSet(srcSet) { + return element(by.css(`img[srcSet="${srcSet}"]`)).isPresent(); + } + + getplaceholderBooleanElement() { + return element(by.css('.placeholder-boolean')); + } + + getFullResCountElement() { + return element(by.css('.full-res-count')); + } } diff --git a/package-lock.json b/package-lock.json index dd130e6..1c0af56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1819,6 +1819,12 @@ } } }, + "conventional-commit-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz", + "integrity": "sha1-XblXOdbCEqy+e29lahG5QLqmiUY=", + "dev": true + }, "conventional-commits-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.0.tgz", @@ -2222,6 +2228,19 @@ "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", "dev": true }, + "cz-conventional-changelog": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz", + "integrity": "sha1-L0vHOQ4yROTfKT5ro1Hkx0Cnx2Q=", + "dev": true, + "requires": { + "conventional-commit-types": "2.2.0", + "lodash.map": "4.6.0", + "longest": "1.0.1", + "right-pad": "1.0.1", + "word-wrap": "1.2.3" + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -6491,6 +6510,12 @@ "lodash.isarray": "3.0.4" } }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9194,6 +9219,12 @@ "align-text": "0.1.4" } }, + "right-pad": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz", + "integrity": "sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=", + "dev": true + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -11659,6 +11690,12 @@ "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", "dev": true }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", diff --git a/package.json b/package.json index f172ef2..3458df5 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "codelyzer": "^4.0.1", "core-js": "^2.4.1", "coveralls": "^3.0.0", + "cz-conventional-changelog": "^2.1.0", "jasmine-core": "~2.6.2", "jasmine-spec-reporter": "~4.1.0", "karma": "~1.7.0", @@ -84,5 +85,10 @@ "tslint": "~5.7.0", "typescript": "~2.4.2", "zone.js": "^0.8.14" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } } } diff --git a/src/app/app.component.html b/src/app/app.component.html index 31a64f7..ae7e872 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,8 +1,13 @@

Scroll down ↓

+

Placeholder image loaded: {{ imagePlaceholderLoaded }}

+

imageLoaded event count: {{ imageLoadedEventCount }}

diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 896de39..77595b7 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { ResponsiveImage, Breakpoint, Size } from './image-loader'; +import { ImageLoadedEvent } from './image-loader/shared'; @Component({ selector: 'sn-root', @@ -29,4 +30,40 @@ export class AppComponent { '@2x': 'http://via.placeholder.com/1400x800?text=lg+2x' } }; + + /** + * Set to true on placeholder loaded event. + * + * @type {boolean} + * @memberof AppComponent + */ + imagePlaceholderLoaded = false; + + /** + * Incremented on each image load event. + * + * @type {number} + * @memberof AppComponent + */ + imageLoadedEventCount = 0; + + /** + * Increments event count on each image loaded event. + * Counter displayed in component template. + * + * @memberof AppComponent + */ + public onPlaceholderLoad(imageLoadedEvent: ImageLoadedEvent) { + this.imagePlaceholderLoaded = true; + } + + /** + * Increments event count on each image loaded event. + * Counter displayed in component template. + * + * @memberof AppComponent + */ + public onFullResLoad(imageLoadedEvent: ImageLoadedEvent) { + this.imageLoadedEventCount++; + } } diff --git a/src/app/image-loader/image-loader.component.html b/src/app/image-loader/image-loader.component.html index 6496ac3..2d56348 100644 --- a/src/app/image-loader/image-loader.component.html +++ b/src/app/image-loader/image-loader.component.html @@ -6,7 +6,8 @@ [class]="imgClass" inViewport (inViewportChange)="onInViewportChange($event)" - [debounce]="0"> + [debounce]="0" + (load)="onImageLoad($event)"> { let fixture: ComponentFixture; @@ -56,6 +58,14 @@ describe('ImageLoaderComponent', () => { expect(spy).toHaveBeenCalled(); }); + it('should set fire placeholder loaded event on image load when loaded is false', () => { + const spy = spyOn(component.imagePlaceholderLoaded, 'emit'); + component.loaded = false; + const imageElement = fixture.debugElement.query(By.css('img')); + imageElement.triggerEventHandler('load', null); + expect(spy).toHaveBeenCalled(); + }); + it('should set supportsSrcSet value', () => { component.supportsSrcSet = false; const img = document.createElement('img'); @@ -145,6 +155,14 @@ describe('ImageLoaderComponent', () => { .toEqual('http://via.placeholder.com/150x350?text=xs+1x 1x, http://via.placeholder.com/300x700?text=xs+2x 2x'); }); + it('should emit a full res loaded event on image load when loaded is true', () => { + const spy = spyOn(component.imageLoaded, 'emit'); + component.loaded = true; + const imageElement = fixture.debugElement.query(By.css('img')); + imageElement.triggerEventHandler('load', null); + expect(spy).toHaveBeenCalled(); + }); + it('should complete observable', () => { const spy = spyOn(component.ngUnsubscribe$, 'complete'); component.ngOnDestroy(); diff --git a/src/app/image-loader/image-loader.component.ts b/src/app/image-loader/image-loader.component.ts index 5390485..ad32929 100644 --- a/src/app/image-loader/image-loader.component.ts +++ b/src/app/image-loader/image-loader.component.ts @@ -1,11 +1,23 @@ -import { Component, Input, OnInit, HostBinding, HostListener, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + OnInit, + HostBinding, + HostListener, + AfterViewInit, + OnDestroy, + ViewChild, + ElementRef +} from '@angular/core'; import { Subject } from 'rxjs/Subject'; import 'rxjs/add/operator/takeUntil'; import 'rxjs/add/operator/debounceTime'; -import { ResponsiveImage, RetinaImage, Size, Breakpoint, Retina } from './shared/image.model'; import * as classes from './shared/classes'; import * as events from './shared/events'; +import { ImageLoadedEvent, ResponsiveImage, RetinaImage, Size, Breakpoint, Retina } from './shared'; /** * A component that renders a `img` element with the correct image url @@ -17,6 +29,8 @@ import * as events from './shared/events'; * [image]="image" * [sizes]="sizes" * imgClass="foo" + * (imagePlaceholderLoaded)="onPlaceholderLoad($event)" + * (imageLoaded)="onFullResLoad($event)" * alt="lorem ipsum"> * * ``` @@ -134,6 +148,22 @@ export class ImageLoaderComponent implements OnInit, AfterViewInit, OnDestroy { * @memberof ImageLoaderComponent */ public supportsSrcSet = false; + /** + * Output for placeholder image loaded event. + * + * @type {EventEmitter} + * @memberof ImageLoaderComponent + */ + @Output() + public imagePlaceholderLoaded: EventEmitter = new EventEmitter(); + /** + * Output for full res image loaded event. + * + * @type {EventEmitter} + * @memberof ImageLoaderComponent + */ + @Output() + public imageLoaded: EventEmitter = new EventEmitter(); /** * If true means the image has not been loaded yet and * the placeholder image is currently displayed @@ -255,6 +285,23 @@ export class ImageLoaderComponent implements OnInit, AfterViewInit, OnDestroy { this.preloadSrc = ''; this.loaded = true; } + /** + * When the main `img` element has loaded + * + * @memberof ImageLoaderComponent + */ + public onImageLoad($event: Event): void { + const eventData = { + $event, + src: this.src, + srcset: this.srcset + }; + if (!this.loaded) { + this.imagePlaceholderLoaded.emit(eventData); + return; + } + this.imageLoaded.emit(eventData); + } /** * Trigger `ngUnsubscribe` complete on * component destroy lifecycle hook diff --git a/src/app/image-loader/shared/image-loaded-event.model.ts b/src/app/image-loader/shared/image-loaded-event.model.ts new file mode 100644 index 0000000..7ee1ab7 --- /dev/null +++ b/src/app/image-loader/shared/image-loaded-event.model.ts @@ -0,0 +1,5 @@ +export interface ImageLoadedEvent { + $event: Event; + src: string; + srcset: string; +} diff --git a/src/app/image-loader/shared/index.ts b/src/app/image-loader/shared/index.ts index 59f023e..9f8cd37 100644 --- a/src/app/image-loader/shared/index.ts +++ b/src/app/image-loader/shared/index.ts @@ -1,3 +1,4 @@ export * from './classes'; export * from './events'; export * from './image.model'; +export * from './image-loaded-event.model';