Search code examples
javascriptcyclejsxstream-js

Unable to stream events within a mocked DOM source in CycleJs test


Given an isolated component built with CycleJs (the component works fine) :

import isolate from '@cycle/isolate';
import { div, ul, li, a } from '@cycle/dom';

function intent(domSource){
    const changeString$ =  domSource
        .select('.string').events('click')
        .map( ev => ev.target.dataset.position);

    return { changeString$ };
}

function model(actions, props$){

    const { changeString$ } = actions;

    return props$.map( props => {

        return changeString$
            .startWith(props.initialString)
            .map( activePosition => ({ activePosition, preset : props.preset }));

    }).flatten().remember();
}

function view(state$){
    return state$.map( state => (
        div(
            ul(
                state.preset.map( (string, position) => (
                    li(
                        a({
                            attrs : {
                                href : '#',
                                'data-pitch' : string.pitch,
                                'data-frequency' : string.frequency,
                                'data-position' : position,
                                class : ['string', parseInt(state.activePosition, 10) === position ? 'active' : ''].join(' ')
                            }
                        },
                        string.name)
                    )
                ))
            )
        )
    ));
}

function stringSelector(sources){
    const actions = intent(sources.DOM);
    const state$  = model(actions, sources.props);
    const vdom$   = view(state$);

    return {
        DOM: vdom$,
        value: state$
    };
}

export default isolate(stringSelector, '.string-selector');

I have tried to test the behavior using @cycle/time :

import test from 'tape';
import xs   from 'xstream';
import { mockDOMSource } from '@cycle/dom';
import { mockTimeSource } from '@cycle/time';
import stringSelector from '../../../src/js/component/stringSelector/index.js';

test('string selector', t => {

    t.plan(1);

    const Time = mockTimeSource();

    const e2Click$       = Time.diagram('-------x-------|');
    const a2Click$       = Time.diagram('---x------x----|');
    const expectedState$ = Time.diagram('0--1---0--1----|');

    const DOM = mockDOMSource({
        '.string[data-picth=e2]': {
            click: e2Click$
        },
        '.string[data-picth=a2]': {
            click: a2Click$
        },
    });

    const selector = stringSelector({
        DOM,
        props: xs.of({
            preset: {
                strings: [{
                    name: 'E',
                    pitch: 'e2',
                    frequency: 82.41
                }, {
                    name: 'A',
                    pitch: 'a2',
                    frequency: 110.0
                }]
            },
            initialString: 0
        })
    });

    const activePosition$ = selector.value.map( state => state.activePosition );

    Time.assertEqual(activePosition$, expectedState$);

    Time.run(t.end.bind(t));
});

But the activePosition$ stream ends directly. I don't know if it comes from the way the DOM is mocked (the events doesn't seems to be triggered) or the way I build the activePosition$ stream ?

When running my test, I have the following message :

Expected

0--1---0--1----|

Got

(0|)

Failed because:

 * Length of actual and expected differs
 * Expected type next at time 0 but got complete
 * Expected stream to complete at 60 but completed at 0

Solution

  • I think I spotted the problem.

    The thing is, with the mocked DOM driver, you need to create event for the exact same selector as the one used in DOM.select('...').events.

    In your case you select .string but you mock an event on .string[data-pitch=..], which will then not match on the app's side.

    Keep in mind that there is no real DOM involved on the testing side. In your case, when you fake a click on the selector .string, no matter what your component renders, it will only generate one event.

    I think what you want to achieve here is to fake the data that is in the event.

    You can probably do something like this:

    const clicks = Time.diagram('---0---1----0---|').map(position => {
        // this will create a fake DOM event that contains the properties you are reading
        return {target: {dataset: {position: position }}}
    })
    
    const DOM = mockDOMSource({
        '.string': {
            click: clicks
        }
    });