精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

全網最細:Jest+Enzyme測試React組件(包含交互、DOM、樣式測試)

開發 前端
在為組件添加prop傳值之前,可配置一個基礎的 mountTest.tsx 來對組件進行一個基礎渲染掛載測試,測試通過后在進行復雜情況下的測試。

介紹

Jest是目前前端工程化下單元測試火熱的技術棧,而Enzyme的支持提供了Jest測試React業務、組件的能力,下面來介紹一下React組件測試的一些實際場景。

1. 測試依賴包

"enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.2",
    "enzyme-to-json": "^3.3.5",
    "jest": "^28.1.1",
    "jest-less-loader": "^0.1.2",
    "jsdom": "^19.0.0",   //解決mount渲染組件失敗的BUG,具體見上文
    "ts-jest": "^28.0.5",

2. 測試環境搭建

由于enzyme的配置在每次需要測試組件時都需要加入,因此配置setup.js后在每次測試組件中提前引入是不錯的選擇。

setup.js:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
const jsdom = require('jsdom');

//解決無法mount渲染組件的問題
const { JSDOM } = jsdom;
const { window } = new JSDOM('');
const { document } = new JSDOM(``).window;

global.document = document;
global.window = window;

//初始化配置
Enzyme.configure({
  adapter: new Adapter(),
});

export default Enzyme;

jest.config.js配置:

module.exports = {
  transform: {
    '^.+\\.(ts|tsx|js|jsx)?$': 'ts-jest',
    '\\.(less|css)$': 'jest-less-loader', // 支持less
  },

  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
  
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

3. 組件基礎渲染測試

在為組件添加prop傳值之前,可配置一個基礎的 mountTest.tsx 來對組件進行一個基礎渲染掛載測試,測試通過后在進行復雜情況下的測試。

mountTest.tsx

import React from 'react';
import { mount } from 'enzyme';

// 此處Component的類型存在疑問,待完善
export default function mountTest(Component: React.ComponentType<any" data-textnode-index-1701226829723="160" data-index-1701226829723="1307" data-index-len-1701226829723="1307" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> | React.ComponentType) {
  describe(`mount and unmount`, () =" data-textnode-index-1701226829723="167" data-index-1701226829723="1369" data-index-len-1701226829723="1369" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    it(`component could be updated and unmounted without errors`, () =" data-textnode-index-1701226829723="173" data-index-1701226829723="1442" data-index-len-1701226829723="1442" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      const wrapper = mount(<Component /" data-textnode-index-1701226829723="177" data-index-1701226829723="1485" data-index-len-1701226829723="1485" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
      expect(() =" data-textnode-index-1701226829723="180" data-index-1701226829723="1505" data-index-len-1701226829723="1505" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
        wrapper.setProps({});
        wrapper.unmount();
      }).not.toThrow();
    });
  });
}

4. 組件交互相關測試

Button按鈕組件測試

這里拿Button按鈕舉例,具體Button組件可在http://react-view-ui.com:92/#/common/button參考,底部描述了組件的API能力。

圖片圖片

先看一下Button組件的整體測試文件,我一共分成了4組測試用例(不包含mountTest基礎測試)。

Button.test.tsx

import React from 'react';
import Button from '../../Button/index';
import Enzyme from '../setup';
import mountTest from '../mountTest';
import { act } from 'react-dom/test-utils';

const { shallow, mount } = Enzyme;

mountTest(Button);

describe(`button`, () =" data-textnode-index-1701226829723="230" data-index-1701226829723="2025" data-index-len-1701226829723="2025" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
  it('button children show correctly', () =" data-textnode-index-1701226829723="236" data-index-1701226829723="2071" data-index-len-1701226829723="2071" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //按鈕文字內容測試
    const component = shallow(<Button" data-textnode-index-1701226829723="242" data-index-1701226829723="2125" data-index-len-1701226829723="2125" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>testButton</Button" data-textnode-index-1701226829723="243" data-index-1701226829723="2144" data-index-len-1701226829723="2144" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    const button = component.find('.button');
    const p = button.find('button');
    expect(p.text()).toBe('testButton');
  });
  it('click callback correctly', () =" data-textnode-index-1701226829723="248" data-index-1701226829723="2310" data-index-len-1701226829723="2310" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //按鈕點擊回調測試
    const mockFn = jest.fn();
    const component = shallow(<Button handleClick={mockFn} /" data-textnode-index-1701226829723="253" data-index-1701226829723="2416" data-index-len-1701226829723="2416" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    const button = component.find('.button');
    button.simulate('click');
    const mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);

    act(() =" data-textnode-index-1701226829723="270" data-index-1701226829723="2596" data-index-len-1701226829723="2596" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      //測禁用按鈕
      component.setProps({
        disabled: true,
      });
    });

    button.simulate('click');
    expect(mockFn.mock.calls.length).toBe(mockFnCallLength);
  });

  it('button type set show correctly color', () =" data-textnode-index-1701226829723="289" data-index-1701226829723="2820" data-index-len-1701226829723="2820" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試按鈕type被賦值className
    const component = mount(<Button type="primary" /" data-textnode-index-1701226829723="299" data-index-1701226829723="2901" data-index-len-1701226829723="2901" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('button').hasClass('primary')).toBe(true);
  });

  it('button loading show correctly', () =" data-textnode-index-1701226829723="312" data-index-1701226829723="3019" data-index-len-1701226829723="3019" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試加載按鈕顯示
    const component = mount(<Button type="primary" loading /" data-textnode-index-1701226829723="322" data-index-1701226829723="3096" data-index-len-1701226829723="3096" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('loading1')).not.toBeUndefined();
  });
});

