Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading manager tests #52

Merged
merged 3 commits into from
Feb 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion babel-jest-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ module.exports = {
return babel.transform(src, {
filename: filename,
retainLines: true,
presets: ['env']
presets: ['env'],
plugins: [
[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true
}
]
]
}).code;
}

Expand Down
24 changes: 7 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"babel-eslint": "^8.0.1",
"babel-jest": "^21.2.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"eslint": "^4.8.0",
"eslint-config-airbnb": "^15.1.0",
Expand All @@ -24,7 +25,8 @@
"grunt-contrib-uglify": "^3.1.0",
"grunt-contrib-watch": "^1.0.0",
"grunt-webpack": "^3.0.2",
"jest-cli": "^0.7.1",
"hanzi-writer-data": "^1.0.0",
"jest-cli": "^22.2.1",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1"
},
Expand All @@ -34,21 +36,9 @@
"prepublishOnly": "grunt"
},
"jest": {
"scriptPreprocessor": "<rootDir>/../babel-jest-processor.js",
"preprocessCachingDisabled": true,
"testFileExtensions": [
"js"
],
"moduleFileExtensions": [
"js"
],
"rootDir": "src",
"unmockedModulePathPatterns": [
"models",
"util",
"geometry",
"svg",
"clone"
]
"transform": {
".*": "<rootDir>/../babel-jest-processor.js"
},
"rootDir": "src"
}
}
7 changes: 6 additions & 1 deletion src/HanziWriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,12 @@ HanziWriter.prototype._withData = function(func) {
// Try reloading again and see if it helps
if (this._loadingManager.loadingFailed) {
this.setCharacter(this._char);
return this._withData(func);
return Promise.resolve().then(() => {
// check loadingFailed again just in case setCharacter fails synchronously
if (!this._loadingManager.loadingFailed) {
return this._withData(func);
}
});
}
return this._withDataPromise.then(() => {
if (!this._loadingManager.loadingFailed) {
Expand Down
18 changes: 13 additions & 5 deletions src/LoadingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function LoadingManager(options) {
this._isLoading = false;
this._loadingPromise = null;

// use this to attribute to determine if there was a problem with loading
// use this to attribute to determine if there was a problem with loading
this.loadingFailed = false;
}

Expand All @@ -16,8 +16,8 @@ LoadingManager.prototype._debouncedLoad = function(char, count) {
const wrappedResolve = (data) => {
if (count === this._loadCounter) this._resolve(data);
};
const wrappedReject = (...args) => {
if (count === this._loadCounter) this._reject(...args);
const wrappedReject = (reason) => {
if (count === this._loadCounter) this._reject(reason);
};

const returnedData = this._options.charDataLoader(char, wrappedResolve, wrappedReject);
Expand All @@ -32,14 +32,22 @@ LoadingManager.prototype._setupLoadingPromise = function() {
this._isLoading = false;
callIfExists(this._options.onLoadCharDataSuccess, data);
return data;
}, (...args) => {
}, (reason) => {
this._isLoading = false;
this.loadingFailed = true;
callIfExists(this._options.onLoadCharDataError, ...args);
callIfExists(this._options.onLoadCharDataError, reason);
// If error callback wasn't provided, throw an error so the developer will be aware something went wrong
if (!this._options.onLoadCharDataError) {
if (reason instanceof Error) throw reason;
const err = new Error(`Failed to load char data for ${this._loadingChar}`);
err.reason = reason;
throw err;
}
});
};

LoadingManager.prototype.loadCharData = function(char) {
this._loadingChar = char;
if (!this._isLoading) {
this._setupLoadingPromise();
}
Expand Down
66 changes: 66 additions & 0 deletions src/__tests__/HanziWriter-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const ren = require('hanzi-writer-data/人.json');
const HanziWriter = require('../HanziWriter');

describe('HanziWriter', () => {
// Hack because JSDom doesn't support SVG well
window.SVGElement.prototype.getTotalLength = () => 10;

describe('constructor', () => {
it("loads data and builds an instance in a dom element", () => {
document.body.innerHTML = '<div id="target"></div>';

const writer = new HanziWriter('target', '人', {
charDataLoader: () => ren,
});

// TODO: add more assertions
expect(document.querySelectorAll('svg').length).toBe(1);
});

it("calls onLoadCharDataError if provided on loading failure", async () => {
document.body.innerHTML = '<div id="target"></div>';

const onLoadCharDataError = jest.fn();
const writer = new HanziWriter('target', '人', {
onLoadCharDataError,
charDataLoader: () => Promise.reject('reasons'),
});

await writer._withDataPromise;

expect(onLoadCharDataError.mock.calls.length).toBe(1);
expect(onLoadCharDataError.mock.calls[0][0]).toBe('reasons');
});

it("tries reloading when calling an animatable method after loading failure", async () => {
document.body.innerHTML = '<div id="target"></div>';

const onLoadCharDataError = jest.fn();
const writer = new HanziWriter('target', '人', {
onLoadCharDataError,
charDataLoader: (char, onComplete, onErr) => {
onErr('reasons');
},
});

await writer._withDataPromise;
await writer.showCharacter();

expect(onLoadCharDataError.mock.calls.length).toBe(2);
expect(onLoadCharDataError.mock.calls[0][0]).toBe('reasons');
expect(onLoadCharDataError.mock.calls[1][0]).toBe('reasons');
});

it("throws an error on loading fauire if onLoadCharDataError is not provided", async () => {
document.body.innerHTML = '<div id="target"></div>';

const writer = new HanziWriter('target', '人', {
charDataLoader: (char, onComplete, onErr) => {
onErr(new Error('reasons'));
},
});

await expect(writer._withDataPromise).rejects.toThrow(new Error('reasons'));
});
});
});
113 changes: 113 additions & 0 deletions src/__tests__/LoadingManager-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const ren = require('hanzi-writer-data/人.json');
const ta = require('hanzi-writer-data/他.json');
const { timeout } = require('../utils');
const LoadingManager = require('../LoadingManager');

describe('LoadingManager', () => {
describe('loadCharData', () => {
it("resolves when data is loaded via async callback", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => {
setTimeout(() => onComplete(ren), 1);
},
});
const data = await manager.loadCharData('人');
expect(data).toBe(ren);
expect(manager.loadingFailed).toBe(false);
});

it("resolves when data is loaded via sync callback", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => { onComplete(ren); },
});
const data = await manager.loadCharData('人');
expect(data).toBe(ren);
expect(manager.loadingFailed).toBe(false);
});

it("resolves when data is loaded via promise", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => Promise.resolve(ren),
});
const data = await manager.loadCharData('人');
expect(data).toBe(ren);
expect(manager.loadingFailed).toBe(false);
});

