import { html, LitElement } from "lit"; import { property, customElement, state } from 'lit/decorators.js'; import sinon from "sinon"; import LiveState from "../src/LiveState"; import { expect } from "@esm-bundle/chai"; import {Channel} from 'phoenix'; import { fixture } from '@open-wc/testing'; import { connectElement } from "../src"; @customElement('test-element') class TestElement extends LitElement { @property() foo: string; @state() bar: string; @state() nested: string; constructor() { super(); this.addEventListener('livestate-error', (e: CustomEvent) => { this.foo = (e.detail as any).message; this.bar = (e.detail as any).type; }) } render() { return html`
${this.foo} ${this.bar}
` } } describe('connectElement', () => { let liveState, stubChannel, receiveStub; beforeEach(() => { liveState = new LiveState({url: "wss://foo.com", topic: "stuff"}); receiveStub = sinon.stub(); receiveStub.withArgs("ok", sinon.match.func).returns({receive: receiveStub}); stubChannel = sinon.createStubInstance(Channel, { join: sinon.stub().returns({ receive: receiveStub }), on: sinon.spy(), push: sinon.spy() }); liveState.channel = stubChannel; }); it('creates livestate instance with arity 2 version', async() => { const el: TestElement = await fixture(''); connectElement(el, { url: 'ws://foo.bar', topic: 'howdy', properties: ['bar'], attributes: ['foo'] }); expect(el['liveState']).to.be.instanceOf(LiveState); }); it('updates on state changes', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'] }); const stateChange = liveState.channel.on.getCall(0).args[1]; stateChange({state: { foo: 'wuzzle', bar: 'wizzle' }, version: 1}); await el.updateComplete; expect(el.bar).to.equal('wizzle'); expect(el.shadowRoot.innerHTML).to.contain('wizzle'); expect(el.getAttribute('foo')).to.equal('wuzzle'); expect(el.shadowRoot.innerHTML).to.contain('wuzzle'); }); it('updates nested state properties', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: [{name: 'nested', path: 'foo.bar'}], }); const stateChange = liveState.channel.on.getCall(0).args[1]; stateChange({state: { foo: {bar: 'wizzle' }}, version: 1}); await el.updateComplete; expect(el.nested).to.equal('wizzle'); }); it('sends events', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'], events: { send: ['sayHi'] } }); el.dispatchEvent(new CustomEvent('sayHi', { detail: { greeting: 'wazzaap' } })); expect(liveState.channel.push.callCount).to.equal(1); const pushCall = liveState.channel.push.getCall(0); expect(pushCall.args[0]).to.equal('lvs_evt:sayHi'); expect(pushCall.args[1]).to.deep.equal({ greeting: 'wazzaap' }); }); it('connects idempotently', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'], events: { send: ['sayHi'] } }); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'], events: { send: ['sayHi'] } }); el.dispatchEvent(new CustomEvent('sayHi', { detail: { greeting: 'wazzaap' } })); expect(liveState.channel.push.callCount).to.equal(1); }); it('receives events', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'], events: { send: ['sayHi'], receive: ['sayHiBack'] } }); const onArgs = liveState.channel.on.getCall(3).args; expect(onArgs[0]).to.equal("sayHiBack") const onHandler = onArgs[1]; let eventDetail; el.addEventListener('sayHiBack', ({ detail }: CustomEvent) => { eventDetail = detail }); onHandler({ foo: 'bar' }) expect(eventDetail).to.deep.equal({ foo: 'bar' }); }); it('does not call connect when given a LiveState instance', async () => { const el: TestElement = await fixture(''); const connectSpy = sinon.spy(liveState, 'connect'); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'] }); expect(connectSpy.callCount).to.equal(0); connectSpy.restore(); }); it('receives errors', async () => { const el: TestElement = await fixture(''); liveState.connect(); connectElement(liveState, el, { properties: ['bar'], attributes: ['foo'], events: { send: ['sayHi'], receive: ['sayHiBack', 'livestate-error'] } }); const errorHandler = receiveStub.getCall(1).args[1]; errorHandler(new Event('WebSocket', {})); await el.updateComplete; expect(el.shadowRoot.innerHTML).to.contain('join error'); }); });