從代碼中可以看到,初始化配置一共有如下代碼:

import React from 'react';
import Button from '../../Button/index';
import Enzyme from '../setup';
import mountTest from '../mountTest';
import { act } from 'react-dom/test-utils';

const { shallow, mount } = Enzyme;

mountTest(Button);

主要功能:引入必要的包、引入測試組件、引入組件渲染方式,這是是shallow和mount兩種,并在最后優先進行了組件基礎渲染測試。

第一組測試用例測試了Button按鈕的文字顯示正確性,是通過jest的find方法查詢到Button按鈕的DOM元素進行判斷;之后設置了組件的disabled屬性,再次進行點擊測試

it('button children show correctly', () =" data-textnode-index-1701226829723="372" data-index-1701226829723="3612" data-index-len-1701226829723="3612" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //按鈕文字內容測試
    const component = shallow(<Button" data-textnode-index-1701226829723="378" data-index-1701226829723="3666" data-index-len-1701226829723="3666" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>testButton</Button" data-textnode-index-1701226829723="379" data-index-1701226829723="3685" data-index-len-1701226829723="3685" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    const button = component.find('.button');
    const p = button.find('button');
    expect(p.text()).toBe('testButton');
});

第二組測試用例測試了按鈕的交互,在渲染組件之后,捕捉到按鈕的DOM,并自定義了mockFn函數傳遞給實際Button組件后進行回調測試,Button我在點擊時是沒有傳參的,因此回調參數長度為0

it('click callback correctly', () =" data-textnode-index-1701226829723="389" data-index-1701226829723="3943" data-index-len-1701226829723="3943" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //按鈕點擊回調測試
    const mockFn = jest.fn();
    const component = shallow(<Button handleClick={mockFn} /" data-textnode-index-1701226829723="398" data-index-1701226829723="4049" data-index-len-1701226829723="4049" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    const button = component.find('.button');
    button.simulate('click');
    const mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);

    act(() =" data-textnode-index-1701226829723="415" data-index-1701226829723="4229" data-index-len-1701226829723="4229" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      //測禁用按鈕
      component.setProps({
        disabled: true,
      });
    });

    button.simulate('click');
    expect(mockFn.mock.calls.length).toBe(mockFnCallLength);
});

第三組測試用例對Button按鈕類型進行了測試,傳遞了type:primary,并對渲染后的組件進行判斷是否有primary的類名

it('button type set show correctly color', () =" data-textnode-index-1701226829723="435" data-index-1701226829723="4514" data-index-len-1701226829723="4514" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試按鈕type被賦值className
    const component = mount(<Button type="primary" /" data-textnode-index-1701226829723="445" data-index-1701226829723="4595" data-index-len-1701226829723="4595" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('button').hasClass('primary')).toBe(true);
});

第四組測試用例對loading Button進行了測試,同樣也是檢查類名的形式,與第三組測試用例類似

it('button loading show correctly', () =" data-textnode-index-1701226829723="459" data-index-1701226829723="4759" data-index-len-1701226829723="4759" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試加載按鈕顯示
    const component = mount(<Button type="primary" loading /" data-textnode-index-1701226829723="469" data-index-1701226829723="4836" data-index-len-1701226829723="4836" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('loading1')).not.toBeUndefined();
  });

這就是我對Button的測試。

Avatar頭像組件測試

由于Button組件本身功能比較簡單,可擴展性有限,作為第一個組件案例進行舉例。

接下來對Avatar組件進行測試。

圖片圖片

還是先上測試源碼。

Avatar.test.tsx:

