Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@testing-library/react": "^16.3.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.18.0",
"@vitest/coverage-v8": "^4.0.0-beta.8",
"@vitest/coverage-v8": "^3.2.4",
"fs-extra": "^11.3.1",
"jsdom": "^26.1.0",
"lefthook": "^1.12.3",
Expand All @@ -33,6 +33,6 @@
"tsx": "^4.20.5",
"turbo": "^2.5.6",
"typescript": "^5.9.2",
"vitest": "^4.0.0-beta.8"
"vitest": "^3.2.4"
}
}
110 changes: 110 additions & 0 deletions packages/sqi-web/src/_util/__tests__/dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect, vi } from 'vitest';
import { isDOM, getDOM, getRefDom, getReactNodeRef } from '../dom';
import React from 'react';

describe('dom utilities', () => {
describe('isDOM', () => {
it('should return true for HTMLElement', () => {
const div = document.createElement('div');
expect(isDOM(div)).toBe(true);
});

it('should return true for SVGElement', () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
expect(isDOM(svg)).toBe(true);
});

it('should return false for non-DOM objects', () => {
expect(isDOM(null)).toBe(false);
expect(isDOM(undefined)).toBe(false);
expect(isDOM({})).toBe(false);
expect(isDOM('string')).toBe(false);
expect(isDOM(123)).toBe(false);
});
});

describe('getDOM', () => {
it('should return currentElement for ref objects', () => {
const div = document.createElement('div');
const refObject = { currentElement: div };
expect(getDOM(refObject)).toBe(div);
});

it('should return the node itself if it is a DOM element', () => {
const div = document.createElement('div');
expect(getDOM(div)).toBe(div);
});

it('should return null for non-DOM objects', () => {
expect(getDOM(null)).toBe(null);
expect(getDOM(undefined)).toBe(null);
expect(getDOM({})).toBe(null);
expect(getDOM('string')).toBe(null);
});

it('should return null for objects without currentElement', () => {
const obj = { foo: 'bar' };
expect(getDOM(obj)).toBe(null);
});
});

describe('getRefDom', () => {
it('should return undefined for falsy input', () => {
expect(getRefDom(null as any)).toBeUndefined();
expect(getRefDom(undefined as any)).toBeUndefined();
});

it('should return currentElement.currentElement for special refs', () => {
const div = document.createElement('div');
const ref = { current: { currentElement: div } };
expect(getRefDom(ref)).toBe(div);
});

it('should return current property for regular refs', () => {
const div = document.createElement('div');
const ref = { current: div };
expect(getRefDom(ref)).toBe(div);
});

it('should return undefined when current is falsy', () => {
const ref = { current: null };
expect(getRefDom(ref)).toBeNull();
});
});

describe('getReactNodeRef', () => {
it('should return null for non-elements', () => {
expect(getReactNodeRef(null)).toBe(null);
expect(getReactNodeRef(undefined)).toBe(null);
expect(getReactNodeRef('string')).toBe(null);
expect(getReactNodeRef(123)).toBe(null);
});

it('should return ref based on React version', () => {
// Mock React version
const versionSpy = vi.spyOn(React, 'version', 'get');

// Test React 19+ behavior (ref in props)
versionSpy.mockReturnValue('19.0.0');
const divWithRefProp = React.createElement('div', { ref: 'test-ref' });
expect(getReactNodeRef(divWithRefProp)).toBe('test-ref');

// Test pre-React 19 behavior (ref as property)
versionSpy.mockReturnValue('18.2.0');
const divWithRefProperty = React.createElement('div', {});
expect(getReactNodeRef(divWithRefProperty)).toBeNull();

versionSpy.mockRestore();
});

it('should return null when element has no ref', () => {
const versionSpy = vi.spyOn(React, 'version', 'get');
versionSpy.mockReturnValue('19.0.0');

const divWithoutRef = React.createElement('div', {});
expect(getReactNodeRef(divWithoutRef)).toBe(null);

versionSpy.mockRestore();
});
});
});
203 changes: 203 additions & 0 deletions packages/sqi-web/src/_util/__tests__/ref.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as React from 'react';
import { composeRef, fillRef, useComposeRef, supportRef, supportNodeRef } from '../ref';
import { render, renderHook } from '@testing-library/react';

// 创建一个变量来保存模拟的React版本
let mockReactVersion = '18.0.0';

// 模拟react模块以允许控制version导出
vi.mock('react', async () => {
const actualReact = await vi.importActual('react');
return {
...actualReact,
get version() {
return mockReactVersion;
},
};
});