it("resolves when data is loaded via sync return", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => ren,
});
const data = await manager.loadCharData('人');
expect(data).toBe(ren);
expect(manager.loadingFailed).toBe(false);
});

it("passes data to onLoadCharDataSuccess if provided", async () => {
let successVal;
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => ren,
onLoadCharDataSuccess: (returnedData) => successVal = returnedData
});
const data = await manager.loadCharData('人');
expect(data).toBe(ren);
expect(successVal).toBe(ren);
expect(manager.loadingFailed).toBe(false);
});

it("throws an error if loading fails via onErr callback and no callback is provided", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => { onErr('OMG'); },
});
await expect(manager.loadCharData('人')).rejects.toThrow(new Error('Failed to load char data for 人'));
expect(manager.loadingFailed).toBe(true);
});

it("rethrows if loading fails via onErr callback passing an Error and no callback is provided", async () => {
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => { onErr(new Error('OMG')); },
});
await expect(manager.loadCharData('人')).rejects.toThrow(new Error('OMG'));
expect(manager.loadingFailed).toBe(true);
});

it("resolves if loading fails via onErr callback and a callback is provided", async () => {
let failureReason;
const manager = new LoadingManager({
charDataLoader: (char, onComplete, onErr) => { onErr('everything is terrible'); },
onLoadCharDataError: (reason) => { failureReason = reason; },
});
const data = await manager.loadCharData('人');
expect(manager.loadingFailed).toBe(true);
expect(data).toBe(undefined);
expect(failureReason).toBe('everything is terrible');
});

it("debounces if multiple loads are called at the same time", async () => {
const onLoadCharDataSuccess = jest.fn();
const onCompleteFns = [];
const manager = new LoadingManager({
onLoadCharDataSuccess,
charDataLoader: (char, onComplete) => {
onCompleteFns.push(onComplete);
},
});

const loadPromise1 = manager.loadCharData('人');
const loadPromise2 = manager.loadCharData('他');
// it should return the same promise for both since loading isn't complete
expect(loadPromise1).toBe(loadPromise2);

onCompleteFns[0].call(null, ren);
onCompleteFns[1].call(null, ta);

const data = await loadPromise1;

// ren should be ignored. It's like it was never requested at all
expect(data).toBe(ta);
expect(onLoadCharDataSuccess.mock.calls.length).toBe(1);
expect(onLoadCharDataSuccess.mock.calls[0][0]).toBe(ta);
expect(manager.loadingFailed).toBe(false);
});
});
});
Loading