import React, { ReactNode } from 'react';
import ReactDOM from 'react-dom';
import Avatar from '../../Avatar/index';
import AvatarGroup from '../../Avatar/group';
import { CameraOutlined } from '@ant-design/icons';
import Enzyme from '../setup';
import mountTest from '../mountTest';
import { act } from 'react-dom/test-utils';

const { mount } = Enzyme;

let container: HTMLDivElement | null;

mountTest(Avatar);

describe('Avatar', () =" data-textnode-index-1701226829723="539" data-index-1701226829723="5435" data-index-len-1701226829723="5435" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
  //測試前準備容器
  beforeEach(() =" data-textnode-index-1701226829723="545" data-index-1701226829723="5466" data-index-len-1701226829723="5466" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    container = document.createElement('div');
    document.body.appendChild(container);
  });
  //測試后刪除容器
  afterEach(() =" data-textnode-index-1701226829723="560" data-index-1701226829723="5588" data-index-len-1701226829723="5588" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    document.body.removeChild(container as HTMLDivElement);
    container = null;
  });

  it('test avatar children content show correctly', () =" data-textnode-index-1701226829723="575" data-index-1701226829723="5732" data-index-len-1701226829723="5732" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試頭像文本顯示
    let contextText: string | ReactNode = 'test';
    const component = mount(<Avatar" data-textnode-index-1701226829723="588" data-index-1701226829723="5833" data-index-len-1701226829723="5833" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>{contextText}</Avatar" data-textnode-index-1701226829723="589" data-index-1701226829723="5855" data-index-len-1701226829723="5855" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('.text-ref').text()).toEqual('test');
    const imgSrc =
      'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png';
    act(() =" data-textnode-index-1701226829723="605" data-index-1701226829723="6059" data-index-len-1701226829723="6059" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      contextText = <img src={imgSrc}" data-textnode-index-1701226829723="606" data-index-1701226829723="6099" data-index-len-1701226829723="6099" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="606" data-index-1701226829723="6105" data-index-len-1701226829723="6105" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>;
    });
    expect(component.find('img')).toBeDefined();
  });
  it('test avatar group correctly', () =" data-textnode-index-1701226829723="614" data-index-1701226829723="6207" data-index-len-1701226829723="6207" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試頭像樣式
    const component = (
      <AvatarGroup size={50} groupStyle={{ margin: '0 10px' }}" data-textnode-index-1701226829723="622" data-index-1701226829723="6307" data-index-len-1701226829723="6307" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(20, 169, 248)' }} shape="square"" data-textnode-index-1701226829723="631" data-index-1701226829723="6382" data-index-len-1701226829723="6382" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
          View
        </Avatar" data-textnode-index-1701226829723="633" data-index-1701226829723="6413" data-index-len-1701226829723="6413" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(51, 112, 255)' }}" data-textnode-index-1701226829723="642" data-index-1701226829723="6473" data-index-len-1701226829723="6473" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>React</Avatar" data-textnode-index-1701226829723="642" data-index-1701226829723="6487" data-index-len-1701226829723="6487" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(0, 208, 184)' }}" data-textnode-index-1701226829723="651" data-index-1701226829723="6546" data-index-len-1701226829723="6546" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>UI</Avatar" data-textnode-index-1701226829723="651" data-index-1701226829723="6557" data-index-len-1701226829723="6557" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
      </AvatarGroup" data-textnode-index-1701226829723="652" data-index-1701226829723="6577" data-index-len-1701226829723="6577" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
    );

    act(() =" data-textnode-index-1701226829723="654" data-index-1701226829723="6596" data-index-len-1701226829723="6596" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      ReactDOM.render(component, container);
    });

    const avatarStyleList = [
      {
        background: 'rgb(20, 169, 248)',
        content: 'View',
      },
      {
        background: 'rgb(51, 112, 255)',
        content: 'React',
      },
      {
        background: 'rgb(0, 208, 184)',
        content: 'UI',
      },
    ];
    const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement;
    expect(groupDom.childElementCount).toBe(3);

    const avatars = Array.from((container as HTMLDivElement).querySelectorAll('.avatar'));
    avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="708" data-index-1701226829723="7191" data-index-len-1701226829723="7191" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      //測試頭像組的每個頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測試頭像形狀
        expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true);
      }
    });
  });

  it('test avatar click callback correctly', () =" data-textnode-index-1701226829723="730" data-index-1701226829723="7653" data-index-len-1701226829723="7653" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //頭像點擊交互測試
    const mockFn = jest.fn();
    const component = mount(
      <Avatar
        size={54}
        triggerType="mask"
        triggerIcon={<CameraOutlined style={{ fontSize: '20px' }} /" data-textnode-index-1701226829723="740" data-index-1701226829723="7850" data-index-len-1701226829723="7850" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>}
        triggerClick={mockFn}
      " data-textnode-index-1701226829723="742" data-index-1701226829723="7887" data-index-len-1701226829723="7887" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <img src="https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png"" data-textnode-index-1701226829723="743" data-index-1701226829723="8006" data-index-len-1701226829723="8006" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="743" data-index-1701226829723="8012" data-index-len-1701226829723="8012" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
      </Avatar" data-textnode-index-1701226829723="744" data-index-1701226829723="8027" data-index-len-1701226829723="8027" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>,
    );
    act(() =" data-textnode-index-1701226829723="746" data-index-1701226829723="8047" data-index-len-1701226829723="8047" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      component.simulate('click');
    });
    let mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);
    act(() =" data-textnode-index-1701226829723="753" data-index-1701226829723="8192" data-index-len-1701226829723="8192" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      component.setProps({
        triggerType: 'button',
      });
    });
    component.update();
    mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);
  });
});