describe('ref utilities', () => {
beforeEach(() => {
mockReactVersion = '18.0.0';
});

describe('fillRef', () => {
it('should call function ref with node', () => {
const ref = vi.fn();
const node = document.createElement('div');
fillRef(ref, node);
expect(ref).toHaveBeenCalledWith(node);
});

it('should set current property of object ref', () => {
const ref = { current: null };
const node = document.createElement('div');
fillRef(ref, node);
expect(ref.current).toBe(node);
});

it('should not throw error for null ref', () => {
expect(() => fillRef(null, document.createElement('div'))).not.toThrow();
});
});

describe('composeRef', () => {
it('should return undefined when no refs provided', () => {
expect(composeRef()).toBeUndefined();
});

it('should return the only ref when one ref provided', () => {
const ref = vi.fn();
expect(composeRef(ref)).toBe(ref);
});

it('should compose multiple function refs', () => {
const ref1 = vi.fn();
const ref2 = vi.fn();
const node = document.createElement('div');

const composedRef = composeRef(ref1, ref2);
(composedRef as React.RefCallback<HTMLDivElement>)(node);

expect(ref1).toHaveBeenCalledWith(node);
expect(ref2).toHaveBeenCalledWith(node);
});

it('should compose multiple object refs', () => {
const ref1 = React.createRef<HTMLDivElement>();
const ref2 = React.createRef<HTMLDivElement>();
const node = document.createElement('div');

const composedRef = composeRef(ref1, ref2);
(composedRef as React.RefCallback<HTMLDivElement>)(node);

expect(ref1.current).toBe(node);
expect(ref2.current).toBe(node);
});

it('should compose mixed refs', () => {
const ref1 = vi.fn();
const ref2 = React.createRef<HTMLDivElement>();
const node = document.createElement('div');

const composedRef = composeRef(ref1, ref2);
(composedRef as React.RefCallback<HTMLDivElement>)(node);

expect(ref1).toHaveBeenCalledWith(node);
expect(ref2.current).toBe(node);
});
});

describe('useComposeRef', () => {
it('should compose refs with hook', () => {
const ref1 = vi.fn();
const ref2 = React.createRef<HTMLDivElement>();

const TestComponent = () => {
const composedRef = useComposeRef(ref1, ref2);
return (
<div ref={composedRef} data-testid="test">
Test
</div>
);
};

const { getByTestId } = render(<TestComponent />);

expect(ref1).toHaveBeenCalledWith(getByTestId('test'));
expect(ref2.current).toBe(getByTestId('test'));
});

it('should return same ref when refs are not changed', () => {
const ref1 = vi.fn();
const { result, rerender } = renderHook(({ ref1 }) => useComposeRef(ref1), { initialProps: { ref1 } });
const firstRef = result.current;

rerender({ ref1 });
expect(result.current).toBe(firstRef);
});

it('should return new ref when refs changed', () => {
const ref1 = vi.fn();
const ref2 = vi.fn();
const { result, rerender } = renderHook(({ refs }) => useComposeRef(...refs), { initialProps: { refs: [ref1] } });
const firstRef = result.current;

rerender({ refs: [ref1, ref2] });
expect(result.current).not.toBe(firstRef);
});
});

describe('supportRef', () => {
it('should return false for falsy values', () => {
expect(supportRef(null)).toBe(false);
expect(supportRef(undefined)).toBe(false);
});

it('should return true for React 19+ elements', () => {
mockReactVersion = '19.0.0';

const element = React.createElement('div');
expect(supportRef(element)).toBe(true);
});

it('should return false for React 19+ function components without forwardRef', () => {
mockReactVersion = '19.0.0';

const FunctionComponent = () => <div />;
expect(supportRef(FunctionComponent)).toBe(false);
});

it('should return true for forwardRef components', () => {
const ForwardRefComponent = React.forwardRef(() => <div />);
expect(supportRef(ForwardRefComponent)).toBe(true);
});

it('should return true for class components', () => {
class ClassComponent extends React.Component {
render() {
return <div />;
}
}
expect(supportRef(ClassComponent)).toBe(true);
});

it('should return false for function components without forwardRef in React 18', () => {
mockReactVersion = '18.0.0';

const FunctionComponent = () => <div />;
expect(supportRef(FunctionComponent)).toBe(false);
});

it('should handle memo components', () => {
const FunctionComponent = () => <div />;
const MemoComponent = React.memo(FunctionComponent);
expect(supportRef(MemoComponent)).toBe(false);
});
});

describe('supportNodeRef', () => {
it('should return false for non-elements', () => {
expect(supportNodeRef(null)).toBe(false);
expect(supportNodeRef('string')).toBe(false);
expect(supportNodeRef(123)).toBe(false);
});

it('should return true for elements that support ref in React 19+', () => {
mockReactVersion = '19.0.0';

const element = React.createElement('div');
expect(supportNodeRef(element)).toBe(true);
});

it('should return false for elements that do not support ref', () => {
mockReactVersion = '18.0.0';

const FunctionComponent = () => <div />;
const element = React.createElement(FunctionComponent);

expect(supportNodeRef(element)).toBe(false);
});
});
});
Loading