diff --git a/js/src/util/subscribe-to-events.spec.js b/js/src/util/subscribe-to-events.spec.js
new file mode 100644
index 000000000..3629e8d79
--- /dev/null
+++ b/js/src/util/subscribe-to-events.spec.js
@@ -0,0 +1,115 @@
+// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+import { spy, stub } from 'sinon';
+
+import subscribeToEvents from './subscribe-to-events';
+import {
+ pastLogs, liveLogs, createApi, createContract
+} from './subscribe-to-events.test.js';
+
+const delay = (t) => new Promise((resolve) => {
+ setTimeout(resolve, t);
+});
+
+describe('util/subscribe-to-events', () => {
+ beforeEach(function () {
+ this.api = createApi();
+ this.contract = createContract(this.api);
+ });
+
+ it('installs a filter', async function () {
+ const { api, contract } = this;
+
+ subscribeToEvents(contract, [ 'Foo', 'Bar' ]);
+ await delay(0);
+
+ expect(api.eth.newFilter.calledOnce).to.equal(true);
+ expect(api.eth.newFilter.firstCall.args).to.eql([ {
+ fromBlock: 0, toBlock: 'latest',
+ address: contract.address,
+ topics: [ [
+ contract.instance.Foo.signature,
+ contract.instance.Bar.signature
+ ] ]
+ } ]);
+ });
+
+ it('queries & parses logs in the beginning', async function () {
+ const { api, contract } = this;
+
+ subscribeToEvents(contract, [ 'Foo', 'Bar' ]);
+
+ await delay(0);
+ expect(api.eth.getFilterLogs.callCount).to.equal(1);
+ expect(api.eth.getFilterLogs.firstCall.args).to.eql([ 123 ]);
+
+ await delay(0);
+ expect(contract.parseEventLogs.callCount).to.equal(1);
+ });
+
+ it('emits logs in the beginning', async function () {
+ const { contract } = this;
+
+ const onLog = spy();
+ const onFoo = spy();
+ const onBar = spy();
+ subscribeToEvents(contract, [ 'Foo', 'Bar' ])
+ .on('log', onLog)
+ .on('Foo', onFoo)
+ .on('Bar', onBar);
+
+ await delay(0);
+
+ expect(onLog.callCount).to.equal(2);
+ expect(onLog.firstCall.args).to.eql([ pastLogs[0] ]);
+ expect(onLog.secondCall.args).to.eql([ pastLogs[1] ]);
+ expect(onFoo.callCount).to.equal(1);
+ expect(onFoo.firstCall.args).to.eql([ pastLogs[0] ]);
+ expect(onBar.callCount).to.equal(1);
+ expect(onBar.firstCall.args).to.eql([ pastLogs[1] ]);
+ });
+
+ it('uninstalls the filter on sunsubscribe', async function () {
+ const { api, contract } = this;
+
+ const s = subscribeToEvents(contract, [ 'Foo', 'Bar' ]);
+ await delay(0);
+ s.unsubscribe();
+ await delay(0);
+
+ expect(api.eth.uninstallFilter.calledOnce).to.equal(true);
+ expect(api.eth.uninstallFilter.firstCall.args).to.eql([ 123 ]);
+ });
+
+ it('checks for new events regularly', async function () {
+ const { api, contract } = this;
+ api.eth.getFilterLogs = stub().resolves([]);
+
+ const onLog = spy();
+ const onBar = spy();
+ const s = subscribeToEvents(contract, [ 'Bar' ], { interval: 5 })
+ .on('log', onLog)
+ .on('Bar', onBar);
+ await delay(9);
+ s.unsubscribe();
+
+ expect(onLog.callCount).to.equal(1);
+ expect(onLog.firstCall.args).to.eql([ liveLogs[0] ]);
+ expect(onBar.callCount).to.equal(1);
+ expect(onBar.firstCall.args).to.eql([ liveLogs[0] ]);
+ });
+});
diff --git a/js/src/util/subscribe-to-events.test.js b/js/src/util/subscribe-to-events.test.js
new file mode 100644
index 000000000..642f8e592
--- /dev/null
+++ b/js/src/util/subscribe-to-events.test.js
@@ -0,0 +1,53 @@
+// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+import { stub } from 'sinon';
+
+export const ADDRESS = '0x1111111111111111111111111111111111111111';
+
+export const pastLogs = [
+ { event: 'Foo', type: 'mined', address: ADDRESS, params: {} },
+ { event: 'Bar', type: 'mined', address: ADDRESS, params: {} }
+];
+
+export const liveLogs = [
+ { event: 'Bar', type: 'mined', address: ADDRESS, params: { foo: 'bar' } }
+];
+
+export const createApi = () => ({
+ eth: {
+ newFilter: stub().resolves(123),
+ uninstallFilter: stub()
+ .rejects(new Error('unknown filter id'))
+ .withArgs(123).resolves(null),
+ getFilterLogs: stub()
+ .rejects(new Error('unknown filter id'))
+ .withArgs(123).resolves(pastLogs),
+ getFilterChanges: stub()
+ .rejects(new Error('unknown filter id'))
+ .withArgs(123).resolves(liveLogs)
+ }
+});
+
+export const createContract = (api) => ({
+ api,
+ address: ADDRESS,
+ instance: {
+ Foo: { signature: 'Foo signature' },
+ Bar: { signature: 'Bar signature' }
+ },
+ parseEventLogs: stub().returnsArg(0)
+});