Skip to content

Commit

Permalink
Enable graceful failure (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamin-t-frost authored Jun 14, 2021
1 parent fdd05f6 commit 108cf47
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 85 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-ld",
"version": "0.1.10",
"version": "0.1.11",
"description": "A Vue.js wrapper for the LaunchDarkly SDK for Browser JavaScript",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
Expand Down
9 changes: 8 additions & 1 deletion src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as LDClient from 'launchdarkly-js-client-sdk';
import { formatFlags } from './utils';
import { formatFlags, rethrow } from './utils';

export const initialize = ({ clientSideId, user, ldOptions, readyBeforeIdentify }) => {
const ldClient = LDClient.initialize(clientSideId, user, ldOptions);
Expand All @@ -20,6 +20,7 @@ export const initialize = ({ clientSideId, user, ldOptions, readyBeforeIdentify
},
flags: {},
ready: false,
error: null,
};

ldClient.on('ready', () => {
Expand All @@ -36,6 +37,12 @@ export const initialize = ({ clientSideId, user, ldOptions, readyBeforeIdentify
...formatFlags(flattenedFlags),
};
});

ldClient.on('error', (e) => {
$ld.error = e;
rethrow(e);
});

return $ld;
};

Expand Down
5 changes: 5 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const formatFlags = (flags) => {
})
);
};

export const rethrow = (e) => {
// Intended to be mocked in tests to avoid uncaught promise rejections in dependencies
throw e;
};
202 changes: 119 additions & 83 deletions tests/unit/vue-ld.spec.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import sinon from 'sinon';
import { createLocalVue, mount } from '@vue/test-utils';
import VueLd from '@/plugin';
import { rethrow } from '@/utils';
import { ldClientReady } from './utils';
import { vueLdOptions, flagsResponse } from './dummy';

const Component = {
template: '<div></div>',
};

jest.mock('@/utils', () => {
return {
...jest.requireActual('@/utils'),
rethrow: jest.fn(),
};
});