拆解一下組件的源碼,測試最初的操作如下:

import React, { ReactNode } from 'react';
import ReactDOM from 'react-dom';
import Avatar from '../../Avatar/index';
import AvatarGroup from '../../Avatar/group';
import { CameraOutlined } from '@ant-design/icons';
import Enzyme from '../setup';
import mountTest from '../mountTest';
import { act } from 'react-dom/test-utils';

const { mount } = Enzyme;

let container: HTMLDivElement | null;

mountTest(Avatar);

和Button的測試區別點其實就在,定義了container容器,用于接下來的DOM測試。

//測試前準備容器
  beforeEach(() =" data-textnode-index-1701226829723="825" data-index-1701226829723="8874" data-index-len-1701226829723="8874" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    container = document.createElement('div');
    document.body.appendChild(container);
  });
  //測試后刪除容器
  afterEach(() =" data-textnode-index-1701226829723="840" data-index-1701226829723="8996" data-index-len-1701226829723="8996" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    document.body.removeChild(container as HTMLDivElement);
    container = null;
  });

在進行測試用例之前,創建了一個空div作為React測試的容器,放置React組件,并在測試用例結束后對該容器進行清除。

接下來我們開始分析測試用例:

第一組測試用例測試了文本頭像和圖片頭像的顯示正確性,首先給組件傳遞了一個test文本值,對文本值進行判斷。之后又給組件傳遞了一張圖片(ReactNode),并對組件中的圖片進行查詢判斷。

it('test avatar children content show correctly', () =" data-textnode-index-1701226829723="858" data-index-1701226829723="9305" data-index-len-1701226829723="9305" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試頭像文本顯示
    let contextText: string | ReactNode = 'test';
    const component = mount(<Avatar" data-textnode-index-1701226829723="871" data-index-1701226829723="9406" data-index-len-1701226829723="9406" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>{contextText}</Avatar" data-textnode-index-1701226829723="872" data-index-1701226829723="9428" data-index-len-1701226829723="9428" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>);
    expect(component.find('.text-ref').text()).toEqual('test');
    const imgSrc =
      'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png';
    act(() =" data-textnode-index-1701226829723="888" data-index-1701226829723="9632" data-index-len-1701226829723="9632" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      contextText = <img src={imgSrc}" data-textnode-index-1701226829723="889" data-index-1701226829723="9672" data-index-len-1701226829723="9672" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="889" data-index-1701226829723="9678" data-index-len-1701226829723="9678" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>;
    });
    expect(component.find('img')).toBeDefined();
});

第二組測試用例較為復雜,沒有通過jest的渲染方式渲染組件,而是用上了之前所講到的container容器,并且創建了一個React虛擬DOM,渲染在測試用例環境中。這樣做其實也是因為測試用例本身是需要測試不同情況下的頭像樣式是否生效,因此會用到這種渲染方式。

