精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

中文字幕22页| 日韩欧美亚洲区| 久久精品人妻一区二区三区| 欧美精品中文| 欧美日韩中文国产| 成人午夜视频在线观看免费| 国产二区在线播放| 国产高清在线精品| 国产suv精品一区二区| 青青操在线视频观看| 果冻天美麻豆一区二区国产| 欧美中文字幕不卡| 精品人妻少妇一区二区| 99视频在线观看地址| av电影一区二区| 成人国产亚洲精品a区天堂华泰| 国产无套在线观看| 久久国产小视频| 亚洲精品v欧美精品v日韩精品| 激情婷婷综合网| 成人三级小说| 日韩欧美极品在线观看| 午夜精品久久久久久久久久久久久 | 黑人粗进入欧美aaaaa| 亚洲综合影视| 亚洲国产经典视频| 亚洲成人av片| 国产伦精品一区二区三| 久久久久精彩视频| 国内精品久久久久久久影视蜜臀| 亚洲自拍小视频免费观看| 大吊一区二区三区| 天天躁日日躁狠狠躁欧美巨大小说| 欧美日韩国产不卡| 91蝌蚪视频在线观看| 99热99re6国产在线播放| 亚洲视频在线一区观看| 午夜一区二区三视频在线观看| 婷婷色在线观看| 国产成人鲁色资源国产91色综| 国产女人18毛片水18精品| 欧美日韩一二三四区| 精品动漫av| 欧美乱妇40p| 国产天堂av在线| 999国产精品| 中文字幕欧美日韩| 69视频在线观看免费| 天堂日韩电影| 日韩国产精品亚洲а∨天堂免| 白丝校花扒腿让我c| 精品国产一区二区三区性色av | 日本天堂免费a| 免费黄网站在线| 国产精品国产自产拍在线| 日韩色妇久久av| 国产在线一在线二| 欧美韩国一区二区| 亚洲一区二区不卡视频| 99青草视频在线播放视| 日韩一区在线看| 18视频在线观看娇喘| 国产精品va在线观看视色 | 中文字幕欧美日韩一区二区三区| 成人在线二区| 亚洲视频网在线直播| 亚洲一区二区三区四区中文| 在线观看a视频| 亚洲欧美日韩系列| www.国产在线播放| 在线黄色的网站| 在线观看欧美精品| 亚洲一区二区偷拍| 91成人福利| 亚洲精品资源在线| 一级在线观看视频| 欧美日韩ab| 91精品成人久久| 亚洲午夜无码久久久久| 久久99精品久久久久久| 91麻豆蜜桃| 婷婷在线免费观看| 国产欧美中文在线| 免费久久久久久| www.九色在线| 欧美少妇xxx| 免费高清视频在线观看| 你懂的在线观看一区二区| 一本色道久久88综合日韩精品| 男人的午夜天堂| 亚洲伦理一区| 91精品久久久久| 天天操天天操天天| 国产精品理论片在线观看| 国产精品无码免费专区午夜| 高清av不卡| 日韩一级精品视频在线观看| 喷水视频在线观看| 欧美xxav| 欧美一级电影在线| 99精品免费观看| 久久久久久久久久久99999| 老汉色影院首页| 欧美日韩大片| 精品国产乱码久久久久久1区2区 | 六月婷婷中文字幕| 国产精品你懂的| 日日橹狠狠爱欧美超碰| www一区二区三区| 亚洲人成电影在线播放| 我家有个日本女人| 免费观看30秒视频久久| 国产日韩一区二区| 超碰在线网址| 欧美调教femdomvk| 丰满少妇高潮一区二区| 国自产拍偷拍福利精品免费一 | 在线观看国产成人| 91麻豆swag| 日韩 欧美 视频| 国产欧美88| 日韩有码在线电影| 日本成人一级片| 久久噜噜亚洲综合| 久久视频这里有精品| 国产一区二区三区免费观看在线| 永久555www成人免费| 国偷自拍第113页| 波多野结衣亚洲一区| 热久久最新地址| www一区二区三区| 最近更新的2019中文字幕| 全部毛片永久免费看| 成人高清伦理免费影院在线观看| 特级黄色录像片| 伦一区二区三区中文字幕v亚洲| 亚洲片在线资源| 毛片毛片女人毛片毛片| 99免费精品视频| 日韩 欧美 视频| 国产精品x8x8一区二区| 久久久久久999| 亚洲成熟女性毛茸茸| 亚洲免费av高清| 久久无码人妻一区二区三区| 亚洲高清影视| 99精品国产高清一区二区| 在线中文字幕视频观看| 日韩一区二区三区视频在线| 丰满少妇被猛烈进入一区二区| 国产麻豆9l精品三级站| 久久久久久久久久久综合| 99re91这里只有精品| 欧美激情女人20p| 丰满岳乱妇国产精品一区| 亚洲国产欧美日韩另类综合| 成年人小视频在线观看| 99这里有精品| 欧美精品一区二区三区在线看午夜| 性感女国产在线| 亚洲天堂av在线免费观看| 艳妇乳肉豪妇荡乳av无码福利 | 国产成人福利夜色影视| 国产午夜精品一区二区三区| 亚洲一区二区三区高清视频| 成人欧美一区二区三区黑人麻豆| 国产在线视频三区| 亚洲欧洲一级| 日本一区二区精品| 国产精品**亚洲精品| 欧美激情一区二区三区高清视频| 亚洲免费国产视频| 色诱亚洲精品久久久久久| 亚洲色图第四色| 国产成人av电影免费在线观看| 青青草精品视频在线| 亚洲宅男网av| 成人福利网站在线观看| 好看的中文字幕在线播放| 亚洲男人天堂2024| 国产精品国产精品国产专区| 亚洲最新视频在线观看| 18禁裸乳无遮挡啪啪无码免费| 美腿丝袜亚洲三区| 男人添女人荫蒂免费视频| 自拍偷拍精品| 91亚洲精品一区二区| 电影在线观看一区| 日韩在线免费高清视频| 丰满人妻一区二区| 欧美艳星brazzers| 九九热视频精品| 国产日产欧产精品推荐色| 人妻激情偷乱视频一区二区三区| 国产日本精品| 日韩video| 国产精品一区高清| 成人在线免费观看一区| 99久久久国产精品免费调教网站 | 久久精品亚洲无码| 国产精品天干天干在线综合| 在线观看一区二区三区四区| 美女免费视频一区| 国产视频九色蝌蚪| 91tv官网精品成人亚洲| 麻豆亚洲一区| 亚洲一区二区免费在线观看| 国产欧美日韩高清| 欧美1级2级| 国内精品美女av在线播放| 午夜伦理在线| 亚洲欧美在线播放| www黄色网址| 欧美精品色综合| 精品国产乱子伦| 午夜伦欧美伦电影理论片| 麻豆明星ai换脸视频| 久久久电影一区二区三区| 久久国产劲爆∧v内射| 国产尤物一区二区在线| 天天色综合天天色| 久久激情综合| 激情综合在线观看| 最新国产乱人伦偷精品免费网站| 懂色av粉嫩av蜜臀av| 色综合咪咪久久网| 亚洲欧洲免费无码| 精品视频亚洲| 日韩久久久久久久| 国产成人三级| 秋霞毛片久久久久久久久| 欧美人成在线观看ccc36| 97国产超碰| 老司机亚洲精品一区二区| 成人av在线亚洲| 日本美女久久| 国产精品欧美激情| 日韩经典一区| 国产精品流白浆视频| 欧美成人性网| 国产精品va在线| 惠美惠精品网| 国产福利视频一区二区| 韩国成人漫画| 国产精品成人av性教育| 自拍偷自拍亚洲精品被多人伦好爽| 97超级碰在线看视频免费在线看 | 国产精品18毛片一区二区| 亚洲午夜免费| 国产精品视频免费观看| 北条麻妃一区二区三区在线观看| 91久久精品国产91久久性色tv| 日韩一区二区三区色| 99在线热播| 久久久久久毛片免费看| 久久亚洲一区二区| 国产a久久精品一区二区三区 | av每日在线更新| 精品国内自产拍在线观看| 羞羞污视频在线观看| 隔壁老王国产在线精品| 涩涩视频在线| 国产精品久久久久久久美男| 亚洲精品成a人ⅴ香蕉片| 91福利入口| 国产精品乱战久久久| 免费在线成人av| 人人狠狠综合久久亚洲婷婷| 这里只有精品66| 国产一区清纯| aaaaaa亚洲| 久久91精品久久久久久秒播| 国产伦精品一区二区三区妓女下载 | 爱久久·www| 蜜臀久久99精品久久久无需会员 | 91中文字精品一区二区| 美女午夜精品| 亚洲国产欧美一区二区三区不卡| 天堂美国久久| 日本在线xxx| 蜜臀av一区二区三区| 日韩大尺度视频| 久久日韩粉嫩一区二区三区| 日韩欧美视频免费观看| 亚洲一区二区三区视频在线| jizz国产在线观看| 日韩亚洲欧美一区二区三区| 三区在线观看| 久久视频国产精品免费视频在线| 阿v视频在线| 成人亚洲激情网| 亚洲精品白浆高清| 法国空姐在线观看免费| 美女爽到呻吟久久久久| 四川一级毛毛片| 国产欧美日韩卡一| 日韩精品久久久久久久| 欧美日韩国产高清一区二区三区| 视频污在线观看| 久久九九全国免费精品观看| 综合久久2023| 成人做爰66片免费看网站| 日本激情一区| 男人操女人逼免费视频| 国产老女人精品毛片久久| 国产精品美女高潮无套| 午夜精品福利一区二区三区蜜桃| 国产一区二区网站| 日韩精品视频在线观看免费| 成人影院在线观看| 国产精品免费在线免费 | 国产精品3区| 日韩欧美视频第二区| 一区二区三区国产盗摄| 欧美69精品久久久久久不卡| 国产精品久久久久久久久晋中| av大全在线观看| 亚洲国产精久久久久久久| 最爽无遮挡行房视频在线| 国产精品羞羞答答| 国产在视频线精品视频www666| 欧美成人高潮一二区在线看| 国产乱妇无码大片在线观看| 日韩三级久久久| 欧美日韩在线一区二区| 久久av少妇| 欧美在线影院在线视频| 里番精品3d一二三区| 福利视频免费在线观看| 国产成人综合在线播放| 老熟妇高潮一区二区三区| 欧美日韩色综合| 99精品老司机免费视频| 国产精品大片wwwwww| 精品视频免费| 精品日韩久久久| 国产三级三级三级精品8ⅰ区| 欧美国产成人精品一区二区三区| 亚洲第一网站免费视频| 91超碰在线免费| 国产午夜精品一区| 亚洲国产激情| 99久久免费看精品国产一区| 精品久久久久久久久中文字幕| 人妻va精品va欧美va| 午夜精品久久久久久久99热浪潮| 久久99偷拍| 久草资源站在线观看| 久久婷婷久久一区二区三区| 亚洲欧美综合自拍| 亚洲午夜精品久久久久久久久久久久| 竹内纱里奈兽皇系列在线观看 | 中文字幕一区二区三区乱码不卡| 亚洲一级不卡视频| 四虎在线视频免费观看| 91精品国产色综合| 综合亚洲自拍| 国产一区二区在线免费播放| 国产精品国产三级国产普通话三级 | 日韩国产高清影视| 一级性生活免费视频| 日韩丝袜美女视频| av女在线播放| 欧洲精品国产| 久久99热这里只有精品| 国产探花在线播放| 日韩精品在线私人| 国产一区二区三区四区五区3d| 在线观看视频黄色| 成人h版在线观看| 欧美一区二区三区久久久| 中文字幕亚洲一区二区三区| 精品视频一区二区三区| 欧美一级视频免费看| 国产午夜精品美女毛片视频| 92久久精品一区二区| 久久全国免费视频| 精品日本12videosex| 色哟哟免费视频| 欧美日韩在线第一页| 免费在线你懂的| 国产亚洲情侣一区二区无| 日本视频中文字幕一区二区三区| 三级影片在线看| 亚洲精品美女久久久| 久久伊人国产| av动漫在线看| 亚洲欧美日韩久久| 黑人与亚洲人色ⅹvideos| 91免费版网站在线观看| 日韩精彩视频在线观看| 青娱乐国产盛宴| 亚洲欧美日韩另类| 日本在线成人| 亚洲色图久久久| 欧美日韩亚洲一区二区| 国产婷婷视频在线| 日本午夜精品一区二区| 成人精品免费网站| 91精品国产乱码久久久|