使用 test-library 对单击事件进行单元测试时出现 Vis-js 时间轴错误

问题描述

我在对使用 vis-js 时间轴控件的 React 组件进行单元测试时遇到了一些问题。我得到的错误是“无法读取未定义的属性‘比例’”,调试代码会发生错误,因为 timeaxis 对象应该具有 step 属性,但该属性仅在运行 timeaxis 的重绘方法时设置。我怀疑问题在于测试库生成了没有高度或宽度的 DOM。我本来希望设置 timeaxis 选项来解决此问题,但情况似乎并非如此。这是我的测试方法

const defaultProps: GanttProps = {
    rowData: [],data: [],expanded: false,availabilityItems: [],dragLocked: false,isVisualizerForPrint: false,shownestedOverride: false,textItemsFordisplay: ["Name"],options: {
        start: moment(0).toDate(),end: moment(0).add(1,"year").toDate(),timeAxis: {scale: 'day',step: 30}
    }
};

it("Should call handleClick",async () => {
        const handleClick = jest.fn();
        const props = {
            ...defaultProps,click: handleClick,data: mockData,rowData: mockRowData
        };
        
        render(<Gantt { ...props } />);

        userEvent.click(screen.getByText(/Test Item/i));

        expect(handleClick).toHaveBeenCalledTimes(1);
    });
import * as React from "react";
import ReactDOM from "react-dom";
import * as visTimeline from "vis-timeline";
import * as typeDefs from "./../common/typeDefs";
import moment from "moment";
import "vis-timeline/styles/vis-timeline-graph2d.min.css";

export type SchedTemplateFunc = (item: typeDefs.TaskItem,element: any) => any;

export type SubgroupStackingLookupBuilder = (items: typeDefs.GanttSubgroupData[]) 
    => { [subgroupId: string]: boolean };

const DefaultTimeZone = "America/Edmonton";
const _timelineItemTypes = ["background","Box","point","range"];
const MonthDragScale: visTimeline.TimelineOptions = 
    { timeAxis: { scale: "month" } };
const YearDragScale: visTimeline.TimelineOptions = 
    { timeAxis: { scale: "year" } };
const defaultOptions: visTimeline.TimelineOptions = {
    stack: false,stackSubgroups: true,minHeight: 200,editable: false,verticalScroll: true,zoomable: true,zoomMax: 253206145314,zoomMin: 80927257,zoomKey: "ctrlKey",moveable: true,orientation: "top",margin: {
        item: { horizontal: 0,vertical: 0 }
    },timeAxis: { scale: "day" }
};

type GanttProps = {
    data?: typeDefs.TaskItem[] | null;
    rowData?: typeDefs.GanttRowData[] | null;
    subGroups?: typeDefs.GanttSubgroupData[] | null;
    availabilityItems?: typeDefs.AvailabilityItem[] | null;
    parentSelector?: string;
    dragDirection?: string;
    dragLocked?: boolean;
    expanded?: boolean;
    isVisualizerForPrint?: boolean;
    shownestedOverride?: boolean;
    timescaleSelector?: string | null;
    textItemsFordisplay?: string[];
    timezone?: string | undefined;
    options?: visTimeline.TimelineOptions | undefined;
    timelineRange?: typeDefs.Range | undefined;
    click?: (event: string,properties: any) => void;
    doubleClick?: (event: React.MouseEvent<HTMLElement>) => void;
    changed?: () => void;
    select?: (items: visTimeline.IdType[],event?: any) => void;
    rangeChange?: (properties?: typeDefs.RangeProperties) => void;
    rangeChanged?: (properties?: typeDefs.RangeProperties) => void;
    subgroupStackingLookup?: SubgroupStackingLookupBuilder;
    focusedItem? : string;
};