it('test avatar group correctly', () =" data-textnode-index-1701226829723="900" data-index-1701226829723="9905" data-index-len-1701226829723="9905" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //測試頭像樣式
    const component = (
      <AvatarGroup size={50} groupStyle={{ margin: '0 10px' }}" data-textnode-index-1701226829723="911" data-index-1701226829723="10005" data-index-len-1701226829723="10005" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(20, 169, 248)' }} shape="square"" data-textnode-index-1701226829723="916" data-index-1701226829723="10080" data-index-len-1701226829723="10080" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
          View
        </Avatar" data-textnode-index-1701226829723="919" data-index-1701226829723="10111" data-index-len-1701226829723="10111" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(51, 112, 255)' }}" data-textnode-index-1701226829723="920" data-index-1701226829723="10171" data-index-len-1701226829723="10171" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>React</Avatar" data-textnode-index-1701226829723="921" data-index-1701226829723="10185" data-index-len-1701226829723="10185" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <Avatar style={{ background: 'rgb(0, 208, 184)' }}" data-textnode-index-1701226829723="924" data-index-1701226829723="10244" data-index-len-1701226829723="10244" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>UI</Avatar" data-textnode-index-1701226829723="925" data-index-1701226829723="10255" data-index-len-1701226829723="10255" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
      </AvatarGroup" data-textnode-index-1701226829723="927" data-index-1701226829723="10275" data-index-len-1701226829723="10275" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
    );

    act(() =" data-textnode-index-1701226829723="931" data-index-1701226829723="10294" data-index-len-1701226829723="10294" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      ReactDOM.render(component, container);
    });

    const avatarStyleList = [
      {
        background: 'rgb(20, 169, 248)',
        content: 'View',
      },
      {
        background: 'rgb(51, 112, 255)',
        content: 'React',
      },
      {
        background: 'rgb(0, 208, 184)',
        content: 'UI',
      },
    ];
    const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement;
    expect(groupDom.childElementCount).toBe(3);

    const avatars = Array.from((container as HTMLDivElement).querySelectorAll('.avatar'));
    avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="987" data-index-1701226829723="10889" data-index-len-1701226829723="10889" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      //測試頭像組的每個頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測試頭像形狀
        expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true);
      }
    });
  });

通過ReactDOM.render渲染后,首先獲取了所有頭像的最外層容器:groupDom,并對頭像組所包含的頭像元素長度進行判斷,我這里是傳了三個頭像,因此預期應該為3。

const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement;
expect(groupDom.childElementCount).toBe(3);

接下來獲取了所有頭像的DOM,并進行遍歷判斷,判斷自定義的頭像背景顏色和所傳文本內容是否相同,兩者都滿足,則該頭像的測試通過;并在我對第一個頭像設置了shape: square,這代表了這是一個方形頭像,因此在遍歷中需要對第一個頭像單獨做一次測試,判斷它的樣式是否生效(圓角)

avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="1042" data-index-1701226829723="11695" data-index-len-1701226829723="11695" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      //測試頭像組的每個頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測試頭像形狀
        expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true);
      }
    });

如上就是第二組測試用例,和之前測試用例不同的無非就是渲染方式和組件的樣式判斷,使用了原生的一些判斷,最后通過jest的toBe方法進行斷言。

第三組測試用例是交互測試,在對頭像設置了triggerIcon、triggerType、triggerClick后可變成交互頭像,具體顯示可查看組件庫文檔-Avatar頭像。這里也是先定義了一個mock函數,傳遞給組件作為回調函數測試,并且整體測試了mask、button兩種交互頭像的回調正確性

it('test avatar click callback correctly', () =" data-textnode-index-1701226829723="1088" data-index-1701226829723="12368" data-index-len-1701226829723="12368" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
    //頭像點擊交互測試
    const mockFn = jest.fn();
    const component = mount(
      <Avatar
        size={54}
        triggerType="mask"
        triggerIcon={<CameraOutlined style={{ fontSize: '20px' }} /" data-textnode-index-1701226829723="1106" data-index-1701226829723="12565" data-index-len-1701226829723="12565" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>}
        triggerClick={mockFn}
      " data-textnode-index-1701226829723="1108" data-index-1701226829723="12602" data-index-len-1701226829723="12602" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
        <img src="https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png"" data-textnode-index-1701226829723="1111" data-index-1701226829723="12721" data-index-len-1701226829723="12721" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="1112" data-index-1701226829723="12727" data-index-len-1701226829723="12727" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>
      </Avatar" data-textnode-index-1701226829723="1114" data-index-1701226829723="12742" data-index-len-1701226829723="12742" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>,
    );
    act(() =" data-textnode-index-1701226829723="1118" data-index-1701226829723="12762" data-index-len-1701226829723="12762" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      component.simulate('click');
    });
    let mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);
    act(() =" data-textnode-index-1701226829723="1132" data-index-1701226829723="12907" data-index-len-1701226829723="12907" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> {
      component.setProps({
        triggerType: 'button',
      });
    });
    component.update();
    mockFnCallLength = mockFn.mock.calls.length;
    expect(mockFnCallLength).toBe(0);
  });

如上就是頭像組件的所有測試用例。

小結

測試React組件無非就是測試其交互性和樣式渲染正確性,因此筆者在React組件測試中使用最頻繁的就是文中所述的兩種渲染形式

  • Jest渲染(mount、render、shallow)
  • ReactDOM渲染(用于測試樣式、元素節點)