describe('VueLd Plugin', () => {
let errorSpy;
let localVue;
let server;
let warnSpy;
let wrapper;

beforeEach(() => {
localVue = createLocalVue();
server = sinon.createFakeServer();
server.autoRespond = true;
server.autoRespondAfter = 0;
server.respondWith([
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(flagsResponse),
]);
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
Expand All @@ -33,99 +36,132 @@ describe('VueLd Plugin', () => {
errorSpy.mockRestore();
});

let wrapper;
it('changes ready state before identify', async () => {
localVue.use(VueLd, vueLdOptions);
wrapper = mount(Component, {
localVue,
describe('Success states', () => {
beforeEach(() => {
server.autoRespond = true;
server.autoRespondAfter = 0;
server.respondWith([
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(flagsResponse),
]);
});
expect(wrapper.vm.$ld).not.toBe(undefined);
expect(wrapper.vm.$ld.ready).toBe(false);

await ldClientReady(wrapper);
it('changes ready state before identify', async () => {
localVue.use(VueLd, vueLdOptions);
wrapper = mount(Component, {
localVue,
});
expect(wrapper.vm.$ld).not.toBe(undefined);
expect(wrapper.vm.$ld.ready).toBe(false);

expect(wrapper.vm.$ld.ready).toBe(true);
});
await ldClientReady(wrapper);

it('changes ready state after identify', async () => {
localVue.use(VueLd, {
...vueLdOptions,
readyBeforeIdentify: false,
});
wrapper = mount(Component, {
localVue,
expect(wrapper.vm.$ld.ready).toBe(true);
});

expect(wrapper.vm.$ld).not.toBe(undefined);
expect(wrapper.vm.$ld.ready).toBe(false);

await ldClientReady(wrapper);
expect(wrapper.vm.$ld.ready).toBe(false);
await wrapper.vm.$ld.identify({
newUser: {
key: 'anonymous2',
email: 'anonymous2@test.com',
name: 'Anonymous User 2',
},
});
expect(wrapper.vm.$ld.ready).toBe(true);
});
it('changes ready state after identify', async () => {
localVue.use(VueLd, {
...vueLdOptions,
readyBeforeIdentify: false,
});
wrapper = mount(Component, {
localVue,
});

it('calls vueLdCallback after ready with correct context', async () => {
localVue.use(VueLd, {
...vueLdOptions,
readyBeforeIdentify: false,
});
wrapper = mount(Component, {
localVue,
});
const vueLdCallback = jest.fn();
await ldClientReady(wrapper);
await wrapper.vm.$ld.identify(
{
expect(wrapper.vm.$ld).not.toBe(undefined);
expect(wrapper.vm.$ld.ready).toBe(false);

await ldClientReady(wrapper);
expect(wrapper.vm.$ld.ready).toBe(false);
await wrapper.vm.$ld.identify({
newUser: {
key: 'anonymous2',
email: 'anonymous2@test.com',
name: 'Anonymous User 2',
},
},
vueLdCallback
);

expect(vueLdCallback).toBeCalled();
expect(vueLdCallback.mock.instances[0]).toBe(wrapper.vm.$ld);
});
});
expect(wrapper.vm.$ld.ready).toBe(true);
});

it('stubs flags when passed the option', async () => {
localVue.use(VueLd, {
...vueLdOptions,
/*
Using a proxy like this will allow you to return true for everything
not explicitly on the base object or set later.
*/
flagsStub: new Proxy(
{
never: false,
},
it('calls vueLdCallback after ready with correct context', async () => {
localVue.use(VueLd, {
...vueLdOptions,
readyBeforeIdentify: false,
});
wrapper = mount(Component, {
localVue,
});
const vueLdCallback = jest.fn();
await ldClientReady(wrapper);
await wrapper.vm.$ld.identify(
{
get(obj, prop) {
const value = obj[prop];
return value === undefined ? true : value;
newUser: {
key: 'anonymous2',
email: 'anonymous2@test.com',
name: 'Anonymous User 2',
},
}
),
});
wrapper = mount(Component, {
localVue,
},
vueLdCallback
);

expect(vueLdCallback).toBeCalled();
expect(vueLdCallback.mock.instances[0]).toBe(wrapper.vm.$ld);
});

expect(wrapper.vm.$ld.flags.never).toBe(false);
expect(wrapper.vm.$ld.flags.anythingElse).toBe(true);
it('stubs flags when passed the option', async () => {
localVue.use(VueLd, {
...vueLdOptions,
/*
Using a proxy like this will allow you to return true for everything
not explicitly on the base object or set later.
*/
flagsStub: new Proxy(
{
never: false,
},
{
get(obj, prop) {
const value = obj[prop];
return value === undefined ? true : value;
},
}
),
});
wrapper = mount(Component, {
localVue,
});

expect(wrapper.vm.$ld.flags.never).toBe(false);
expect(wrapper.vm.$ld.flags.anythingElse).toBe(true);

wrapper.vm.$ld.flags.neverLater = false;
expect(wrapper.vm.$ld.flags.neverLater).toBe(false);

wrapper.vm.$ld.flags.neverLater = false;
expect(wrapper.vm.$ld.flags.neverLater).toBe(false);
delete wrapper.vm.$ld.flags.neverLater;
expect(wrapper.vm.$ld.flags.anythingElse).toBe(true);
});
});

delete wrapper.vm.$ld.flags.neverLater;
expect(wrapper.vm.$ld.flags.anythingElse).toBe(true);
describe('Error states', () => {
beforeEach(() => {
server.autoRespond = true;
server.autoRespondAfter = 0;
server.respondWith([
400,
{ 'Content-Type': 'application/json' },
JSON.stringify({ message: 'Bad Request' }),
]);
});

it('should set error when ldClient emits an error', async () => {
localVue.use(VueLd, vueLdOptions);
wrapper = mount(Component, {
localVue,
});
await ldClientReady(wrapper);
expect(wrapper.vm.$ld.error).toBeTruthy();
// Should not eat errors
expect(rethrow).toHaveBeenCalled();
});
});
});

0 comments on commit 108cf47

Please sign in to comment.