const Gantt = (props: GanttProps) => {
    const [loaded,setLoaded] = React.useState(false);
    const [items,setItems] = React.useState<visTimeline.DataItemCollectionType>([]);
    const [groups,setGroups] = React.useState<visTimeline.DataGroupCollectionType>([]);
    const [range,setRange] = React.useState<typeDefs.Range>()

    let timeline: visTimeline.Timeline | undefined = undefined;
    let timelineNode: HTMLdivelement | null = null;
    let invisibleInput: HTMLInputElement | null = null;
    let hideColumnsTimer: number = 0;

    const timelineRef = React.useRef<visTimeline.Timeline | undefined>(timeline);

    const deferredLoad = _.debounce(() => setLoaded(true),200);

    const createTimeline = (): void => {
        if (timelineNode && !timelineRef.current) {
            let tlOptions = {
                ...defaultOptions,...props.options,autoResize: true,moveable: !props.dragLocked
            };

            if (tlOptions.height === undefined) {
                delete tlOptions.height;
            }

            if (tlOptions.maxHeight === undefined) {
                delete tlOptions.maxHeight;
            }

            timeline = new visTimeline.Timeline(
                timelineNode,items ?? [],groups,tlOptions
            );

            timelineRef.current = timeline;

            timelineRef.current.on("doubleClick",doubleClickHandler);
            timelineRef.current.on("changed",changedHandler);
            timelineRef.current.on("select",selectHandler);
            timelineRef.current.on("rangechanged",rangeChangedHandler);
            timelineRef.current.on("rangechange",rangeChangeHandler);

            timelineRef.current.on("mouseDown",mouseDownHandler);
            timelineRef.current.on("mouseUp",mouseUpHandler);
            timelineRef.current.on("click",(properties) => {
                clickHandler("click",properties);
            });
        } else if (!loaded) {
            deferredLoad();
        }
    }

    /* Start Event Handlers */
    const doubleClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
        ...
    }

    const clickHandler = (event: string,properties: any): void => {
        ...
    }

    const changedHandler = (): void => {
        ...
    };

    const rangeChangedHandler = (properties?: typeDefs.RangeProperties): void => {
        ...
    };

    const rangeChangeHandler = (properties?: typeDefs.RangeProperties): void => {
        ...
    }

    // vanishes the columns on drag so that they don't make dragging choppy
    const mouseDownHandler = (event: visTimeline.TimelineEventPropertiesResult )
        : boolean => 
    {
        ...
    }

    const mouseUpHandler = () : boolean => {
        ...
    }

    const selectHandler = (params: typeDefs.SelectionEventInfo): void => {
        ...
    }

    /* End Event Handlers */

    /**
     * Update the timeline scale options based on current scale.
     * Timeline scale is being switched to auto when the zoom reaches a level where days labels can fit and will revert back to day when zoomed out above that scale level.
     * @returns Void
     */
    const updateOptions = (prevOptions?: visTimeline.TimelineOptions): void => {
        if (!timelineRef.current) {
            return;
        }

        let options: visTimeline.TimelineOptions | null = null;

        if (calcScale > 0) {
            options = {
                showMinorLabels: true
                
            };
        }

        if (prevOptions && prevOptions!.multiselect !== props.options!.multiselect) {
            options = { ...options,multiselect: props.options!.multiselect };
        }

        if (options) {
            timelineRef.current.setoptions(options);
        }
    }

    // ---------- start effect hooks
    React.useEffect(() => {
        createTimeline();

        if(props.timescaleSelector) {
            createTimescale();
        }

        // find the nearest focusable grid target in the parent element
        if (timelineNode) {
            const parent = timelineNode.parentElement;
            if (parent !== null) {
                const grandparent = parent.parentElement;
                if (grandparent !== null) {
                    invisibleInput = grandparent.querySelector(
                        `.form-control[type="text"]`
                    );
                }
            }
        }

        return () => {
            if (timelineRef.current) {
                timelineRef.current.destroy();
            }
        }
    },[]);

    React.useEffect(() => {
        setItems(() => [...]);
   },[props.data]);

    React.useEffect(() => {
        setGroups([...]);
    },[props.rowData]);

    React.useEffect(() => {
        removeResizeLeftDiv();

        if(timelineRef.current)
            timelineRef.current.setItems(items);
    },[items]);

    React.useEffect(() => {
        removeResizeLeftDiv();
        
        if(timelineRef.current)
            timelineRef.current.setData({
                groups: groups,items: items
            });
    },[groups]);

    React.useEffect(() => {
        if(!range) {
            return;
        }

        if (
            timelineRef.current && props.options &&
            props.options.maxHeight
        ) {
            timelineRef.current.setoptions({
                maxHeight: props.options!.maxHeight
            });
        }
        updateOptions();

        if(range?.start && range?.end)
        {
            if(timelineRef.current)
                timelineRef.current.setwindow(
                    range.start,range.end,{
                        animation: true
                    });
        }

    },[range]);

    React.useEffect(() => {
        if(props?.timelineRange)
        {
            setRange(props.timelineRange);
        }
    },[props.timelineRange])

    React.useEffect(() => {

        if(props.focusedItem && timelineRef.current)
        {
            timelineRef.current.focus(props.focusedItem,{animation: false,zoom: false});
        }
    },[props.focusedItem])

    // ---------- end effect hooks

    return (
        <div ref={el => (timelineNode = el)} />
    );
}

export default Gantt; export type { GanttProps };

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)