因此掌握了這兩種渲染形式去書寫測試用例,可以測試到大部分的組件業務場景,在組件上線之前mock出更多的場景來避免錯誤發生。

責任編輯:武曉燕 來源: 量子前端
相關推薦

2017-03-21 21:37:06

組件UI測試架構

2022-10-26 08:00:49

單元測試React

2020-03-19 14:50:31

Reac單元測試前端

2021-06-26 07:40:21

前端自動化測試Jest

2024-05-09 16:21:46

Deepseek技術算法

2023-05-18 14:01:00

前端自動化測試

2021-10-12 19:16:26

Jest單元測試

2022-02-04 22:18:28

React路由應用

2025-04-18 16:05:39

2012-09-03 13:51:43

測試軟件測試單元測試

2021-02-26 15:10:00

前端React組件交互

2022-07-01 08:00:00

自動處理Mockoon測試

2023-11-08 13:18:00

JestJavaScript框架

2022-08-09 13:08:27

VitestJest前端

2013-04-08 09:28:09

測試

2022-04-14 08:00:00

Cypress測試開發

2017-01-01 09:43:40

2010-01-20 16:13:15

2023-04-09 15:08:20

Cypress組件測試

2012-04-09 10:07:08

JavaScript
點贊
收藏

51CTO技術棧公眾號

亚洲国产日韩a在线播放| 首页亚洲欧美制服丝腿| 日韩天堂在线观看| 97在线国产视频| 五月天激情开心网| 日本成人在线不卡视频| 久久久av电影| 亚洲熟女乱综合一区二区三区| 美女福利一区二区三区| 中文字幕一区二区在线播放| 国产精品免费区二区三区观看| 亚州国产精品视频| 色喇叭免费久久综合| 欧美mv日韩mv国产| www.欧美日本| 日本h片在线观看| 久久香蕉国产线看观看99| 成人h视频在线观看播放| 精品视频一区二区在线观看| 国产一区二区在线| 精品免费视频一区二区| 密臀av一区二区三区| 男女在线视频| 中文字幕在线不卡一区| 欧美13一14另类| 日韩在线视频免费观看| 黑人糟蹋人妻hd中文字幕 | 日韩视频国产视频| 人妻无码视频一区二区三区| av有码在线观看| 国产精品日日摸夜夜摸av| 精品中文字幕一区| 亚洲爱爱综合网| 久久99精品久久久| 国产福利视频一区二区| 国产无码精品久久久| 欧美~级网站不卡| 日韩在线观看免费网站| 亚洲精品国产一区黑色丝袜| 久久97久久97精品免视看秋霞| 6080亚洲精品一区二区| 中文字幕网av| 99精品在免费线偷拍| 欧美性xxxx在线播放| 国产美女主播在线| 污视频网站在线免费| 自拍av一区二区三区| 在线观看日韩羞羞视频| 阿v免费在线观看| 久久九九99视频| 久久综合九色欧美狠狠| 污视频软件在线观看| 成人国产精品免费观看动漫| 俄罗斯精品一区二区| 国产精品无码一区二区桃花视频| 美女在线观看视频一区二区| 国产精品普通话| 中文字幕av第一页| 日本亚洲一区二区| 国产成人小视频在线观看| √资源天堂中文在线| 久久国产成人| 日本成人激情视频| 亚洲天堂男人av| 老司机精品导航| 国产成人精品日本亚洲| 日韩乱码一区二区三区| 人人精品人人爱| 国产精品入口尤物| 91精品国产乱码久久久| 国产一区二区欧美| 在线精品视频在线观看高清| 最好看的2019的中文字幕视频| 国产中年熟女高潮大集合| 欧美在线关看| 亚洲欧洲高清在线| 永久免费av无码网站性色av| 日韩理论电影大全| 麻豆国产精品va在线观看不卡 | 6080yy精品一区二区三区| 欧美日韩国产精品综合| 悠悠资源网久久精品| 91精品国产91久久久久久| 亚洲精品成人在线视频| 日本不卡123| 亚洲a∨日韩av高清在线观看| 国产丰满美女做爰| 99国产精品视频免费观看| 日韩精品一区二区三区外面| 麻豆传媒免费在线观看| 亚洲成人一区二区在线观看| 少妇高清精品毛片在线视频 | 亚洲在线视频观看| 少妇高潮一区二区三区69| 久久精品免视看| www.黄色网址.com| 在线观看v片| 欧美日韩精品一区二区三区四区| 色姑娘综合天天| 首页亚洲中字| 久久精品男人天堂| 欧美三级一区二区三区| 精品亚洲成av人在线观看| 国语精品中文字幕| 久久日韩视频| 色狠狠av一区二区三区| 99热这里只有精品2| 亚洲免费福利一区| 久久999免费视频| 中文字幕av第一页| 99国产精品久久久久久久久久久| 一本一本久久a久久精品综合妖精| 男插女视频久久久| 欧美精选午夜久久久乱码6080| 亚洲一区二区在线免费| 99re6这里只有精品| 97超碰国产精品女人人人爽| 91av久久久| 久久影视一区二区| 国产在线视频在线| 四虎在线精品| 国产亚洲精品高潮| 国产做受高潮漫动| 国产东北露脸精品视频| 亚洲韩国在线| 在线中文字幕播放| 精品91自产拍在线观看一区| 色老板免费视频| 日本vs亚洲vs韩国一区三区二区| 精品综合久久久| ririsao久久精品一区| 欧美精品在线观看一区二区| 国产人妻一区二区| 国产日韩亚洲欧美精品| 国产伦精品一区二区| av在线导航| 538prom精品视频线放| 亚洲AV无码成人精品区明星换面| 99在线精品免费视频九九视| 成人av资源| 欧美videosex性欧美黑吊| 666欧美在线视频| 国产探花在线视频| 美女视频免费一区| 日日噜噜噜噜夜夜爽亚洲精品| 色在线视频观看| 亚洲精品www久久久| 精品视频久久久久| 成人丝袜视频网| 亚洲 欧美 综合 另类 中字| 亚洲日本视频在线| 欧美日本黄视频| 亚洲成人av综合| 一区二区三区加勒比av| 午夜激情视频网| 中国精品18videos性欧美| 91亚洲va在线va天堂va国| 黄色大片在线播放| 日韩欧美美女一区二区三区| 欧美日韩国产精品综合| 国产成人日日夜夜| 欧美高清中文字幕| 欧美男人操女人视频| 97在线观看视频国产| 午夜av免费在线观看| 欧美日韩在线看| 蜜桃无码一区二区三区| 免费在线视频一区| 法国空姐在线观看免费| 91综合精品国产丝袜长腿久久| 久久久久久久久久久网站| 亚洲精品视频网| 欧美视频13p| 91成人精品一区二区| 激情综合色播激情啊| 黄色一级视频播放| 欧美一级色片| 国产女精品视频网站免费| 久久久久久久久免费视频| 欧美不卡激情三级在线观看| 成人毛片18女人毛片| 国产欧美一区二区三区网站| 日本中文字幕观看| 亚洲天堂成人| 日本在线观看一区二区| 涩涩涩久久久成人精品| 久久久久久久一区二区三区| 青青久在线视频免费观看| 欧美日韩性生活| 欧美成人精品欧美一| 91丨porny丨户外露出| 免费看污污网站| 国产精品v日韩精品v欧美精品网站| 国产综合第一页| 国产经典一区| 欧美黑人一级爽快片淫片高清| 亚洲 国产 欧美 日韩| 欧美日韩国产综合一区二区三区| 精品视频久久久久| 国产精品久久看| 波多野结衣有码| 精品在线免费观看| 免费在线激情视频| 欧美体内she精视频在线观看| 久久青青草原一区二区| 国产日韩中文在线中文字幕| 57pao成人国产永久免费| 黄网页在线观看| 亚洲欧洲av一区二区| 超碰人人人人人人| 欧美在线免费视屏| 国产无码精品在线播放| 亚洲天堂2014| 国产sm调教视频| 成人avav影音| 黄色片子免费看| 日本不卡一区二区| 爱福利视频一区二区| 欧美1区2区3区| 亚洲一区二区免费视频软件合集| 亚洲成aⅴ人片久久青草影院| 91精品免费视频| 亚洲精品粉嫩美女一区| 91成人精品网站| 视频在线观看入口黄最新永久免费国产| 亚洲人成人99网站| 污污视频在线免费看| 日韩午夜激情免费电影| 国产又粗又猛视频| 欧美色视频在线| 亚洲av无码精品一区二区| 午夜精品123| 久久久久久久伊人| 尤物视频一区二区| 裸体武打性艳史| **性色生活片久久毛片| 公肉吊粗大爽色翁浪妇视频| 久久精品男人天堂av| 双性尿奴穿贞c带憋尿| 成人aa视频在线观看| 佐佐木明希电影| 国产成人免费在线观看不卡| 三年中文在线观看免费大全中国| 日韩精品一级二级| 日本成人中文字幕在线| 日韩在线一区二区| 妺妺窝人体色www在线观看| 美女诱惑一区| 日本一极黄色片| 日本欧美加勒比视频| jizz欧美激情18| 日韩精彩视频在线观看| 91蝌蚪视频在线观看| 日韩黄色一级片| 欧美一级黄色影院| 日本在线播放一区二区三区| 一道本在线免费视频| 精品在线免费观看| 亚洲三级在线视频| 国产v综合v亚洲欧| 中文字幕精品久久久| 91色porny在线视频| www.狠狠爱| 国产精品视频在线看| 永久av免费网站| 一区二区三区四区视频精品免费| 18精品爽视频在线观看| 亚洲成人免费电影| 亚洲精品中文字幕乱码三区91| 色欧美片视频在线观看在线视频| 一级久久久久久| 欧美喷潮久久久xxxxx| av观看在线免费| 精品第一国产综合精品aⅴ| 天堂中文资源在线观看| 亚洲日韩欧美视频一区| 3d成人动漫在线| 美女av一区二区三区| 第一av在线| 国产成人综合精品| www.久久爱.com| 精品国产乱码久久久久| 精品无人区麻豆乱码久久久| 99热一区二区三区| 亚洲久久一区| 性生活免费在线观看| 国产不卡视频在线播放| 中文字幕在线1| 亚洲私人影院在线观看| 国产无遮无挡120秒| 日本韩国欧美国产| 国产后入清纯学生妹| 亚洲精品久久久久久久久久久久| 第一页在线观看| 欧美精品第一页在线播放| 成人免费福利| 亚洲综合在线小说| 猛男gaygay欧美视频| 潘金莲一级淫片aaaaa免费看| 亚洲欧洲一区二区天堂久久| 中文字幕永久有效| 不卡一区二区三区四区| 九九热久久免费视频| 亚洲成人激情av| 国产精品国产一区二区三区四区| 日韩av在线播放资源| 制服丝袜在线播放| 国产成人自拍视频在线观看| 国产91精品入| 一区二区在线不卡| 翔田千里一区二区| 国产精品91av| 中文字幕色av一区二区三区| 日韩av男人天堂| 欧美一级日韩不卡播放免费| 加勒比一区二区三区在线| 久久久免费在线观看| 成人污污www网站免费丝瓜| 日本三级中国三级99人妇网站 | 国产资源在线视频| 国产一区二区看久久| 中文字幕av久久爽一区| 婷婷开心激情综合| 国产成人久久精品77777综合| 在线亚洲午夜片av大片| 桃色av一区二区| 国产伦精品一区二区三区| 欧美区日韩区| 亚洲精品视频三区| 国产精品人妖ts系列视频| av黄色在线播放| 亚洲国产三级网| 国产网红在线观看| a级国产乱理论片在线观看99| 天天射天天综合网| 欧美日韩中文不卡| 欧美激情一区不卡| 日韩 国产 欧美| 亚洲欧美国产一本综合首页| 涩涩涩在线视频| 久久精品女人的天堂av| 中文亚洲欧美| 给我看免费高清在线观看| 天天色综合成人网| 午夜性色福利影院| 2019中文字幕全在线观看| 女一区二区三区| 黄色一级片播放| 91丨九色丨蝌蚪富婆spa| 成人午夜视频在线播放| 精品一区二区电影| 日韩免费电影| 涩涩涩999| 视频一区免费在线观看| 成人性生交大免费看| 在线一区二区三区| 91美女视频在线| 成人激情视频网| 2023国产精品久久久精品双| 三级黄色片免费观看| 亚洲一区二区三区三| 婷婷五月综合激情| 国产91在线高潮白浆在线观看| 精品精品久久| 小明看看成人免费视频| 一区二区三区鲁丝不卡| 香港三日本三级少妇66| 日韩av成人在线观看| 成人嘿咻视频免费看| 国产无色aaa| 亚洲一二三区在线观看| 三级黄视频在线观看| 国产精品福利网站| 91综合久久一区二区| 韩国三级在线看| 欧美日韩国产区| av在线首页| 91高跟黑色丝袜呻吟在线观看| 在线成人国产| 女女互磨互喷水高潮les呻吟| 91精品国产欧美一区二区18| gogo高清在线播放免费| 你懂的视频在线一区二区| 美女视频一区二区三区| 精品无码黑人又粗又大又长| 亚洲女人天堂网| 国产精品一区免费在线| 国产a级片网站| 中文字幕av资源一区| 亚洲成人av综合| 国产精品69精品一区二区三区| 一区二区免费不卡在线| 特级西西人体4444xxxx| 欧美四级电影网| mm视频在线视频| 一区二区三区四区视频在线| 9人人澡人人爽人人精品| 伊人精品在线视频| 68精品久久久久久欧美| 国产大片一区|